// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import { createLogger } from '../common/logger.js';
const logger = createLogger({ component: 'rate-limit' });
/**
 * Default tier configurations
 */
export const DEFAULT_TIER_LIMITS = {
    free: {
        requestsPerMinute: 60,
        requestsPerHour: 1000,
        burstLimit: 10,
    },
    pro: {
        requestsPerMinute: 300,
        requestsPerHour: 10000,
        burstLimit: 50,
    },
    enterprise: {
        requestsPerMinute: 1000,
        requestsPerHour: 50000,
        burstLimit: 100,
    },
    unlimited: {
        requestsPerMinute: Infinity,
        requestsPerHour: Infinity,
        burstLimit: Infinity,
    },
};
/**
 * Per-Tenant Rate Limiter
 */
export class TenantRateLimiter {
    state = new Map();
    tierLimits;
    cleanupInterval = null;
    constructor(tierLimits = DEFAULT_TIER_LIMITS) {
        this.tierLimits = tierLimits;
        this.startCleanup();
    }
    /**
     * Check and consume rate limit for a tenant
     */
    check(tenantId, tier = 'free') {
        const now = Date.now();
        const limits = this.tierLimits[tier] ?? this.tierLimits['free'];
        // Get or create tenant state
        let state = this.state.get(tenantId);
        if (!state) {
            state = this.createInitialState(tier, now);
            this.state.set(tenantId, state);
        }
        // Update tier if changed
        if (state.tier !== tier) {
            state.tier = tier;
        }
        // Reset windows if expired
        this.resetExpiredWindows(state, now);
        // Check limits
        const secondRemaining = limits.burstLimit - state.second.count;
        const minuteRemaining = limits.requestsPerMinute - state.minute.count;
        const hourRemaining = limits.requestsPerHour - state.hour.count;
        const allowed = secondRemaining > 0 && minuteRemaining > 0 && hourRemaining > 0;
        if (allowed) {
            // Consume one request
            state.second.count++;
            state.minute.count++;
            state.hour.count++;
        }
        const result = {
            allowed,
            remaining: {
                minute: Math.max(0, minuteRemaining - (allowed ? 1 : 0)),
                hour: Math.max(0, hourRemaining - (allowed ? 1 : 0)),
                burst: Math.max(0, secondRemaining - (allowed ? 1 : 0)),
            },
            resetAt: {
                minute: state.minute.resetAt,
                hour: state.hour.resetAt,
                burst: state.second.resetAt,
            },
        };
        if (!allowed) {
            // Calculate retry-after based on which limit was hit
            if (secondRemaining <= 0) {
                result.retryAfter = Math.ceil((state.second.resetAt - now) / 1000);
            }
            else if (minuteRemaining <= 0) {
                result.retryAfter = Math.ceil((state.minute.resetAt - now) / 1000);
            }
            else {
                result.retryAfter = Math.ceil((state.hour.resetAt - now) / 1000);
            }
            logger.warn({
                tenantId,
                tier,
                remaining: result.remaining,
                retryAfter: result.retryAfter,
            }, 'Rate limit exceeded');
        }
        return result;
    }
    /**
     * Get current state for a tenant (for monitoring)
     */
    getState(tenantId) {
        const state = this.state.get(tenantId);
        if (!state)
            return null;
        const limits = this.tierLimits[state.tier] ?? this.tierLimits['free'];
        return {
            tier: state.tier,
            usage: {
                second: state.second.count,
                minute: state.minute.count,
                hour: state.hour.count,
            },
            limits,
        };
    }
    /**
     * Reset rate limit for a tenant
     */
    reset(tenantId) {
        this.state.delete(tenantId);
        logger.info({ tenantId }, 'Rate limit reset');
    }
    /**
     * Update tier limits
     */
    setTierLimits(tier, limits) {
        this.tierLimits[tier] = limits;
        logger.info({ tier, limits }, 'Tier limits updated');
    }
    /**
     * Create initial state for a tenant
     */
    createInitialState(tier, now) {
        return {
            minute: { count: 0, resetAt: now + 60000 },
            hour: { count: 0, resetAt: now + 3600000 },
            second: { count: 0, resetAt: now + 1000 },
            tier,
        };
    }
    /**
     * Reset expired windows
     */
    resetExpiredWindows(state, now) {
        if (now >= state.second.resetAt) {
            state.second = { count: 0, resetAt: now + 1000 };
        }
        if (now >= state.minute.resetAt) {
            state.minute = { count: 0, resetAt: now + 60000 };
        }
        if (now >= state.hour.resetAt) {
            state.hour = { count: 0, resetAt: now + 3600000 };
        }
    }
    /**
     * Start periodic cleanup of stale entries
     */
    startCleanup() {
        // Clean up every 5 minutes
        this.cleanupInterval = setInterval(() => {
            const now = Date.now();
            const staleThreshold = now - 3600000; // 1 hour
            for (const [tenantId, state] of this.state.entries()) {
                // Remove if all windows have been expired for over an hour
                if (state.second.resetAt < staleThreshold &&
                    state.minute.resetAt < staleThreshold &&
                    state.hour.resetAt < staleThreshold) {
                    this.state.delete(tenantId);
                }
            }
        }, 300000);
        // Don't prevent process exit
        this.cleanupInterval.unref();
    }
    /**
     * Stop the rate limiter
     */
    stop() {
        if (this.cleanupInterval) {
            clearInterval(this.cleanupInterval);
            this.cleanupInterval = null;
        }
    }
    /**
     * Get statistics
     */
    getStats() {
        const byTier = {};
        for (const state of this.state.values()) {
            byTier[state.tier] = (byTier[state.tier] ?? 0) + 1;
        }
        return {
            activeTenants: this.state.size,
            byTier,
        };
    }
}
// Singleton instance
let rateLimiter = null;
/**
 * Get the rate limiter singleton
 */
export function getRateLimiter() {
    if (!rateLimiter) {
        rateLimiter = new TenantRateLimiter();
    }
    return rateLimiter;
}
/**
 * Create rate limit middleware for Fastify
 */
export function createRateLimitMiddleware(options) {
    const limiter = options?.tierLimits
        ? new TenantRateLimiter(options.tierLimits)
        : getRateLimiter();
    const getTier = options?.getTier ?? ((req) => req.auth?.roles?.includes('enterprise')
        ? 'enterprise'
        : req.auth?.roles?.includes('pro')
            ? 'pro'
            : 'free');
    const skip = options?.skip ?? (() => false);
    return async (request, reply) => {
        // Skip if configured
        if (skip(request)) {
            return;
        }
        // Skip if not authenticated (auth middleware will handle)
        if (!request.auth?.tenantId) {
            return;
        }
        const tenantId = request.auth.tenantId;
        const tier = getTier(request);
        const result = limiter.check(tenantId, tier);
        // Set rate limit headers
        reply.header('X-RateLimit-Limit-Minute', result.remaining.minute + (result.allowed ? 1 : 0));
        reply.header('X-RateLimit-Remaining-Minute', result.remaining.minute);
        reply.header('X-RateLimit-Reset-Minute', Math.ceil(result.resetAt.minute / 1000));
        reply.header('X-RateLimit-Limit-Hour', result.remaining.hour + (result.allowed ? 1 : 0));
        reply.header('X-RateLimit-Remaining-Hour', result.remaining.hour);
        if (!result.allowed) {
            reply.header('Retry-After', result.retryAfter);
            reply.status(429).send({
                error: {
                    code: 'RATE_LIMIT_EXCEEDED',
                    message: 'Too many requests',
                    retryAfter: result.retryAfter,
                    limits: {
                        minute: result.remaining.minute,
                        hour: result.remaining.hour,
                    },
                },
            });
            return;
        }
    };
}
// Cache for route-specific rate limiters to avoid creating new ones on each request
// This prevents memory leaks from creating new TenantRateLimiter instances with intervals
const routeLimiterCache = new Map();
/**
 * Rate limit decorator for specific routes
 */
export function withRateLimit(customLimits) {
    return async (request, reply) => {
        if (!request.auth?.tenantId) {
            return;
        }
        // Get tier for limit calculation
        const tier = request.auth.roles?.includes('enterprise')
            ? 'enterprise'
            : request.auth.roles?.includes('pro')
                ? 'pro'
                : 'free';
        // Apply stricter limits if specified
        if (customLimits) {
            const baseLimits = DEFAULT_TIER_LIMITS[tier] ?? DEFAULT_TIER_LIMITS['free'];
            const effectiveLimits = {
                requestsPerMinute: Math.min(customLimits.requestsPerMinute ?? Infinity, baseLimits.requestsPerMinute),
                requestsPerHour: Math.min(customLimits.requestsPerHour ?? Infinity, baseLimits.requestsPerHour),
                burstLimit: Math.min(customLimits.burstLimit ?? Infinity, baseLimits.burstLimit),
            };
            // Create route-specific key
            const routeKey = `${request.auth.tenantId}:${request.routeOptions.url}`;
            // Create a cache key based on the effective limits
            const limiterCacheKey = `${effectiveLimits.requestsPerMinute}:${effectiveLimits.requestsPerHour}:${effectiveLimits.burstLimit}`;
            // Reuse existing limiter or create a new one (avoids memory leak from creating intervals)
            let customLimiter = routeLimiterCache.get(limiterCacheKey);
            if (!customLimiter) {
                customLimiter = new TenantRateLimiter({
                    custom: effectiveLimits,
                });
                routeLimiterCache.set(limiterCacheKey, customLimiter);
            }
            const result = customLimiter.check(routeKey, 'custom');
            if (!result.allowed) {
                reply.header('Retry-After', result.retryAfter);
                reply.status(429).send({
                    error: {
                        code: 'RATE_LIMIT_EXCEEDED',
                        message: 'Too many requests for this endpoint',
                        retryAfter: result.retryAfter,
                    },
                });
                return;
            }
        }
    };
}
/**
 * Stop the rate limiter singleton and cleanup route-specific limiters
 */
export function stopRateLimiter() {
    if (rateLimiter) {
        rateLimiter.stop();
        rateLimiter = null;
    }
    // Stop all cached route-specific limiters
    for (const limiter of routeLimiterCache.values()) {
        limiter.stop();
    }
    routeLimiterCache.clear();
    logger.info('Rate limiters stopped and cleaned up');
}
//# sourceMappingURL=rate-limit.js.map