/**
 * Redis-backed rate limiting middleware for SentryAgent.ai AgentIdP.
 *
 * Uses `rate-limiter-flexible` with a sliding-window `RateLimiterRedis` when
 * `REDIS_RATE_LIMIT_ENABLED=true` and Redis is reachable.  Falls back to
 * `RateLimiterMemory` transparently when Redis is unavailable (task 1.4).
 *
 * Configuration env vars:
 *   RATE_LIMIT_WINDOW_MS      — window length in milliseconds (default 60000)
 *   RATE_LIMIT_MAX_REQUESTS   — maximum requests per window (default 100)
 *   REDIS_RATE_LIMIT_ENABLED  — set to "true" to enable Redis backend
 */

import { Request, Response, NextFunction } from 'express';
import {
  RateLimiterRedis,
  RateLimiterMemory,
  RateLimiterAbstract,
  RateLimiterRes,
} from 'rate-limiter-flexible';
import { getRateLimitRedisClient } from '../infrastructure/redisClient.js';
import { rateLimitHitsTotal } from '../metrics/registry.js';
import { RateLimitError } from '../utils/errors.js';

/** Singleton rate limiter — created once and reused across requests. */
let rateLimiter: RateLimiterAbstract | null = null;

/**
 * Returns the configured rate limiter instance (RateLimiterRedis or fallback
 * RateLimiterMemory). The instance is memoised after the first successful
 * construction so configuration is parsed only once per process lifetime.
 *
 * When the ioredis client is unavailable (Redis unreachable or disabled) the
 * function falls back to in-process memory without throwing.
 *
 * @returns Configured RateLimiterAbstract instance.
 */
function getRateLimiter(): RateLimiterAbstract {
  if (rateLimiter) {
    return rateLimiter;
  }

  const windowMs = parseInt(process.env['RATE_LIMIT_WINDOW_MS'] ?? '60000', 10);
  const maxRequests = parseInt(process.env['RATE_LIMIT_MAX_REQUESTS'] ?? '100', 10);
  const windowSeconds = Math.ceil(windowMs / 1000);

  const redisClient = getRateLimitRedisClient();

  if (redisClient !== null) {
    // RateLimiterRedis: sliding window backed by ioredis.
    // insuranceLimiter provides in-memory fallback when Redis is temporarily down.
    rateLimiter = new RateLimiterRedis({
      storeClient: redisClient,
      keyPrefix: 'rl',
      points: maxRequests,
      duration: windowSeconds,
      blockDuration: 0,
      insuranceLimiter: new RateLimiterMemory({
        points: maxRequests,
        duration: windowSeconds,
      }),
    });
  } else {
    // Redis disabled or unreachable — use in-process memory limiter.
    rateLimiter = new RateLimiterMemory({
      points: maxRequests,
      duration: windowSeconds,
    });
  }

  return rateLimiter;
}

/**
 * Resets the memoised rate limiter singleton.
 * Exposed for testing purposes only — do NOT call in production code.
 *
 * @internal
 */
export function _resetRateLimiterForTests(): void {
  rateLimiter = null;
}

/**
 * Derives the rate-limit key for a given request.
 * Authenticated requests key by `client_id`; unauthenticated requests key by IP.
 *
 * @param req - Express request object.
 * @returns String key unique to the client.
 */
function resolveClientKey(req: Request): string {
  return req.user?.client_id ?? req.ip ?? 'unknown';
}

/**
 * Express middleware that applies sliding-window rate limiting per client.
 *
 * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and
 * `Retry-After` (on rejection) headers.  Increments the
 * `agentidp_rate_limit_hits_total` Prometheus counter and calls
 * `next(RateLimitError)` when the limit is exceeded.
 *
 * @param req - Express request.
 * @param res - Express response.
 * @param next - Express next function.
 */
export async function rateLimitMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
): Promise<void> {
  const limiter = getRateLimiter();
  const key = resolveClientKey(req);

  try {
    const result: RateLimiterRes = await limiter.consume(key);

    // Headers present on every successful response.
    res.setHeader('X-RateLimit-Limit', limiter.points);
    res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
    res.setHeader(
      'X-RateLimit-Reset',
      Math.ceil(Date.now() / 1000 + result.msBeforeNext / 1000),
    );

    next();
  } catch (err) {
    if (err instanceof RateLimiterRes) {
      // Rate limit exceeded — err is the RateLimiterRes rejection object.
      const retryAfterSeconds = Math.ceil(err.msBeforeNext / 1000);
      const endpoint = req.path;

      // Prometheus counter — increment on every HTTP 429 (task 1.5).
      rateLimitHitsTotal.inc({ endpoint });

      // Standard headers on rate-limit rejection (task 1.6).
      res.setHeader('X-RateLimit-Limit', limiter.points);
      res.setHeader('X-RateLimit-Remaining', 0);
      res.setHeader(
        'X-RateLimit-Reset',
        Math.ceil(Date.now() / 1000 + err.msBeforeNext / 1000),
      );
      res.setHeader('Retry-After', retryAfterSeconds);

      next(new RateLimitError());
    } else {
      // Unexpected error (e.g. Redis failure not caught by insuranceLimiter).
      next(err);
    }
  }
}
