import { NextResponse } from 'next/server';
import { RouteContext, RouteHandler } from './withErrorHandler';

import { childLogger } from '@server/logger';
const log = childLogger('middleware.rate-limit');

export type RateLimitBucket = {
  limit: number;
  windowMs: number;
  keyOf: (req: Request) => string | null | undefined;
  scope: string;
};

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

const buckets = new Map<string, Counter>();
const failureCounters = new Map<string, Counter>();

let lastSweep = Date.now();
const SWEEP_INTERVAL_MS = 60_000;

function sweep(now: number) {
  if (now - lastSweep < SWEEP_INTERVAL_MS) return;
  lastSweep = now;
  for (const [k, v] of buckets) if (v.resetAt <= now) buckets.delete(k);
  for (const [k, v] of failureCounters) if (v.resetAt <= now) failureCounters.delete(k);
}

/**
 * Best-effort client IP extraction. NOTE: these headers are user-controllable
 * unless the deploying buyer fronts the app with a trusted reverse proxy that
 * overwrites them (nginx, Cloudflare, etc.). For that reason this value is
 * only used as a *defense-in-depth* bucket — every protected route ALSO
 * applies a non-IP bucket (per-email, per-userId, or per-token) that an
 * attacker cannot spoof.
 */
export function getClientIp(req: Request): string {
  const h = req.headers;
  const xff = h.get('x-forwarded-for');
  if (xff) return xff.split(',')[0].trim();
  return (
    h.get('x-real-ip') ||
    h.get('cf-connecting-ip') ||
    h.get('x-client-ip') ||
    'unknown'
  );
}

export type RateLimitResult = {
  allowed: boolean;
  retryAfterSec?: number;
  scope?: string;
};

export function checkRateLimit(scope: string, key: string, limit: number, windowMs: number): RateLimitResult {
  const now = Date.now();
  sweep(now);
  const k = `${scope}:${key}`;
  const existing = buckets.get(k);
  if (!existing || existing.resetAt <= now) {
    buckets.set(k, { count: 1, resetAt: now + windowMs });
    return { allowed: true };
  }
  if (existing.count >= limit) {
    return { allowed: false, retryAfterSec: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)), scope };
  }
  existing.count++;
  return { allowed: true };
}

export function withRateLimit(buckets: RateLimitBucket[], handler: RouteHandler): RouteHandler {
  return async (req: Request, context: RouteContext) => {
    try {
      for (const b of buckets) {
        const rawKey = b.keyOf(req);
        if (!rawKey) continue;
        const result = checkRateLimit(b.scope, String(rawKey), b.limit, b.windowMs);
        if (!result.allowed) {
          return NextResponse.json(
            {
              error: 'Too many requests. Please slow down and try again later.',
              code: 'RATE_LIMITED',
              scope: result.scope,
              retryAfter: result.retryAfterSec,
            },
            {
              status: 429,
              headers: { 'Retry-After': String(result.retryAfterSec ?? 60) },
            }
          );
        }
      }
    } catch (err) {
      // Fail CLOSED: if the limiter itself crashes, refuse the request rather
      // than let attackers defeat throttling by triggering exceptions.
      log.error({ err }, 'RateLimit limiter error (failing closed)');
      return NextResponse.json(
        { error: 'Service temporarily unavailable. Please try again shortly.', code: 'RATE_LIMITER_UNAVAILABLE' },
        { status: 503, headers: { 'Retry-After': '30' } }
      );
    }
    return handler(req, context);
  };
}

const FAIL_LOCK_THRESHOLD = 8;
const FAIL_LOCK_WINDOW_MS = 15 * 60 * 1000;

export function isLockedOut(scope: string, key: string): { locked: boolean; retryAfterSec?: number } {
  const now = Date.now();
  sweep(now);
  const c = failureCounters.get(`${scope}:${key}`);
  if (!c) return { locked: false };
  if (c.resetAt <= now) {
    failureCounters.delete(`${scope}:${key}`);
    return { locked: false };
  }
  if (c.count >= FAIL_LOCK_THRESHOLD) {
    return { locked: true, retryAfterSec: Math.ceil((c.resetAt - now) / 1000) };
  }
  return { locked: false };
}

export function recordFailure(scope: string, key: string): void {
  const now = Date.now();
  const k = `${scope}:${key}`;
  const c = failureCounters.get(k);
  if (!c || c.resetAt <= now) {
    failureCounters.set(k, { count: 1, resetAt: now + FAIL_LOCK_WINDOW_MS });
  } else {
    c.count++;
    c.resetAt = now + FAIL_LOCK_WINDOW_MS;
  }
}

export function clearFailures(scope: string, key: string): void {
  failureCounters.delete(`${scope}:${key}`);
}

export const FAILURE_LOCK_THRESHOLD = FAIL_LOCK_THRESHOLD;
export const FAILURE_LOCK_WINDOW_MS = FAIL_LOCK_WINDOW_MS;
