import type { NextFunction, Request, RequestHandler, Response } from 'express';
import rateLimit, { type Options } from 'express-rate-limit';
import { errorResponse } from './structured-error.filter';

/**
 * Defaults from ADR-0015's "denial-of-service mitigation" line and
 * the phase-2 placeholder note in `main.ts`. Concrete numbers are
 * deliberately conservative; staging / prod can tighten through
 * env without code changes.
 *
 *   RATE_LIMIT_PER_MINUTE          — general bucket. 120 ≈ 2 r/s.
 *   RATE_LIMIT_AUTH_PER_MINUTE     — stricter on `/auth/login` and
 *                                    `/auth/callback` to slow
 *                                    brute-force / replay loops.
 *                                    10/min is plenty for legit users.
 */
const DEFAULT_RATE_LIMIT_PER_MINUTE = 120;
const DEFAULT_RATE_LIMIT_AUTH_PER_MINUTE = 10;
const WINDOW_MS = 60_000;

/** Auth-flow prefixes for which the stricter bucket applies. */
const STRICT_PATHS = ['/api/auth/login', '/api/auth/callback'];

/**
 * Paths the rate-limit middleware lets through unconditionally.
 * `/api/health` is polled by container orchestrators and load
 * balancers — counting those against a user quota is meaningless.
 */
const SKIP_PATHS = ['/api/health'];

export interface RateLimitConfig {
  readonly perMinute: number;
  readonly authPerMinute: number;
}

export function readRateLimitConfig(): RateLimitConfig {
  return {
    perMinute: parsePositiveInt(
      process.env['RATE_LIMIT_PER_MINUTE'],
      DEFAULT_RATE_LIMIT_PER_MINUTE,
      'RATE_LIMIT_PER_MINUTE',
    ),
    authPerMinute: parsePositiveInt(
      process.env['RATE_LIMIT_AUTH_PER_MINUTE'],
      DEFAULT_RATE_LIMIT_AUTH_PER_MINUTE,
      'RATE_LIMIT_AUTH_PER_MINUTE',
    ),
  };
}

/**
 * Build the rate-limit middleware. Single `express-rate-limit`
 * instance with a dynamic per-request `max`: 10/min on auth-flow
 * entry routes, 120/min everywhere else. Bucket key = session id
 * when the request carries an active session, else the remote IP.
 * That means an unauthenticated brute-force attempt against
 * `/auth/login` is bound to its source IP, not a session it
 * doesn't have.
 *
 * Storage: in-memory. ADR-0015 §"Single-instance v1" plus
 * `express-rate-limit`'s default in-memory store is fine until we
 * scale out the BFF horizontally; the migration to a Redis-backed
 * store is one constructor arg the day we need it.
 *
 * The 429 response uses the same envelope as `StructuredErrorFilter`
 * so the SPA gets `{ error: { code: 'rate_limited', message, traceId } }`
 * regardless of whether the rejection came from Nest or this raw
 * middleware.
 */
export function createRateLimitMiddleware(config: RateLimitConfig): RequestHandler {
  const options: Partial<Options> = {
    windowMs: WINDOW_MS,
    standardHeaders: 'draft-7',
    legacyHeaders: false,
    skip: (req: Request, _res: Response) => SKIP_PATHS.some((p) => req.path.startsWith(p)),
    keyGenerator: (req: Request, _res: Response) => {
      const sessionId = req.sessionID;
      const hasSession = Boolean(req.session?.user);
      if (hasSession && sessionId) {
        return `s:${sessionId}`;
      }
      return `ip:${req.ip ?? 'unknown'}`;
    },
    handler: (_req: Request, res: Response, _next: NextFunction, _optionsUsed: Options) => {
      res.status(429).json(errorResponse('rate_limited', 'Too many requests'));
    },
    max: (req: Request, _res: Response) =>
      STRICT_PATHS.some((p) => req.path.startsWith(p)) ? config.authPerMinute : config.perMinute,
  };
  // The library's `RateLimitRequestHandler` augments `RequestHandler`
  // with diagnostic methods (`resetKey`, etc.) we don't expose here.
  // Cast to the plain Express type so `app.use(...)` accepts it.
  return rateLimit(options) as unknown as RequestHandler;
}

function parsePositiveInt(raw: string | undefined, fallback: number, name: string): number {
  if (raw === undefined || raw === '') {
    return fallback;
  }
  const value = Number(raw);
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`${name} must be a positive integer (got "${raw}").`);
  }
  return value;
}
