import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { Request } from 'express';

type Bucket = {
  count: number;
  resetAt: number;
};

@Injectable()
export class AutomationRateLimitGuard implements CanActivate {
  private readonly buckets = new Map<string, Bucket>();
  private readonly windowMs = 60_000;
  private readonly maxRequests = 120;
  // Hard backstop so the Map cannot grow without bound under key churn.
  private readonly maxKeys = 50_000;
  private lastSweepAt = 0;

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const key = this.getKey(request);
    const now = Date.now();

    this.evict(now);

    const bucket = this.buckets.get(key);

    if (!bucket || bucket.resetAt <= now) {
      this.buckets.set(key, { count: 1, resetAt: now + this.windowMs });
      return true;
    }

    bucket.count += 1;
    if (bucket.count > this.maxRequests) {
      throw new HttpException(
        'Automation event rate limit exceeded',
        HttpStatus.TOO_MANY_REQUESTS,
      );
    }

    return true;
  }

  private getKey(request: Request): string {
    // trust proxy is set in main.ts; req.ip already reflects the correct client IP
    // (leftmost untrusted hop in x-forwarded-for as resolved by Express). Using the
    // raw x-forwarded-for header directly is spoofable by clients — do not do that.
    return request.ip || request.socket.remoteAddress || 'unknown';
  }

  private evict(now: number): void {
    // Lazy sweep: prune expired buckets at most once per window so the happy
    // path stays cheap (guarded by lastSweepAt, not O(n) on every request).
    if (now - this.lastSweepAt > this.windowMs) {
      for (const [key, bucket] of this.buckets) {
        if (bucket.resetAt <= now) {
          this.buckets.delete(key);
        }
      }
      this.lastSweepAt = now;
    }

    // Hard cap as a backstop against pathological key churn within a single
    // window. Clearing the whole Map is fail-open (it only resets counters for
    // one window) — acceptable for a rate limiter; an LRU would avoid the global
    // reset but adds a dependency.
    if (this.buckets.size > this.maxKeys) {
      this.buckets.clear();
    }
  }
}
