import { RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
import type { RateLimitConfig as BaseRateLimitConfig, RateLimitRule } 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;
}

// Extend the base config to add rate-limiter specific fields
export interface RateLimitConfig extends BaseRateLimitConfig {
  keyPrefix?: string;
}

export class QueueRateLimiter {
  private limiters = new Map<string, RateLimiterRedis>();
  private rules: RateLimitRule[] = [];
  private readonly logger: Logger;

  constructor(
    private redisClient: ReturnType<typeof import('./utils').getRedisConnection>,
    logger?: Logger
  ) {
    this.logger = logger || console;
  }

  /**
   * Add a rate limit rule
   */
  addRule(rule: RateLimitRule): void {
    this.rules.push(rule);

    const key = this.getRuleKey(rule.level, rule.queueName, rule.handler, rule.operation);
    const limiter = new RateLimiterRedis({
      storeClient: this.redisClient,
      keyPrefix: `rl:${key}`,
      points: rule.config.points,
      duration: rule.config.duration,
      blockDuration: rule.config.blockDuration || 0,
    });

    this.limiters.set(key, limiter);

    this.logger.info('Rate limit rule added', {
      level: rule.level,
      queueName: rule.queueName,
      handler: rule.handler,
      operation: rule.operation,
      points: rule.config.points,
      duration: rule.config.duration,
    });
  }

  /**
   * Check if a job can be processed based on rate limits
   * Uses hierarchical precedence: operation > handler > queue > global
   * The most specific matching rule takes precedence
   */
  async checkLimit(
    queueName: string,
    handler: string,
    operation: string
  ): Promise<{
    allowed: boolean;
    retryAfter?: number;
    remainingPoints?: number;
    appliedRule?: RateLimitRule;
  }> {
    const applicableRule = this.getMostSpecificRule(queueName, handler, operation);

    if (!applicableRule) {
      return { allowed: true };
    }

    const key = this.getRuleKey(
      applicableRule.level,
      applicableRule.queueName,
      applicableRule.handler,
      applicableRule.operation
    );
    const limiter = this.limiters.get(key);

    if (!limiter) {
      this.logger.warn('Rate limiter not found for rule', { key, rule: applicableRule });
      return { allowed: true };
    }

    try {
      const result = await this.consumePoint(
        limiter,
        this.getConsumerKey(queueName, handler, operation)
      );

      return {
        ...result,
        appliedRule: applicableRule,
      };
    } catch (error) {
      this.logger.error('Rate limit check failed', { queueName, handler, operation, error });
      // On error, allow the request to proceed
      return { allowed: true };
    }
  }

  /**
   * Get the most specific rule that applies to this job
   * Precedence: operation > handler > queue > global
   */
  private getMostSpecificRule(
    queueName: string,
    handler: string,
    operation: string
  ): RateLimitRule | undefined {
    // 1. Check for operation-specific rule (most specific)
    let rule = this.rules.find(
      r =>
        r.level === 'operation' &&
        r.queueName === queueName &&
        r.handler === handler &&
        r.operation === operation
    );
    if (rule) {
      return rule;
    }

    // 2. Check for handler-specific rule
    rule = this.rules.find(
      r => r.level === 'handler' && r.queueName === queueName && r.handler === handler
    );
    if (rule) {
      return rule;
    }

    // 3. Check for queue-specific rule
    rule = this.rules.find(r => r.level === 'queue' && r.queueName === queueName);
    if (rule) {
      return rule;
    }

    // 4. Check for global rule (least specific)
    rule = this.rules.find(r => r.level === 'global');
    return rule;
  }

  /**
   * Consume a point from the rate limiter
   */
  private async consumePoint(
    limiter: RateLimiterRedis,
    key: string
  ): Promise<{ allowed: boolean; retryAfter?: number; remainingPoints?: number }> {
    try {
      const result = await limiter.consume(key);
      return {
        allowed: true,
        remainingPoints: result.remainingPoints,
      };
    } catch (rejRes) {
      if (rejRes instanceof RateLimiterRes) {
        this.logger.warn('Rate limit exceeded', {
          key,
          retryAfter: rejRes.msBeforeNext,
        });

        return {
          allowed: false,
          retryAfter: rejRes.msBeforeNext,
          remainingPoints: rejRes.remainingPoints,
        };
      }
      throw rejRes;
    }
  }

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

  /**
   * Get consumer key for rate limiting (what gets counted)
   */
  private getConsumerKey(queueName: string, handler: string, operation: string): string {
    return `${queueName}:${handler}:${operation}`;
  }

  /**
   * Get current rate limit status for a queue/handler/operation
   */
  async getStatus(
    queueName: string,
    handler: string,
    operation: string
  ): Promise<{
    queueName: string;
    handler: string;
    operation: string;
    appliedRule?: RateLimitRule;
    limit?: {
      level: string;
      points: number;
      duration: number;
      remaining: number;
      resetIn: number;
    };
  }> {
    const applicableRule = this.getMostSpecificRule(queueName, handler, operation);

    if (!applicableRule) {
      return {
        queueName,
        handler,
        operation,
      };
    }

    const key = this.getRuleKey(
      applicableRule.level,
      applicableRule.queueName,
      applicableRule.handler,
      applicableRule.operation
    );
    const limiter = this.limiters.get(key);

    if (!limiter) {
      return {
        queueName,
        handler,
        operation,
        appliedRule: applicableRule,
      };
    }

    try {
      const consumerKey = this.getConsumerKey(queueName, handler, operation);
      const result = await limiter.get(consumerKey);

      const limit = {
        level: applicableRule.level,
        points: limiter.points,
        duration: limiter.duration,
        remaining: result?.remainingPoints ?? limiter.points,
        resetIn: result?.msBeforeNext ?? 0,
      };

      return {
        queueName,
        handler,
        operation,
        appliedRule: applicableRule,
        limit,
      };
    } catch (error) {
      this.logger.error('Failed to get rate limit status', {
        queueName,
        handler,
        operation,
        error,
      });
      return {
        queueName,
        handler,
        operation,
        appliedRule: applicableRule,
      };
    }
  }

  /**
   * Reset rate limits for a specific consumer
   */
  async reset(queueName: string, handler?: string, operation?: string): Promise<void> {
    if (handler && operation) {
      // Reset specific operation
      const consumerKey = this.getConsumerKey(queueName, handler, operation);
      const rule = this.getMostSpecificRule(queueName, handler, operation);

      if (rule) {
        const key = this.getRuleKey(rule.level, rule.queueName, rule.handler, rule.operation);
        const limiter = this.limiters.get(key);
        if (limiter) {
          await limiter.delete(consumerKey);
        }
      }
    } else {
      // Reset broader scope - this is more complex with the new hierarchy
      this.logger.warn('Broad reset not implemented yet', { queueName, handler, operation });
    }

    this.logger.info('Rate limits reset', { queueName, handler, operation });
  }

  /**
   * Get all configured rate limit rules
   */
  getRules(): RateLimitRule[] {
    return [...this.rules];
  }

  /**
   * Remove a rate limit rule
   */
  removeRule(level: string, queueName?: string, handler?: string, operation?: string): boolean {
    const key = this.getRuleKey(level, queueName, handler, operation);
    const ruleIndex = this.rules.findIndex(
      r =>
        r.level === level &&
        (!queueName || r.queueName === queueName) &&
        (!handler || r.handler === handler) &&
        (!operation || r.operation === operation)
    );

    if (ruleIndex >= 0) {
      this.rules.splice(ruleIndex, 1);
      this.limiters.delete(key);

      this.logger.info('Rate limit rule removed', { level, queueName, handler, operation });
      return true;
    }

    return false;
  }
}
