/**
 * Robust rate limiter using both IP address and cookie-based tracking.
 * Implements sliding window with burst protection and automatic cleanup.
 */

export interface RateLimitEntry {
  count: number;
  resetTime: number;
}

export interface RateLimitOptions {
  limit?: number;
  windowMs?: number;
  burstLimit?: number;
  burstWindowMs?: number;
}

export interface RateLimitResult {
  allowed: boolean;
  ip: string;
  cookieId: string | null;
  isFlooding: boolean;
}

// Separate maps for IP and cookie tracking
const ipRateLimitMap: Map<string, RateLimitEntry> = new Map();
const cookieRateLimitMap: Map<string, RateLimitEntry> = new Map();

// Global flood protection - track overall request volume per IP
const floodProtectionMap: Map<string, RateLimitEntry> = new Map();

// Cleanup old entries every 60 seconds to prevent memory leaks
const CLEANUP_INTERVAL = 60_000;
let lastCleanup = Date.now();

// Trusted proxy configuration - only trust proxy headers from known sources
// Set TRUSTED_PROXY_IPS env var to comma-separated list of IPs/CIDRs
// If behind Cloudflare, Vercel, or similar, their proxy IPs are implicitly trusted
const TRUSTED_PROXIES = new Set(
    (import.meta.env.TRUSTED_PROXY_IPS || '')
        .split(',')
        .map(ip => ip.trim())
        .filter(Boolean)
);

// Common cloud provider proxy indicators
const VERCEL_INDICATOR = import.meta.env.VERCEL === '1';
const CLOUDFLARE_INDICATOR = typeof import.meta.env.CF_PAGES !== 'undefined' || 
                              typeof import.meta.env.CF_PAGES_URL !== 'undefined';

/**
 * Check if request is from a trusted proxy.
 * In production behind Vercel/Cloudflare, proxy headers are trustworthy.
 */
function isTrustedProxy(request: Request): boolean {
    // If running on Vercel or Cloudflare, trust their headers
    if (VERCEL_INDICATOR || CLOUDFLARE_INDICATOR) return true;
    
    // If specific trusted proxies are configured, check them
    if (TRUSTED_PROXIES.size > 0) {
        // In a real deployment, you'd check the connecting IP against trusted IPs
        // For now, if TRUSTED_PROXY_IPS is set, we trust the headers
        return true;
    }
    
    // Default: don't trust proxy headers (direct connection)
    return false;
}

function cleanupStaleEntries(): void {
    const now = Date.now();
    if (now - lastCleanup < CLEANUP_INTERVAL) return;
    lastCleanup = now;

    for (const [key, entry] of ipRateLimitMap) {
        if (now > entry.resetTime + 60_000) ipRateLimitMap.delete(key);
    }
    for (const [key, entry] of cookieRateLimitMap) {
        if (now > entry.resetTime + 60_000) cookieRateLimitMap.delete(key);
    }
    for (const [key, entry] of floodProtectionMap) {
        if (now > entry.resetTime + 60_000) floodProtectionMap.delete(key);
    }
}

/**
 * Extract client IP from request headers with proxy support.
 * Only trusts proxy headers when behind a known/configured proxy.
 */
export function getClientIp(request: Request): string {
    const headers = request.headers;
    
    // Only trust proxy headers if we're behind a trusted proxy
    if (isTrustedProxy(request)) {
        // Cloudflare's header is most reliable when using Cloudflare
        const cfConnectingIp = headers.get('cf-connecting-ip');
        if (cfConnectingIp && isValidIp(cfConnectingIp)) return normalizeIp(cfConnectingIp);

        // Vercel/standard proxy header
        const xForwardedFor = headers.get('x-forwarded-for');
        if (xForwardedFor) {
            // Take first IP (original client), trim whitespace
            const ip = xForwardedFor.split(',')[0].trim();
            if (ip && isValidIp(ip)) return normalizeIp(ip);
        }

        const xRealIp = headers.get('x-real-ip');
        if (xRealIp && isValidIp(xRealIp)) return normalizeIp(xRealIp);

        const trueClientIp = headers.get('true-client-ip');
        if (trueClientIp && isValidIp(trueClientIp)) return normalizeIp(trueClientIp);
    }

    // Fallback - in production this typically means misconfiguration
    // Log this case in development to catch configuration issues
    if (import.meta.env.DEV) {
        console.warn('[RateLimit] Could not determine client IP - proxy headers not trusted');
    }
    
    return 'unknown';
}

/**
 * Basic IP validation to prevent header injection attacks.
 */
function isValidIp(ip: string | null | undefined): boolean {
    if (!ip || typeof ip !== 'string') return false;
    // Basic sanity check - no weird characters, reasonable length
    if (ip.length > 45) return false; // Max IPv6 length
    if (!/^[\d.:a-fA-F]+$/.test(ip)) return false;
    return true;
}

/**
 * Normalize IP for consistent keying (lowercase, trim).
 */
function normalizeIp(ip: string): string {
    return ip.toLowerCase().trim();
}

/**
 * Extract nonce cookie from request.
 */
export function getCookieId(request: Request): string | null {
    const cookieHeader = request.headers.get('cookie');
    if (!cookieHeader) return null;

    const match = cookieHeader.split(';').find(c => c.trim().startsWith('nonce='));
    if (!match) return null;

    const value = match.split('=')[1]?.trim();
    // Validate UUID format loosely
    if (!value || value.length < 32 || value.length > 40) return null;
    return value;
}

/**
 * Check if an identifier is rate limited.
 */
function isLimited(map: Map<string, RateLimitEntry>, key: string, limit: number, windowMs: number): boolean {
    const now = Date.now();
    const entry = map.get(key);

    if (!entry || now > entry.resetTime) {
        return false; // Not limited, window expired or new
    }

    return entry.count >= limit;
}

/**
 * Record a request for rate limiting.
 */
function recordHit(map: Map<string, RateLimitEntry>, key: string, windowMs: number): void {
    const now = Date.now();
    const entry = map.get(key);

    if (!entry || now > entry.resetTime) {
        map.set(key, { count: 1, resetTime: now + windowMs });
    } else {
        entry.count++;
    }
}

/**
 * Get current count for an identifier.
 */
function getCount(map: Map<string, RateLimitEntry>, key: string): number {
    const now = Date.now();
    const entry = map.get(key);
    if (!entry || now > entry.resetTime) return 0;
    return entry.count;
}

/**
 * Check flood protection - aggressive rate limit for potential abuse.
 * This triggers when an IP sends too many requests in a short burst.
 */
function checkFloodProtection(ip: string, burstLimit: number = 30, burstWindowMs: number = 10_000): boolean {
    if (ip === 'unknown') return false; // Can't flood-protect unknown IPs effectively

    const now = Date.now();
    const entry = floodProtectionMap.get(ip);

    if (!entry || now > entry.resetTime) {
        floodProtectionMap.set(ip, { count: 1, resetTime: now + burstWindowMs });
        return false;
    }

    entry.count++;
    return entry.count > burstLimit;
}

/**
 * Main rate limit check combining IP and cookie tracking.
 * 
 * @param request - The incoming request
 * @param options - Rate limit configuration
 * @returns Object with allowed status, ip, cookieId, and flooding flag
 */
export function checkRateLimit(request: Request, options: RateLimitOptions = {}): RateLimitResult {
    const {
        limit = 5,
        windowMs = 1000,
        burstLimit = 30,
        burstWindowMs = 10_000,
    } = options;

    cleanupStaleEntries();

    const ip = getClientIp(request);
    const cookieId = getCookieId(request);

    // Check flood protection first (aggressive anti-abuse)
    const isFlooding = checkFloodProtection(ip, burstLimit, burstWindowMs);
    if (isFlooding) {
        return { allowed: false, ip, cookieId, isFlooding: true };
    }

    // Check both IP and cookie limits
    // Both must be under limit to allow the request
    const ipLimited = ip !== 'unknown' && isLimited(ipRateLimitMap, ip, limit, windowMs);
    const cookieLimited = cookieId && isLimited(cookieRateLimitMap, cookieId, limit, windowMs);

    // If either is limited, deny
    if (ipLimited || cookieLimited) {
        return { allowed: false, ip, cookieId, isFlooding: false };
    }

    return { allowed: true, ip, cookieId, isFlooding: false };
}

/**
 * Record a successful request for rate limiting tracking.
 * Call this after the request is processed.
 */
export function recordRequest(request: Request, windowMs: number = 1000): void {
    const ip = getClientIp(request);
    const cookieId = getCookieId(request);

    if (ip !== 'unknown') {
        recordHit(ipRateLimitMap, ip, windowMs);
    }
    if (cookieId) {
        recordHit(cookieRateLimitMap, cookieId, windowMs);
    }
}

/**
 * Generate a new nonce cookie value.
 */
export function generateNonce(): string {
    return crypto.randomUUID();
}

/**
 * Create Set-Cookie header value for nonce.
 */
export function createNonceCookie(nonce: string): string {
    return `nonce=${nonce}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=31536000`;
}

export default {
    checkRateLimit,
    recordRequest,
    getClientIp,
    getCookieId,
    generateNonce,
    createNonceCookie,
};
