import { logger } from '../../logger.js';

/**
 * Configuration options for rate limiter
 */
export interface IRateLimitConfig {
  /** Maximum tokens per period */
  maxPerPeriod: number;
  
  /** Time period in milliseconds */
  periodMs: number;
  
  /** Whether to apply per domain/key (vs globally) */
  perKey: boolean;
  
  /** Initial token count (defaults to max) */
  initialTokens?: number;
  
  /** Grace tokens to allow occasional bursts */
  burstTokens?: number;
  
  /** Apply global limit in addition to per-key limits */
  useGlobalLimit?: boolean;
}

/**
 * Token bucket for an individual key
 */
interface TokenBucket {
  /** Current number of tokens */
  tokens: number;

  /** Last time tokens were refilled */
  lastRefill: number;

  /** Total allowed requests */
  allowed: number;

  /** Total denied requests */
  denied: number;

  /** Error count for blocking decisions */
  errors: number;

  /** Timestamp of first error in current window */
  firstErrorTime: number;
}

/**
 * Rate limiter using token bucket algorithm
 * Provides more sophisticated rate limiting with burst handling
 */
export class RateLimiter {
  /** Rate limit configuration */
  private config: IRateLimitConfig;
  
  /** Token buckets per key */
  private buckets: Map<string, TokenBucket> = new Map();
  
  /** Global bucket for non-keyed rate limiting */
  private globalBucket: TokenBucket;
  
  /**
   * Create a new rate limiter
   * @param config Rate limiter configuration
   */
  constructor(config: IRateLimitConfig) {
    // Set defaults
    this.config = {
      maxPerPeriod: config.maxPerPeriod,
      periodMs: config.periodMs,
      perKey: config.perKey ?? true,
      initialTokens: config.initialTokens ?? config.maxPerPeriod,
      burstTokens: config.burstTokens ?? 0,
      useGlobalLimit: config.useGlobalLimit ?? false
    };
    
    // Initialize global bucket
    this.globalBucket = {
      tokens: this.config.initialTokens,
      lastRefill: Date.now(),
      allowed: 0,
      denied: 0,
      errors: 0,
      firstErrorTime: 0
    };
    
    // Log initialization
    logger.log('info', `Rate limiter initialized: ${this.config.maxPerPeriod} per ${this.config.periodMs}ms${this.config.perKey ? ' per key' : ''}`);
  }
  
  /**
   * Check if a request is allowed under rate limits
   * @param key Key to check rate limit for (e.g. domain, user, IP)
   * @param cost Token cost (defaults to 1)
   * @returns Whether the request is allowed
   */
  public isAllowed(key: string = 'global', cost: number = 1): boolean {
    // If using global bucket directly, just check that
    if (key === 'global' || !this.config.perKey) {
      return this.checkBucket(this.globalBucket, cost);
    }
    
    // Get the key-specific bucket
    const bucket = this.getBucket(key);
    
    // If we also need to check global limit
    if (this.config.useGlobalLimit) {
      // Both key bucket and global bucket must have tokens
      return this.checkBucket(bucket, cost) && this.checkBucket(this.globalBucket, cost);
    } else {
      // Only need to check the key-specific bucket
      return this.checkBucket(bucket, cost);
    }
  }
  
  /**
   * Check if a bucket has enough tokens and consume them
   * @param bucket The token bucket to check
   * @param cost Token cost
   * @returns Whether tokens were consumed
   */
  private checkBucket(bucket: TokenBucket, cost: number): boolean {
    // Refill tokens based on elapsed time
    this.refillBucket(bucket);
    
    // Check if we have enough tokens
    if (bucket.tokens >= cost) {
      // Use tokens
      bucket.tokens -= cost;
      bucket.allowed++;
      return true;
    } else {
      // Rate limit exceeded
      bucket.denied++;
      return false;
    }
  }
  
  /**
   * Consume tokens for a request (if available)
   * @param key Key to consume tokens for
   * @param cost Token cost (defaults to 1)
   * @returns Whether tokens were consumed
   */
  public consume(key: string = 'global', cost: number = 1): boolean {
    const isAllowed = this.isAllowed(key, cost);
    return isAllowed;
  }
  
  /**
   * Get the remaining tokens for a key
   * @param key Key to check
   * @returns Number of remaining tokens
   */
  public getRemainingTokens(key: string = 'global'): number {
    const bucket = this.getBucket(key);
    this.refillBucket(bucket);
    return bucket.tokens;
  }
  
  /**
   * Get stats for a specific key
   * @param key Key to get stats for
   * @returns Rate limit statistics
   */
  public getStats(key: string = 'global'): {
    remaining: number;
    limit: number;
    resetIn: number;
    allowed: number;
    denied: number;
  } {
    const bucket = this.getBucket(key);
    this.refillBucket(bucket);
    
    // Calculate time until next token
    const resetIn = bucket.tokens < this.config.maxPerPeriod ?
      Math.ceil(this.config.periodMs / this.config.maxPerPeriod) :
      0;
    
    return {
      remaining: bucket.tokens,
      limit: this.config.maxPerPeriod,
      resetIn,
      allowed: bucket.allowed,
      denied: bucket.denied
    };
  }
  
  /**
   * Get or create a token bucket for a key
   * @param key The rate limit key
   * @returns Token bucket
   */
  private getBucket(key: string): TokenBucket {
    if (!this.config.perKey || key === 'global') {
      return this.globalBucket;
    }
    
    if (!this.buckets.has(key)) {
      // Create new bucket
      this.buckets.set(key, {
        tokens: this.config.initialTokens,
        lastRefill: Date.now(),
        allowed: 0,
        denied: 0,
        errors: 0,
        firstErrorTime: 0
      });
    }
    
    return this.buckets.get(key);
  }
  
  /**
   * Refill tokens in a bucket based on elapsed time
   * @param bucket Token bucket to refill
   */
  private refillBucket(bucket: TokenBucket): void {
    const now = Date.now();
    const elapsedMs = now - bucket.lastRefill;
    
    // Calculate how many tokens to add
    const rate = this.config.maxPerPeriod / this.config.periodMs;
    const tokensToAdd = elapsedMs * rate;
    
    if (tokensToAdd >= 0.1) { // Allow for partial token refills
      // Add tokens, but don't exceed the normal maximum (without burst)
      // This ensures burst tokens are only used for bursts and don't refill
      const normalMax = this.config.maxPerPeriod;
      bucket.tokens = Math.min(
        // Don't exceed max + burst
        this.config.maxPerPeriod + (this.config.burstTokens || 0), 
        // Don't exceed normal max when refilling
        Math.min(normalMax, bucket.tokens + tokensToAdd)
      );
      
      // Update last refill time
      bucket.lastRefill = now;
    }
  }
  
  /**
   * Reset rate limits for a specific key
   * @param key Key to reset
   */
  public reset(key: string = 'global'): void {
    if (key === 'global' || !this.config.perKey) {
      this.globalBucket.tokens = this.config.initialTokens;
      this.globalBucket.lastRefill = Date.now();
    } else if (this.buckets.has(key)) {
      const bucket = this.buckets.get(key);
      bucket.tokens = this.config.initialTokens;
      bucket.lastRefill = Date.now();
    }
  }
  
  /**
   * Reset all rate limiters
   */
  public resetAll(): void {
    this.globalBucket.tokens = this.config.initialTokens;
    this.globalBucket.lastRefill = Date.now();
    
    for (const bucket of this.buckets.values()) {
      bucket.tokens = this.config.initialTokens;
      bucket.lastRefill = Date.now();
    }
  }
  
  /**
   * Cleanup old buckets to prevent memory leaks
   * @param maxAge Maximum age in milliseconds
   */
  public cleanup(maxAge: number = 24 * 60 * 60 * 1000): void {
    const now = Date.now();
    let removed = 0;

    for (const [key, bucket] of this.buckets.entries()) {
      if (now - bucket.lastRefill > maxAge) {
        this.buckets.delete(key);
        removed++;
      }
    }

    if (removed > 0) {
      logger.log('debug', `Cleaned up ${removed} stale rate limit buckets`);
    }
  }

  /**
   * Record an error for a key (e.g., IP address) and determine if blocking is needed
   * RFC 5321 Section 4.5.4.1 suggests limiting errors to prevent abuse
   *
   * @param key Key to record error for (typically an IP address)
   * @param errorWindow Time window for error tracking in ms (default: 5 minutes)
   * @param errorThreshold Maximum errors before blocking (default: 10)
   * @returns true if the key should be blocked due to excessive errors
   */
  public recordError(key: string, errorWindow: number = 5 * 60 * 1000, errorThreshold: number = 10): boolean {
    const bucket = this.getBucket(key);
    const now = Date.now();

    // Reset error count if the time window has expired
    if (bucket.firstErrorTime === 0 || now - bucket.firstErrorTime > errorWindow) {
      bucket.errors = 0;
      bucket.firstErrorTime = now;
    }

    // Increment error count
    bucket.errors++;

    // Log error tracking
    logger.log('debug', `Error recorded for ${key}: ${bucket.errors}/${errorThreshold} in window`);

    // Check if threshold exceeded
    if (bucket.errors >= errorThreshold) {
      logger.log('warn', `Error threshold exceeded for ${key}: ${bucket.errors} errors`);
      return true; // Should block
    }

    return false; // Continue allowing
  }
}