import {
  Injectable,
  type CanActivate,
  type ExecutionContext,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { type UserRole } from '@prisma/client';
import { LoggerService } from '../logger.service';
import { RedisService } from '../redis.service';

/**
 * Role-based rate limits (requests per window).
 * Applied only to authenticated routes that use JwtAuthGuard.
 */
export const DEFAULT_ROLE_LIMITS: Record<UserRole, number> = {
  BUYER: 100,
  SELLER: 150,
  AGENT: 200,
  DEVELOPER: 300,
  PARK_OPERATOR: 300,
  ADMIN: 500,
};

/** Default sliding window in seconds. */
export const DEFAULT_WINDOW_SECONDS = 60;

/** Metadata key for per-route overrides via @UserRateLimit decorator. */
export const USER_RATE_LIMIT_KEY = 'user_rate_limit';

export interface UserRateLimitOptions {
  /** Override limits per role for this route. Roles not listed use DEFAULT_ROLE_LIMITS. */
  limits?: Partial<Record<UserRole, number>>;
  /** Window in seconds (default 60). */
  windowSeconds?: number;
}

/**
 * Guard that enforces per-user (by user ID) rate limiting for authenticated routes.
 *
 * Uses a Redis sliding-window counter keyed by `rate_limit:user:{userId}`.
 * Falls back to allowing the request if Redis is unavailable (fail-open)
 * to avoid blocking legitimate traffic during cache outages.
 *
 * This guard checks `request.user` — it must run AFTER JwtAuthGuard.
 * If no authenticated user is found, the guard passes (unauthenticated
 * routes are handled by the existing IP-based ThrottlerBehindProxyGuard).
 */
@Injectable()
export class UserRateLimitGuard 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 user rate limiting to avoid flaky E2E tests
    if (process.env['NODE_ENV'] === 'test') {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // Skip for unauthenticated requests — IP-based throttler handles those
    if (!user?.sub || !user?.role) {
      return true;
    }

    const userId: string = user.sub;
    const role: UserRole = user.role;

    // Resolve per-route overrides
    const options = this.reflector.getAllAndOverride<UserRateLimitOptions | undefined>(
      USER_RATE_LIMIT_KEY,
      [context.getHandler(), context.getClass()],
    );

    const windowSeconds = options?.windowSeconds ?? DEFAULT_WINDOW_SECONDS;
    const limit = options?.limits?.[role] ?? DEFAULT_ROLE_LIMITS[role] ?? DEFAULT_ROLE_LIMITS.BUYER;

    const key = `rate_limit:user:${userId}`;

    try {
      const client = this.redis.getClient();

      // Atomic increment + conditional TTL set via Lua script.
      // Uses INCR + conditional EXPIRE to avoid race conditions.
      const result = await client.eval(
        `local current = redis.call('INCR', KEYS[1])
         if current == 1 then
           redis.call('EXPIRE', KEYS[1], ARGV[1])
         end
         local ttl = redis.call('TTL', KEYS[1])
         return {current, ttl}`,
        1,
        key,
        windowSeconds,
      ) as [number, number];

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

      const response = context.switchToHttp().getResponse();

      // Always 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', ttl > 0 ? ttl : windowSeconds);

      if (current > limit) {
        const retryAfter = ttl > 0 ? ttl : windowSeconds;
        response.setHeader('Retry-After', retryAfter);

        this.logger.warn(
          `User rate limit exceeded: userId=${userId}, role=${role}, ` +
            `current=${current}/${limit}, retryAfter=${retryAfter}s`,
          'UserRateLimitGuard',
        );

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

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

      // Fail open on Redis errors — log and allow the request
      this.logger.warn(
        `User rate limit check failed (Redis error), allowing request: userId=${userId}, error=${
          error instanceof Error ? error.message : 'unknown'
        }`,
        'UserRateLimitGuard',
      );
      return true;
    }
  }
}
