import {
  Injectable,
  Inject,
  type CanActivate,
  type ExecutionContext,
  HttpException,
  HttpStatus,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible";
import type { Redis } from "ioredis";
import type { Request, Response } from "express";
import { Logger } from "nestjs-pino";
import { RATE_LIMIT_KEY, type RateLimitOptions } from "./rate-limit.decorator.js";
import { buildRateLimitKey, getRequestRateLimitIdentity } from "./rate-limit.util.js";

/**
 * Rate limit guard using rate-limiter-flexible with Redis backend.
 * Replaces @nestjs/throttler with a unified, more flexible solution.
 *
 * Features:
 * - Redis-backed for distributed rate limiting
 * - Per-IP + User-Agent tracking for better security
 * - In-memory blocking for reduced Redis load
 * - Standard rate limit headers (X-RateLimit-*)
 */
@Injectable()
export class RateLimitGuard implements CanActivate {
  private readonly limiters = new Map<string, RateLimiterRedis>();

  constructor(
    private readonly reflector: Reflector,
    @Inject("REDIS_CLIENT") private readonly redis: Redis,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Check for route-level options first, then controller-level
    const options =
      this.reflector.get<RateLimitOptions>(RATE_LIMIT_KEY, context.getHandler()) ??
      this.reflector.get<RateLimitOptions>(RATE_LIMIT_KEY, context.getClass());

    // No rate limit configured or explicitly skipped
    if (!options || options.skip) {
      return true;
    }

    const request = context.switchToHttp().getRequest<Request>();
    const response = context.switchToHttp().getResponse<Response>();

    const key = this.buildKey(request, context);
    const limiter = this.getOrCreateLimiter(options, context);

    try {
      const result = await limiter.consume(key);

      // Set standard rate limit headers
      this.setRateLimitHeaders(response, options, result);

      return true;
    } catch (error: unknown) {
      if (error instanceof RateLimiterRes) {
        this.setRateLimitHeaders(response, options, error, true);

        const retryAfterSeconds = Math.ceil(error.msBeforeNext / 1000);

        this.logger.warn(
          { key, retryAfter: retryAfterSeconds, path: request.path },
          "Rate limit exceeded"
        );

        throw new HttpException(
          {
            statusCode: HttpStatus.TOO_MANY_REQUESTS,
            message: `Too many requests. Please try again in ${retryAfterSeconds} seconds.`,
            error: "Too Many Requests",
            retryAfter: retryAfterSeconds,
          },
          HttpStatus.TOO_MANY_REQUESTS,
          {
            cause: error,
          }
        );
      }

      // Redis connection error - fail open with warning
      this.logger.error({ error, key }, "Rate limiter error - failing open");
      return true;
    }
  }

  /**
   * Build a unique key for rate limiting based on IP and User-Agent
   */
  private buildKey(request: Request, context: ExecutionContext): string {
    const { ip, userAgentHash } = getRequestRateLimitIdentity(request);

    // Use handler name as part of key to separate limits per endpoint
    const handlerName = context.getHandler().name;
    const controllerName = context.getClass().name;

    return buildRateLimitKey("rl", controllerName, handlerName, ip, userAgentHash);
  }

  /**
   * Get or create a rate limiter for the given options
   */
  private getOrCreateLimiter(
    options: RateLimitOptions,
    context: ExecutionContext
  ): RateLimiterRedis {
    const handlerName = context.getHandler().name;
    const controllerName = context.getClass().name;
    const cacheKey = `${controllerName}:${handlerName}:${options.limit}:${options.ttl}`;

    let limiter = this.limiters.get(cacheKey);
    if (!limiter) {
      limiter = new RateLimiterRedis({
        storeClient: this.redis,
        keyPrefix: options.keyPrefix ?? `rate_limit:${controllerName}:${handlerName}`,
        points: options.limit,
        duration: options.ttl,
        // Block in-memory after limit exceeded to reduce Redis load
        inMemoryBlockOnConsumed: options.limit + 1,
      });
      this.limiters.set(cacheKey, limiter);
    }

    return limiter;
  }

  /**
   * Set standard rate limit headers on response
   */
  private setRateLimitHeaders(
    response: Response,
    options: RateLimitOptions,
    result: RateLimiterRes,
    exceeded = false
  ): void {
    response.setHeader("X-RateLimit-Limit", String(options.limit));
    response.setHeader("X-RateLimit-Remaining", String(Math.max(0, result.remainingPoints)));
    response.setHeader("X-RateLimit-Reset", String(Math.ceil(result.msBeforeNext / 1000)));

    if (exceeded) {
      response.setHeader("Retry-After", String(Math.ceil(result.msBeforeNext / 1000)));
    }
  }
}
