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

/**
 * Brute-force protection for the host and admin sign-in forms.
 *
 * Failed attempts are counted in sliding windows under three keys:
 * - the client address: one network guessing across many accounts;
 * - the (e-mail, address) pair: one network guessing one account;
 * - the e-mail alone: one account guessed from many networks.
 *
 * Only the first two can block. Once one of them reaches its limit, every attempt from that
 * address (or for that account from that address) is refused — even with the right password —
 * until the window has passed. The e-mail counter never refuses anything: if it did, anyone who
 * knows an owner's address could keep them locked out with a few requests from any network.
 * Instead it adds a short delay and is logged once when it trips. Passwords are admin-generated
 * 16-character random strings (≈ 92 bits), so a slowed-down distributed guess gets nowhere.
 *
 * Successful sign-ins are not counted, so 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;
  /** Extra wait before checking the password, because the account is under attack (0 normally). */
  delayMs: number;
}

export interface AttemptKeys {
  /** Blocks: one network across all accounts. */
  ip: string;
  /** Blocks: one network against one account. */
  pair: string;
  /** Never blocks: slowdown and logging only. */
  account: string;
}

export interface LoginThrottleOptions {
  /** Failures allowed per (e-mail, address) pair in the window. */
  maxFailures?: number;
  /** Failures allowed per address in the window (a bar's staff share one address). */
  maxIpFailures?: number;
  /** Failures per account (from anywhere) after which attempts for it are slowed down. */
  accountSlowAfter?: number;
  accountDelayMs?: number;
  windowMs?: number;
  maxKeys?: number;
}

export const LOGIN_MAX_FAILURES = 10;
export const LOGIN_MAX_IP_FAILURES = 30;
export const LOGIN_ACCOUNT_SLOW_AFTER = 10;
export const LOGIN_ACCOUNT_DELAY_MS = 1_500;
export const LOGIN_WINDOW_MS = 15 * 60 * 1000;

export class LoginThrottle {
  private readonly pairFailures: SlidingWindowLimiter;
  private readonly ipFailures: SlidingWindowLimiter;
  private readonly accountFailures: SlidingWindowLimiter;
  private readonly blockedUntil = new Map<string, number>();
  private readonly maxFailures: number;
  private readonly accountSlowAfter: number;
  private readonly accountDelayMs: number;
  private readonly windowMs: number;
  private readonly maxKeys: number;

  constructor({
    maxFailures = LOGIN_MAX_FAILURES,
    maxIpFailures = LOGIN_MAX_IP_FAILURES,
    accountSlowAfter = LOGIN_ACCOUNT_SLOW_AFTER,
    accountDelayMs = LOGIN_ACCOUNT_DELAY_MS,
    windowMs = LOGIN_WINDOW_MS,
    maxKeys = 10_000,
  }: LoginThrottleOptions = {}) {
    this.maxFailures = maxFailures;
    this.accountSlowAfter = accountSlowAfter;
    this.accountDelayMs = accountDelayMs;
    this.windowMs = windowMs;
    this.maxKeys = maxKeys;
    this.pairFailures = new SlidingWindowLimiter(maxFailures, windowMs, maxKeys);
    this.ipFailures = new SlidingWindowLimiter(maxIpFailures, windowMs, maxKeys);
    this.accountFailures = new SlidingWindowLimiter(accountSlowAfter, windowMs, maxKeys);
  }

  /** May an attempt with these keys go ahead right now, and after how long a pause? Counts nothing. */
  check(keys: AttemptKeys, now: number): ThrottleResult {
    let retryAfterMs = 0;
    for (const key of [keys.ip, keys.pair]) {
      const until = this.blockedUntil.get(key);
      if (until === undefined) continue;
      if (until <= now) {
        this.blockedUntil.delete(key);
        continue;
      }
      retryAfterMs = Math.max(retryAfterMs, until - now);
    }
    const delayMs = this.accountFailures.count(keys.account, now) >= this.accountSlowAfter ? this.accountDelayMs : 0;
    return { allowed: retryAfterMs === 0, retryAfterMs, delayMs };
  }

  /**
   * Count one failed attempt. The address and pair keys become blocked when they reach their
   * limits. Returns `accountTripped: true` exactly once per window, when the account's own counter
   * reaches its slowdown threshold (for logging).
   */
  recordFailure(keys: AttemptKeys, now: number): { accountTripped: boolean } {
    this.countBlocking(this.ipFailures, keys.ip, now);
    this.countBlocking(this.pairFailures, keys.pair, now);
    const account = this.accountFailures.hit(keys.account, now);
    return { accountTripped: account.allowed && account.remaining === 0 };
  }

  private countBlocking(limiter: SlidingWindowLimiter, key: string, now: number): void {
    const result = limiter.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): AttemptKeys {
  const normalized = email.trim().toLowerCase();
  return { ip: `${scope}:ip:${ip}`, pair: `${scope}:pair:${normalized}|${ip}`, account: `${scope}:email:${normalized}` };
}

/** "o***@bar.test": enough for an operator to recognise the account in a log, not to harvest it. */
export function maskEmail(email: string): string {
  const [local = '', domain = ''] = email.trim().toLowerCase().split('@');
  return `${local.slice(0, 1)}***@${domain}`;
}

/** 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`;
}
