import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  HttpException,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { RedisService } from '@chatapp/redis';

// Rate limit configuration per route pattern
interface RateLimitConfig {
  limit: number;
  windowSeconds: number;
  additionalWindow?: { limit: number; windowSeconds: number };
}

// Route-specific rate limits
const ROUTE_LIMITS: { pattern: RegExp; config: RateLimitConfig }[] = [
  // Messages POST: 5 per second, 100 per minute
  {
    pattern: /^\/api\/v1\/messages\/[^/]+$/,
    config: { limit: 5, windowSeconds: 1, additionalWindow: { limit: 100, windowSeconds: 60 } },
  },
  // Files upload POST: 20 per minute
  {
    pattern: /^\/api\/v1\/files\/upload$/,
    config: { limit: 20, windowSeconds: 60 },
  },
];

// Default rate limit: 300 per minute
const DEFAULT_LIMIT: RateLimitConfig = { limit: 300, windowSeconds: 60 };

export const RATE_LIMIT_KEY = 'rateLimit';

/**
 * Rate Limit Interceptor using Redis for distributed counting.
 * Counts requests per user + endpoint and enforces configurable limits.
 */
@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
  private readonly logger = new Logger(RateLimitInterceptor.name);

  constructor(
    private readonly redisService: RedisService,
  ) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const userId = request.user?.userId || request.ip || 'anonymous';
    const method = request.method;
    const path = request.path || request.routeOptions?.path || request.url?.split('?')[0] || '';

    // Skip rate limiting for GET requests on non-list endpoints (optimization)
    if (method === 'GET' && !path.includes('messages')) {
      return next.handle();
    }

    // Find matching rate limit config
    const fullPath = path.startsWith('/api') ? path : `/api/v1${path.startsWith('/') ? path : '/' + path}`;
    const routeConfig = ROUTE_LIMITS.find((r) => r.pattern.test(fullPath));
    const config = routeConfig?.config || DEFAULT_LIMIT;

    const checkAndCount = async (): Promise<boolean> => {
      // Check primary window
      const primaryKey = `ratelimit:${userId}:${method}:${path}:${config.windowSeconds}s`;
      const primaryCount = await this.incrementWithTtl(primaryKey, config.windowSeconds);
      if (primaryCount > config.limit) {
        return false;
      }

      // Check additional window if configured (e.g., 100/min on top of 5/s)
      if (config.additionalWindow) {
        const secondaryKey = `ratelimit:${userId}:${method}:${path}:${config.additionalWindow.windowSeconds}s`;
        const secondaryCount = await this.incrementWithTtl(secondaryKey, config.additionalWindow.windowSeconds);
        if (secondaryCount > config.additionalWindow.limit) {
          return false;
        }
      }

      return true;
    };

    const allowed = await checkAndCount();

    if (!allowed) {
      this.logger.warn(`Rate limit exceeded for user ${userId} on ${method} ${path}`);
      throw new HttpException(
        { statusCode: 429, message: 'Rate limit exceeded. Please try again later.', error: 'Too Many Requests' },
        429,
      );
    }

    return next.handle();
  }

  /**
   * Increment a Redis counter and set TTL if it's a new key.
   * Uses INCR + EXPIRE for atomic-ish counting.
   */
  private async incrementWithTtl(key: string, ttlSeconds: number): Promise<number> {
    const client = this.redisService.client;
    const count = await client.incr(key);
    if (count === 1) {
      // Set TTL only on the first increment to avoid resetting the window
      await client.expire(key, ttlSeconds);
    }
    return count;
  }
}