import { SlidingWindowLimiter } from '../rate-limit';

/**
 * Brute-force protection for the host and admin sign-in forms.
 *
 * Failed attempts are counted per key (one key per client IP and one per e-mail address) in a
 * sliding window. Once any key reaches the limit, every attempt for that key is refused — even
 * one with the right password — until the window has passed, so guessing cannot continue.
 * Successful sign-ins are not counted, so venue staff sharing a bar's Wi-Fi never lock each
 * other out by signing in normally. Per process only, like the other in-memory limiters.
 */
export interface ThrottleResult {
  allowed: boolean;
  /** Milliseconds until the blocking key frees up (0 when allowed). */
  retryAfterMs: number;
}

export interface LoginThrottleOptions {
  maxFailures?: number;
  windowMs?: number;
  maxKeys?: number;
}

export const LOGIN_MAX_FAILURES = 10;
export const LOGIN_WINDOW_MS = 15 * 60 * 1000;

export class LoginThrottle {
  private readonly failures: SlidingWindowLimiter;
  private readonly blockedUntil = new Map<string, number>();
  private readonly maxFailures: number;
  private readonly windowMs: number;
  private readonly maxKeys: number;

  constructor({ maxFailures = LOGIN_MAX_FAILURES, windowMs = LOGIN_WINDOW_MS, maxKeys = 10_000 }: LoginThrottleOptions = {}) {
    this.maxFailures = maxFailures;
    this.windowMs = windowMs;
    this.maxKeys = maxKeys;
    this.failures = new SlidingWindowLimiter(maxFailures, windowMs, maxKeys);
  }

  /** May an attempt for these keys go ahead right now? Does not count anything. */
  check(keys: readonly string[], now: number): ThrottleResult {
    let retryAfterMs = 0;
    for (const key of keys) {
      const until = this.blockedUntil.get(key);
      if (until === undefined) continue;
      if (until <= now) {
        this.blockedUntil.delete(key);
        continue;
      }
      retryAfterMs = Math.max(retryAfterMs, until - now);
    }
    return { allowed: retryAfterMs === 0, retryAfterMs };
  }

  /** Count one failed attempt against every key; keys that reach the limit become blocked. */
  recordFailure(keys: readonly string[], now: number): void {
    for (const key of keys) {
      const result = this.failures.hit(key, now);
      if (result.remaining === 0 || !result.allowed) {
        // Blocked for a full window from the attempt that hit the limit.
        this.blockedUntil.set(key, now + this.windowMs);
        if (this.blockedUntil.size > this.maxKeys) {
          const oldest = this.blockedUntil.keys().next().value;
          if (oldest !== undefined) this.blockedUntil.delete(oldest);
        }
      }
    }
  }

  get limit(): number {
    return this.maxFailures;
  }
}

/** Throttle keys for one attempt. E-mails are lower-cased so case changes don't reset the count. */
export function throttleKeys(scope: 'host' | 'admin', ip: string, email: string): string[] {
  return [`${scope}:ip:${ip}`, `${scope}:email:${email.trim().toLowerCase()}`];
}

/** Friendly wait time for the error message ("about 12 minutes"). */
export function describeWait(ms: number): string {
  const minutes = Math.max(1, Math.ceil(ms / 60_000));
  return minutes === 1 ? 'about a minute' : `about ${minutes} minutes`;
}
