import type Redis from 'ioredis';
import { RateLimiterRedis, type IRateLimiterRedisOptions } from 'rate-limiter-flexible';
import type { RateLimitRule, RateLimitWindow } from './types';

// Logger interface for type safety
interface Logger {
  info(message: string, meta?: Record<string, unknown>): void;
  error(message: string, meta?: Record<string, unknown>): void;
  warn(message: string, meta?: Record<string, unknown>): void;
  debug(message: string, meta?: Record<string, unknown>): void;
  trace(message: string, meta?: Record<string, unknown>): void;
}

interface RateLimiterEntry {
  windows: Array<{
    limiter: RateLimiterRedis;
    window: RateLimitWindow;
  }>;
  cost: number;
}

export interface RateLimitResult {
  allowed: boolean;
  retryDelayMs?: number;
  remainingPoints?: number;
  cost?: number;
}

/**
 * Enhanced rate limiter that supports:
 * - Multiple time windows per rule
 * - Variable costs per operation
 * - Hierarchical rules (operation > handler > queue > global)
 */
export class QueueRateLimiter {
  private limiters = new Map<string, RateLimiterEntry>();
  private readonly logger: Logger;

  constructor(
    private redisClient: Redis,
    logger?: Logger
  ) {
    this.logger = logger || console;
    this.logger.debug('QueueRateLimiter initialized', {
      redisClientType: typeof this.redisClient,
      redisClientKeys: this.redisClient ? Object.keys(this.redisClient).slice(0, 10) : [],
      hasClient: !!this.redisClient,
      isIoRedis: this.redisClient && this.redisClient.constructor.name === 'Redis'
    });
  }

  /**
   * Add a rate limit rule
   */
  async addRule(rule: RateLimitRule): Promise<void> {
    const key = this.getRuleKey(rule);
    
    // Create array of limiters for each window
    const windows: RateLimiterEntry['windows'] = [];
    const limits = rule.config.limits || [];
    
    this.logger.debug('Adding rate limit rule', {
      key,
      rule,
      windowCount: limits.length,
      cost: rule.config.cost
    });
    
    for (const window of limits) {
      const limiterOptions: IRateLimiterRedisOptions = {
        storeClient: this.redisClient as any,
        keyPrefix: `rate_limit:${key}:${window.duration}s`,
        points: window.points,
        duration: window.duration,
        blockDuration: window.blockDuration || 0,
      };
      
      this.logger.debug('Creating RateLimiterRedis', {
        keyPrefix: limiterOptions.keyPrefix,
        points: limiterOptions.points,
        duration: limiterOptions.duration,
        hasRedisClient: !!this.redisClient,
        redisClientType: typeof this.redisClient
      });
      
      try {
        const limiter = new RateLimiterRedis(limiterOptions);
        windows.push({ limiter, window });
      } catch (error) {
        this.logger.error('Failed to create RateLimiterRedis', { error, limiterOptions });
        throw error;
      }
    }
    
    // Default cost: handler defines the pool (no default cost), operations default to 1
    const defaultCost = rule.level === 'operation' ? 1 : 0;
    
    this.limiters.set(key, { 
      windows,
      cost: rule.config.cost !== undefined ? rule.config.cost : defaultCost
    });
    
    this.logger.debug('Rate limit rule added', {
      key,
      level: rule.level,
      handler: rule.handler,
      operation: rule.operation,
      windows: windows.length,
      cost: rule.config.cost !== undefined ? rule.config.cost : defaultCost
    });
  }

  /**
   * Check if an operation is allowed based on rate limits
   */
  async checkLimit(handler: string, operation?: string): Promise<RateLimitResult> {
    // Build keys to check from most specific to least specific
    const keysToCheck: string[] = [];
    
    if (operation) {
      keysToCheck.push(this.getRuleKey({ level: 'operation', handler, operation, config: {} } as RateLimitRule));
    }
    keysToCheck.push(this.getRuleKey({ level: 'handler', handler, config: {} } as RateLimitRule));
    keysToCheck.push(this.getRuleKey({ level: 'queue', config: {} } as RateLimitRule));
    keysToCheck.push(this.getRuleKey({ level: 'global', config: {} } as RateLimitRule));
    
    this.logger.debug('Checking rate limits', {
      handler,
      operation,
      keysToCheck,
      availableKeys: Array.from(this.limiters.keys())
    });
    
    // First, find the cost from the most specific rule
    let cost = 1; // Default cost
    for (const key of keysToCheck) {
      const entry = this.limiters.get(key);
      if (entry && entry.cost !== undefined) {
        cost = entry.cost;
        this.logger.debug(`[RateLimiter] Using cost ${cost} from rule: ${key}`);
        this.logger.debug('Using cost from rule', { 
          key, 
          cost,
          handler,
          operation 
        });
        break;
      }
    }
    
    // Then find the rate limit windows from the first matching rule that has windows
    let windowEntry: RateLimiterEntry | undefined;
    let appliedKey: string | undefined;
    
    for (const key of keysToCheck) {
      const entry = this.limiters.get(key);
      if (entry && entry.windows.length > 0) {
        windowEntry = entry;
        appliedKey = key;
        break;
      }
    }
    
    if (!windowEntry) {
      this.logger.debug('No rate limit rules found', {
        handler,
        operation
      });
      return { allowed: true };
    }
    
    this.logger.debug(`[RateLimiter] Applying rate limit rule: ${appliedKey}`, {
      appliedKey,
      windowCount: windowEntry.windows.length,
      cost,
      handler,
      operation
    });

    // Check all windows with the determined cost
    let maxRetryDelayMs = 0;
    
    this.logger.debug('Checking rate limit windows', {
      handler,
      operation,
      cost,
      windowCount: windowEntry.windows.length
    });
    
    // Special handling for 0-cost operations - they should always pass
    if (cost === 0) {
      this.logger.debug(`[RateLimiter] Zero-cost operation ${handler}:${operation || 'N/A'} - allowing without consuming points`);
      return {
        allowed: true,
        cost: 0
      };
    }
    
    for (const { limiter, window } of windowEntry.windows) {
      // For handler-level rate limits, all operations share the same pool
      // Use the applied key to determine the consumer key
      const consumerKey = appliedKey?.startsWith('handler:') ? handler : 
                         (operation ? `${handler}:${operation}` : handler);
      
      try {
        // First check current state without consuming
        const currentState = await limiter.get(consumerKey);
        const now = Date.now();
        this.logger.debug(`[RateLimiter] Current state before consume:`, {
          handler,
          operation,
          consumerKey,
          cost,
          window: `${window.points}pts/${window.duration}s`,
          consumedPoints: currentState?.consumedPoints || 0,
          remainingPoints: currentState?.remainingPoints ?? window.points,
          msBeforeNext: currentState?.msBeforeNext || 0,
          currentTime: new Date(now).toISOString(),
          resetTime: currentState?.msBeforeNext ? new Date(now + currentState.msBeforeNext).toISOString() : 'N/A',
          hasState: !!currentState,
          stateDetails: currentState ? JSON.stringify(currentState) : 'no state'
        });
        
        // Try to consume points
        this.logger.debug(`[RateLimiter] Attempting to consume ${cost} points for ${consumerKey}`);
        const result = await limiter.consume(consumerKey, cost);
        this.logger.debug(`[RateLimiter] Successfully consumed ${cost} points, ${result.remainingPoints} remaining`);
        this.logger.debug('Consumed points successfully', {
          handler,
          operation,
          consumerKey,
          window: `${window.points}pts/${window.duration}s`,
          cost,
          remainingPoints: result.remainingPoints,
          msBeforeNext: result.msBeforeNext
        });
        this.logger.trace('Rate limit window passed', {
          handler,
          operation,
          window: `${window.points}pts/${window.duration}s`,
          cost
        });
      } catch (rejRes: any) {
        // Rate limit exceeded for this window
        const retryDelayMs = rejRes.msBeforeNext || window.blockDuration || 1000;
        maxRetryDelayMs = Math.max(maxRetryDelayMs, retryDelayMs);
        
        this.logger.debug(`[RateLimiter] RATE LIMIT HIT: ${consumerKey} - ${cost} points rejected, retry in ${retryDelayMs}ms`, {
          consumedPoints: rejRes.consumedPoints || 0,
          remainingPoints: rejRes.remainingPoints || 0,
          totalPoints: rejRes.totalPoints || window.points,
          isFirstInDuration: rejRes.isFirstInDuration || false,
          msBeforeNext: rejRes.msBeforeNext || 0,
          retryDelayMs
        });
        this.logger.trace('Rate limit exceeded', {
          handler,
          operation,
          consumerKey,
          window: `${window.points}pts/${window.duration}s`,
          cost,
          remainingPoints: rejRes.remainingPoints || 0,
          retryDelayMs: maxRetryDelayMs
        });
        
        return {
          allowed: false,
          retryDelayMs: maxRetryDelayMs,
          remainingPoints: rejRes.remainingPoints || 0,
          cost
        };
      }
    }

    // All windows passed
    return {
      allowed: true,
      cost
    };
  }

  /**
   * Get rule key for storing rate limiter
   */
  private getRuleKey(rule: RateLimitRule): string {
    switch (rule.level) {
      case 'global':
        return 'global';
      case 'queue':
        return `queue:${rule.queueName || 'default'}`;
      case 'handler':
        return `handler:${rule.handler}`;
      case 'operation':
        return `operation:${rule.handler}:${rule.operation}`;
      default:
        return rule.level;
    }
  }

  /**
   * Get current rate limit status
   */
  async getStatus(handler: string, operation?: string): Promise<any> {
    // Build keys to check from most specific to least specific
    const keysToCheck: string[] = [];
    
    if (operation) {
      keysToCheck.push(this.getRuleKey({ level: 'operation', handler, operation, config: {} } as RateLimitRule));
    }
    keysToCheck.push(this.getRuleKey({ level: 'handler', handler, config: {} } as RateLimitRule));
    keysToCheck.push(this.getRuleKey({ level: 'queue', config: {} } as RateLimitRule));
    keysToCheck.push(this.getRuleKey({ level: 'global', config: {} } as RateLimitRule));
    
    // Find the first matching rule
    let entry: RateLimiterEntry | undefined;
    let appliedKey: string | undefined;
    
    for (const key of keysToCheck) {
      entry = this.limiters.get(key);
      if (entry) {
        appliedKey = key;
        break;
      }
    }
    
    if (!entry) {
      return {
        handler,
        operation,
        hasRule: false
      };
    }

    // Get status for all windows
    const consumerKey = operation ? `${handler}:${operation}` : handler;
    const windows = await Promise.all(
      entry.windows.map(async ({ limiter, window }) => {
        const result = await limiter.get(consumerKey);
        return {
          points: window.points,
          duration: window.duration,
          remaining: result?.remainingPoints ?? window.points,
          resetIn: result?.msBeforeNext ?? 0,
        };
      })
    );

    return {
      handler,
      operation,
      hasRule: true,
      appliedKey,
      cost: entry.cost,
      windows
    };
  }

  /**
   * Reset rate limits for a specific handler/operation
   */
  async reset(handler: string, operation?: string): Promise<void> {
    const keysToCheck: string[] = [];
    
    if (operation) {
      keysToCheck.push(this.getRuleKey({ level: 'operation', handler, operation, config: {} } as RateLimitRule));
    }
    keysToCheck.push(this.getRuleKey({ level: 'handler', handler, config: {} } as RateLimitRule));
    
    for (const key of keysToCheck) {
      const entry = this.limiters.get(key);
      if (entry) {
        await Promise.all(
          entry.windows.map(({ limiter }) => limiter.delete(handler))
        );
      }
    }
    
    this.logger.debug('Rate limits reset', { handler, operation });
  }
}