/**
 * In-memory progressive-delay lockout tracker for failed logins.
 *
 * ## Why progressive delays instead of a hard lockout
 * A permanent account lock is itself an attack surface: an attacker who knows
 * (or guesses) an address can lock the owner out forever — a denial of service
 * aimed at the victim, not a break-in attempt. Instead of locking, we escalate
 * the wait a client must sit through before its next attempt: the more failures
 * pile up, the longer the enforced pause. The delay always expires, so a real
 * user is never permanently shut out, while a scripted attacker slows down
 * quadratically-ish with each batch of attempts. On a successful login the
 * counter resets.
 *
 * ## Tiers (consecutive failures -> enforced wait before the next attempt)
 *    1–4   failures   → no delay
 *    5–7   failures   → 30 seconds
 *    8–10  failures   → 2 minutes
 *    11–14 failures   → 15 minutes
 *    15+   failures   → 1 hour (hard cap — never grows beyond this)
 *
 * ## Deployment caveat (read before scaling out)
 * Like the rate limiter in `@/lib/security/rateLimit`, the counters live in the
 * process heap: each instance enforces its OWN lockout budget and a restart
 * clears every entry. That is acceptable for a single-instance deployment — the
 * tracker is one layer of defence (rate limits and per-account keys are the
 * others). Before scaling out, move the counters to a shared store (Redis or a
 * Postgres table) keyed by the same `lockout:email:…` / `lockout:ip:…` strings.
 */

/** Escalating tiers; the last tier's `maxFailures: Infinity` is the ceiling. */
export const LOCKOUT_TIERS = [
  { minFailures: 1, maxFailures: 4, delaySeconds: 0 },
  { minFailures: 5, maxFailures: 7, delaySeconds: 30 },
  { minFailures: 8, maxFailures: 10, delaySeconds: 120 },
  { minFailures: 11, maxFailures: 14, delaySeconds: 900 },
  { minFailures: 15, maxFailures: Infinity, delaySeconds: 3600 },
] as const;

/**
 * Maximum wait any key can ever be asked to sit through. Kept in one place so
 * the "no permanent lockout" guarantee stays greppable and testable.
 */
export const MAX_LOCKOUT_SECONDS = 3600;

/** Hard ceiling on tracked keys so a buggy caller cannot grow the map unbounded. */
const MAX_ENTRIES = 10_000;

/**
 * A key becomes evictable once its lock has expired AND it has been idle for
 * this long. Deliberately generous — an entry that has not been touched for
 * two hours carries no defensive value worth keeping.
 */
const STALE_MS = 2 * 60 * 60 * 1000;

/** Delay (in seconds) a client must wait after `failures` consecutive failures. */
export function delaySecondsForFailures(failures: number): number {
  for (const tier of LOCKOUT_TIERS) {
    if (failures <= tier.maxFailures) return tier.delaySeconds;
  }
  return MAX_LOCKOUT_SECONDS; // unreachable: the last tier matches everything
}

interface Entry {
  /** Consecutive failures recorded so far (resets on success). */
  failures: number;
  /** Epoch-ms timestamp when the current delay ends. 0 when not locked. */
  lockedUntil: number;
  /** Epoch-ms timestamp of the most recent failure (drives eviction). */
  lastFailureAt: number;
}

export interface LockoutState {
  locked: boolean;
  /** Whole seconds until the key can be tried again. 0 when not locked. */
  retryAfterSeconds: number;
  failures: number;
}

export interface LockoutOptions {
  /** Clock for deterministic tests; defaults to `Date.now`. */
  now?: () => number;
  /** Map-size ceiling for `sweep` eviction; defaults to MAX_ENTRIES. */
  maxEntries?: number;
  /** Idle time after which an unlocked key is evictable; defaults to STALE_MS. */
  staleMs?: number;
}

export interface LockoutTracker {
  /**
   * Records one failed attempt for `key` and returns the resulting state. The
   * new failure is already included in `failures`, and `locked` is true when
   * the escalated delay applies from now.
   */
  recordFailure(key: string): LockoutState;
  /** Clears the counter for `key` — call on a successful login. */
  recordSuccess(key: string): void;
  /** Reads the current state without recording anything. */
  checkLockout(key: string): LockoutState;
  /** Opportunistic eviction of stale entries once the map gets large. */
  sweep(): void;
  /** Empties every entry. Used by tests and tooling that wants a clean slate. */
  clear(): void;
  /** Number of tracked keys. Exposed for tests and observability. */
  size(): number;
}

export function createLockoutTracker(options: LockoutOptions = {}): LockoutTracker {
  const nowFn = options.now ?? Date.now;
  const maxEntries = options.maxEntries ?? MAX_ENTRIES;
  const staleMs = options.staleMs ?? STALE_MS;
  const entries = new Map<string, Entry>();

  /**
   * Opportunistic eviction, mirroring the rate limiter's sweep: only runs once
   * the map is large, and only removes keys whose delay has fully elapsed and
   * that have sat idle beyond `staleMs`. Locked entries are never evicted, so
   * an active attacker's escalation is never silently forgotten.
   */
  function sweep(nowMs: number): void {
    if (entries.size < maxEntries) return;
    for (const [key, entry] of entries) {
      if (entry.lockedUntil <= nowMs && nowMs - entry.lastFailureAt >= staleMs) {
        entries.delete(key);
      }
    }
  }

  function stateFor(key: string, nowMs: number): LockoutState {
    const entry = entries.get(key);
    if (!entry) return { locked: false, retryAfterSeconds: 0, failures: 0 };
    if (entry.lockedUntil <= nowMs) {
      return { locked: false, retryAfterSeconds: 0, failures: entry.failures };
    }
    return {
      locked: true,
      retryAfterSeconds: Math.ceil((entry.lockedUntil - nowMs) / 1000),
      failures: entry.failures,
    };
  }

  return {
    recordFailure(key) {
      const nowMs = nowFn();
      sweep(nowMs);
      const previous = entries.get(key);
      const failures = (previous?.failures ?? 0) + 1;
      const delaySeconds = delaySecondsForFailures(failures);
      entries.set(key, {
        failures,
        lockedUntil: delaySeconds > 0 ? nowMs + delaySeconds * 1000 : 0,
        lastFailureAt: nowMs,
      });
      return stateFor(key, nowMs);
    },

    recordSuccess(key) {
      entries.delete(key);
    },

    checkLockout(key) {
      const nowMs = nowFn();
      sweep(nowMs);
      return stateFor(key, nowMs);
    },

    sweep() {
      sweep(nowFn());
    },

    clear() {
      entries.clear();
    },

    size() {
      return entries.size;
    },
  };
}

/**
 * Process-wide default tracker used by the auth routes. Single-instance only —
 * see the deployment caveat at the top of this module.
 */
export const lockout = createLockoutTracker();
