/**
 * In-memory rate limiter
 * Stores counters in a Map — resets on server restart, no DB required.
 */

// Persist the store on globalThis so that module-cache invalidation (e.g. during
// Next.js hot reload) does not silently reset all counters within the same process.
// CRITICAL LIMITATION: this Map is process-local. In serverless or multi-worker
// deployments every instance maintains its own store and rate limits do not
// distribute across instances. For production deployments with multiple workers
// replace this Map with a shared atomic store (e.g. Redis / Upstash).
const STORE_KEY = Symbol.for('__ZEN_RATE_LIMIT_STORE__');
if (!globalThis[STORE_KEY]) globalThis[STORE_KEY] = new Map();
/** @type {Map<string, { count: number, windowStart: number, windowMs: number, blockedUntil: number|null }>} */
const store = globalThis[STORE_KEY];

// Purge expired entries every 10 minutes to avoid memory leak
const cleanup = setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of store.entries()) {
    const windowExpired = now > entry.windowStart + entry.windowMs;
    const blockExpired = !entry.blockedUntil || now > entry.blockedUntil;
    if (windowExpired && blockExpired) {
      store.delete(key);
    }
  }
}, 10 * 60 * 1000);

// Allow garbage collection in test/serverless environments
if (cleanup.unref) cleanup.unref();

/**
 * Rate limit presets per action.
 * maxAttempts : number of requests allowed in the window
 * windowMs    : rolling window duration
 * blockMs     : how long to block once the limit is exceeded
 */
export const RATE_LIMITS = {
  login:           { maxAttempts: 5,   windowMs: 15 * 60 * 1000, blockMs: 30 * 60 * 1000 },
  register:        { maxAttempts: 5,   windowMs: 60 * 60 * 1000, blockMs: 60 * 60 * 1000 },
  forgot_password: { maxAttempts: 3,   windowMs: 60 * 60 * 1000, blockMs: 60 * 60 * 1000 },
  reset_password:  { maxAttempts: 5,   windowMs: 15 * 60 * 1000, blockMs: 30 * 60 * 1000 },
  verify_email:    { maxAttempts: 10,  windowMs: 60 * 60 * 1000, blockMs: 30 * 60 * 1000 },
  api:             { maxAttempts: 120, windowMs: 60 * 1000,       blockMs:  5 * 60 * 1000 },
};

/**
 * Check whether a given identifier is allowed for an action, and record the attempt.
 *
 * @param {string} identifier  - IP address or user ID
 * @param {string} action      - Key from RATE_LIMITS (e.g. 'login')
 * @returns {{ allowed: boolean, retryAfterMs?: number, remaining?: number }}
 */
export function checkRateLimit(identifier, action) {
  const config = RATE_LIMITS[action];
  if (!config) return { allowed: true };

  const key = `${action}:${identifier}`;
  const now = Date.now();
  let entry = store.get(key);

  // Still blocked
  if (entry?.blockedUntil && now < entry.blockedUntil) {
    return { allowed: false, retryAfterMs: entry.blockedUntil - now };
  }

  // Start a fresh window (first request, or previous window has expired)
  if (!entry || now > entry.windowStart + entry.windowMs) {
    store.set(key, { count: 1, windowStart: now, windowMs: config.windowMs, blockedUntil: null });
    return { allowed: true, remaining: config.maxAttempts - 1 };
  }

  // Increment counter in the current window
  entry.count += 1;

  if (entry.count > config.maxAttempts) {
    entry.blockedUntil = now + config.blockMs;
    store.set(key, entry);
    return { allowed: false, retryAfterMs: config.blockMs };
  }

  store.set(key, entry);
  return { allowed: true, remaining: config.maxAttempts - entry.count };
}

/**
 * Return true only when the string resembles a valid IPv4 or IPv6 address.
 * This prevents arbitrary attacker-supplied strings from being used as
 * rate-limit identifiers (which could allow bucket manipulation).
 * @param {string|null|undefined} ip
 * @returns {boolean}
 */
function isValidIp(ip) {
  if (!ip || typeof ip !== 'string') return false;
  // IPv4 — four decimal octets, each 0-255
  if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
    return ip.split('.').every(octet => parseInt(octet, 10) <= 255);
  }
  // IPv6 — simplified structural check (colons + hex groups)
  return /^[0-9a-fA-F:]+$/.test(ip) && ip.includes(':');
}

/**
 * Extract the client IP from Next.js headers() (server actions).
 *
 * X-Forwarded-For and X-Real-IP are only trusted when ZEN_TRUST_PROXY=true is
 * explicitly set, confirming a trusted reverse proxy populates those headers.
 * Without this flag the headers are fully attacker-controlled and MUST NOT be
 * used as rate-limit keys — an attacker would trivially rotate identifiers.
 *
 * Set ZEN_TRUST_PROXY=true only when a verified reverse proxy (e.g. Nginx,
 * Cloudflare, AWS ALB) strips and rewrites forwarded headers before they reach
 * this application.
 *
 * @param {import('next/headers').ReadonlyHeaders} headersList
 * @returns {string}
 */
export function getIpFromHeaders(headersList) {
  if (process.env.ZEN_TRUST_PROXY === 'true') {
    const forwarded = headersList.get('x-forwarded-for')?.split(',')[0]?.trim();
    if (forwarded && isValidIp(forwarded)) return forwarded;
    const realIp = headersList.get('x-real-ip')?.trim();
    if (realIp && isValidIp(realIp)) return realIp;
  }
  // In development, use loopback so rate limiting stays active and the
  // "IP cannot be determined" warning is not emitted.
  // In production without a trusted proxy, return 'unknown' to suspend rate
  // limiting rather than collapse all traffic into one shared bucket.
  // Configure ZEN_TRUST_PROXY=true behind a verified reverse proxy.
  return process.env.NODE_ENV === 'development' ? '127.0.0.1' : 'unknown';
}

/**
 * Extract the client IP from a Next.js Request object (API routes).
 * See getIpFromHeaders for the full trust-proxy rationale.
 * @param {Request} request
 * @returns {string}
 */
export function getIpFromRequest(request) {
  if (process.env.ZEN_TRUST_PROXY === 'true') {
    const forwarded = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
    if (forwarded && isValidIp(forwarded)) return forwarded;
    const realIp = request.headers.get('x-real-ip')?.trim();
    if (realIp && isValidIp(realIp)) return realIp;
  }
  return process.env.NODE_ENV === 'development' ? '127.0.0.1' : 'unknown';
}

/**
 * Format a block duration in human-readable French.
 * @param {number} ms
 * @returns {string}
 */
export function formatRetryAfter(ms) {
  const seconds = Math.ceil(ms / 1000);
  if (seconds < 60) return `${seconds} secondes`;
  const minutes = Math.ceil(seconds / 60);
  if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''}`;
  const hours = Math.ceil(minutes / 60);
  return `${hours} heure${hours > 1 ? 's' : ''}`;
}
