'use strict';

const DEFAULT_LIMITS = Object.freeze({
  // Remote/mobile page bootstrap fans out across settings, sessions, standup,
  // auth state, WebSocket setup, and cached refresh probes. Keep read traffic
  // comfortably above that burst while leaving mutation/admin buckets tight.
  default: { refillPerMinute: 600, burst: 120 },
  mutation: { refillPerMinute: 30, burst: 5 },
  admin: { refillPerMinute: 10, burst: 2 },
  step_up: { refillPerMinute: 10, burst: 3 },
});

const DEFAULT_FAILED_AUTH = Object.freeze({
  maxFailures: 20,
  windowMs: 60 * 60 * 1000,
  cooldownMs: 60 * 60 * 1000,
});

function nowMs() {
  return Date.now();
}

function normalizeIpLockKey(ip) {
  let key = String(ip || '').trim();
  if (!key) return '';
  if (key.startsWith('[') && key.endsWith(']')) key = key.slice(1, -1);
  const mappedIpv4 = key.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
  return mappedIpv4 ? mappedIpv4[1] : key;
}

const IP_LOCK_AUTH_FAILURE_CODES = Object.freeze(new Set([
  'invalid-token',
]));

function normalizeAuthFailureCode(code) {
  return String(code || '').trim().replace(/_/g, '-');
}

function shouldRecordIpAuthFailure(auth, decision = {}) {
  if (!auth || auth.isLoopback || auth.authenticated) return false;
  const codes = [
    auth.source,
    decision.code,
  ].map(normalizeAuthFailureCode).filter(Boolean);
  return codes.some((code) => IP_LOCK_AUTH_FAILURE_CODES.has(code));
}

class AuthRateLimiter {
  constructor(options = {}) {
    this.limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
    this.failedAuth = { ...DEFAULT_FAILED_AUTH, ...(options.failedAuth || {}) };
    this.ipLockoutExemptions = new Set((options.ipLockoutExemptions || [])
      .map(normalizeIpLockKey)
      .filter(Boolean));
    this.buckets = new Map();
    this.failures = new Map();
  }

  _bucketKey(kind, key) {
    return `${kind}:${key || 'anonymous'}`;
  }

  check(kind, key, atMs = nowMs()) {
    const limit = this.limits[kind] || this.limits.default;
    const bucketKey = this._bucketKey(kind, key);
    const refillPerMs = limit.refillPerMinute / 60 / 1000;
    const current = this.buckets.get(bucketKey) || {
      tokens: limit.burst,
      updatedAt: atMs,
    };
    const elapsed = Math.max(0, atMs - current.updatedAt);
    current.tokens = Math.min(limit.burst, current.tokens + elapsed * refillPerMs);
    current.updatedAt = atMs;
    if (current.tokens < 1) {
      this.buckets.set(bucketKey, current);
      const retryAfterMs = Math.ceil((1 - current.tokens) / refillPerMs);
      return { ok: false, code: 'rate_limited', retryAfterMs };
    }
    current.tokens -= 1;
    this.buckets.set(bucketKey, current);
    return { ok: true };
  }

  isIpLockoutExempt(ip) {
    const key = normalizeIpLockKey(ip);
    return !!key && this.ipLockoutExemptions.has(key);
  }

  isIpLocked(ip, atMs = nowMs()) {
    const key = normalizeIpLockKey(ip);
    if (!key) return { ok: true };
    if (this.ipLockoutExemptions.has(key)) return { ok: true, exempt: true };
    const state = this.failures.get(key);
    if (!state || !state.lockedUntil || state.lockedUntil <= atMs) {
      if (state && state.lockedUntil && state.lockedUntil <= atMs) this.failures.delete(key);
      return { ok: true };
    }
    return {
      ok: false,
      code: 'ip_locked',
      retryAfterMs: state.lockedUntil - atMs,
    };
  }

  recordAuthFailure(ip, atMs = nowMs()) {
    const key = normalizeIpLockKey(ip);
    if (!key) return { locked: false };
    if (this.ipLockoutExemptions.has(key)) return { locked: false, count: 0, lockedUntil: null, exempt: true };
    const existing = this.failures.get(key);
    const windowStart = existing && atMs - existing.windowStart < this.failedAuth.windowMs
      ? existing.windowStart
      : atMs;
    const count = existing && windowStart === existing.windowStart ? existing.count + 1 : 1;
    const state = { windowStart, count, lockedUntil: existing?.lockedUntil || 0 };
    if (count >= this.failedAuth.maxFailures) {
      state.lockedUntil = atMs + this.failedAuth.cooldownMs;
    }
    this.failures.set(key, state);
    return { locked: !!state.lockedUntil, count, lockedUntil: state.lockedUntil || null };
  }

  recordAuthSuccess(ip) {
    const key = normalizeIpLockKey(ip);
    if (this.ipLockoutExemptions.has(key)) return;
    if (key) this.failures.delete(key);
  }

  reset() {
    this.buckets.clear();
    this.failures.clear();
  }
}

function rateKindForRule(rule, pathname = '') {
  if (pathname === '/api/auth/begin-step-up') return 'step_up';
  if (!rule?.mutation) return 'default';
  if (rule.scope === 'admin' || rule.scope === 'create') return 'admin';
  return 'mutation';
}

module.exports = {
  AuthRateLimiter,
  DEFAULT_FAILED_AUTH,
  DEFAULT_LIMITS,
  IP_LOCK_AUTH_FAILURE_CODES,
  normalizeAuthFailureCode,
  normalizeIpLockKey,
  rateKindForRule,
  shouldRecordIpAuthFailure,
};
