import {
  Injectable,
  type CanActivate,
  type ExecutionContext,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { type Request, type Response } from 'express';
import {
  ENDPOINT_RATE_LIMIT_KEY,
  type EndpointRateLimitOptions,
} from '../decorators/endpoint-rate-limit.decorator';
import { LoggerService } from '../logger.service';
import { RedisService } from '../redis.service';

/** Express request extended with optional JWT user payload. */
interface AuthenticatedRequest extends Request {
  user?: { sub: string; role: string };
}

/**
 * Lua script implementing a true sliding-window rate limiter using a Redis sorted set.
 *
 * Algorithm:
 * 1. Remove all entries outside the current window (older than `now - windowMs`).
 * 2. Count remaining entries (= requests within the window).
 * 3. If under the limit, add the current request with score = current timestamp.
 * 4. Set TTL on the key to auto-expire after the window.
 * 5. Return [currentCount, oldestEntryAge] so we can compute Retry-After.
 *
 * This is more precise than a fixed-window counter because it considers
 * the exact timestamp of each request rather than bucketing.
 *
 * KEYS[1] = rate limit key
 * ARGV[1] = now (ms timestamp)
 * ARGV[2] = windowMs (window duration in ms)
 * ARGV[3] = limit (max requests)
 * ARGV[4] = unique request ID (to distinguish concurrent requests)
 * ARGV[5] = windowSeconds (for key TTL)
 */
const SLIDING_WINDOW_LUA = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local windowMs = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local requestId = ARGV[4]
local windowSec = tonumber(ARGV[5])

-- Remove entries outside the window
redis.call('ZREMRANGEBYSCORE', key, 0, now - windowMs)

-- Count current requests in window
local current = redis.call('ZCARD', key)

if current < limit then
  -- Add this request
  redis.call('ZADD', key, now, requestId)
  -- Ensure key expires after the window
  redis.call('EXPIRE', key, windowSec + 1)
  return {current + 1, 0}
else
  -- Rate limit exceeded — compute time until oldest entry expires
  local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
  local retryAfterMs = 0
  if #oldest >= 2 then
    retryAfterMs = tonumber(oldest[2]) + windowMs - now
    if retryAfterMs < 0 then retryAfterMs = 0 end
  end
  return {current, retryAfterMs}
end
`;

/** Monotonically increasing counter to generate unique request IDs. */
let requestCounter = 0;

/**
 * Guard that enforces per-endpoint rate limiting using a Redis sorted-set sliding window.
 *
 * Only activates on routes decorated with `@EndpointRateLimit()`.
 * Routes without the decorator are not affected.
 *
 * Features:
 * - True sliding window (sorted set) for accurate rate measurement
 * - Keys by IP or authenticated user + endpoint path
 * - Proper 429 response with `Retry-After` header
 * - Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`)
 * - Admin bypass support
 * - Fail-open on Redis errors (logs warning, allows request)
 */
@Injectable()
export class EndpointRateLimitGuard implements CanActivate {
  constructor(
    private readonly redis: RedisService,
    private readonly reflector: Reflector,
    private readonly logger: LoggerService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // In test mode, skip endpoint rate limiting to avoid flaky E2E tests
    if (process.env['NODE_ENV'] === 'test') {
      return true;
    }

    const options = this.reflector.getAllAndOverride<EndpointRateLimitOptions | undefined>(
      ENDPOINT_RATE_LIMIT_KEY,
      [context.getHandler(), context.getClass()],
    );

    // No @EndpointRateLimit decorator → skip
    if (!options) {
      return true;
    }

    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const response: Response = context.switchToHttp().getResponse();
    const user = request.user;

    // Admin bypass (default: true)
    const adminBypass = options.adminBypass !== false;
    if (adminBypass && user?.role === 'ADMIN') {
      return true;
    }

    const windowSeconds = options.windowSeconds ?? 60;
    const windowMs = windowSeconds * 1000;
    const limit = options.limit;
    const keyStrategy = options.keyStrategy ?? 'ip';

    // Build the rate limit key: endpoint_rate_limit:{method}:{path}:{identifier}
    const method = request.method;
    const routePath = request.route?.path ?? request.path;
    const identifier = this.resolveIdentifier(request, keyStrategy);

    const key = `endpoint_rate_limit:${method}:${routePath}:${identifier}`;

    try {
      const client = this.redis.getClient();
      const now = Date.now();
      const requestId = `${now}:${process.pid}:${++requestCounter}`;

      const result = await client.eval(
        SLIDING_WINDOW_LUA,
        1,
        key,
        now,
        windowMs,
        limit,
        requestId,
        windowSeconds,
      ) as [number, number];

      const current = result[0];
      const retryAfterMs = result[1];

      // Set rate limit headers for observability
      response.setHeader('X-RateLimit-Limit', limit);
      response.setHeader('X-RateLimit-Remaining', Math.max(0, limit - current));
      response.setHeader('X-RateLimit-Reset', windowSeconds);

      if (current > limit || retryAfterMs > 0) {
        // The request was NOT added (limit reached) — return 429
        const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
        response.setHeader('Retry-After', retryAfterSeconds);

        this.logger.warn(
          `Endpoint rate limit exceeded: ${method} ${routePath}, ` +
            `key=${keyStrategy}:${identifier}, current=${current}/${limit}, ` +
            `retryAfter=${retryAfterSeconds}s`,
          'EndpointRateLimitGuard',
        );

        throw new HttpException(
          {
            statusCode: HttpStatus.TOO_MANY_REQUESTS,
            message: 'Too many requests. Please try again later.',
            retryAfter: retryAfterSeconds,
          },
          HttpStatus.TOO_MANY_REQUESTS,
        );
      }

      return true;
    } catch (error) {
      // Re-throw intentional 429 errors
      if (error instanceof HttpException && error.getStatus() === HttpStatus.TOO_MANY_REQUESTS) {
        throw error;
      }

      // Fail open on Redis errors
      this.logger.warn(
        `Endpoint rate limit check failed (Redis error), allowing request: ` +
          `${method} ${routePath}, key=${keyStrategy}:${identifier}, ` +
          `error=${error instanceof Error ? error.message : 'unknown'}`,
        'EndpointRateLimitGuard',
      );
      return true;
    }
  }

  /**
   * Resolve the identifier for the rate limit key based on the strategy.
   */
  private resolveIdentifier(request: AuthenticatedRequest, strategy: 'ip' | 'user'): string {
    if (strategy === 'user') {
      const user = request.user;
      if (user?.sub) {
        return `user:${user.sub}`;
      }
      // Fall back to IP for unauthenticated requests on user-keyed endpoints
    }

    // IP-based: extract real IP behind proxy
    const forwarded = request.headers['x-forwarded-for'];
    const ip =
      typeof forwarded === 'string'
        ? (forwarded.split(',')[0]?.trim() ?? '127.0.0.1')
        : (request.ip ?? '127.0.0.1');

    return `ip:${ip}`;
  }
}
