/**
 * Lockout Policy — FASE-09 Domain Extraction (T-46)
 *
 * Extracts account lockout logic from handleChat into a dedicated
 * domain service. Manages login attempt tracking and lockout decisions.
 *
 * State is persisted in SQLite via domainState.js.
 *
 * @module domain/lockoutPolicy
 */

import {
  saveLockoutState,
  loadLockoutState,
  deleteLockoutState,
  loadAllLockedIdentifiers,
} from "../lib/db/domainState";

/**
 * @typedef {Object} LockoutConfig
 * @property {number} [maxAttempts=5] - Max failed attempts before lockout
 * @property {number} [lockoutDurationMs=900000] - Lockout duration (15 min default)
 * @property {number} [attemptWindowMs=300000] - Window for counting attempts (5 min)
 */

/** @type {Map<string, { attempts: number[], lockedUntil: number|null }>} In-memory cache */
const lockoutCache = new Map();

/** @type {LockoutConfig} */
const DEFAULT_CONFIG = {
  maxAttempts: 5,
  lockoutDurationMs: 15 * 60 * 1000, // 15 minutes
  attemptWindowMs: 5 * 60 * 1000, // 5 minutes
};

/**
 * Load state from DB into cache if not already cached.
 * @param {string} identifier
 * @returns {{ attempts: number[], lockedUntil: number|null }}
 */
function getState(identifier) {
  if (lockoutCache.has(identifier)) {
    return lockoutCache.get(identifier);
  }

  try {
    const fromDb = loadLockoutState(identifier);
    if (fromDb) {
      lockoutCache.set(identifier, fromDb);
      return fromDb;
    }
  } catch {
    // DB may not be ready
  }

  return null;
}

/**
 * Persist state to both cache and DB.
 * @param {string} identifier
 * @param {{ attempts: number[], lockedUntil: number|null }} state
 */
function persistState(identifier, state) {
  lockoutCache.set(identifier, state);
  try {
    saveLockoutState(identifier, state);
  } catch {
    // Non-critical
  }
}

/**
 * Check if an identifier (IP, username, API key) is currently locked out.
 *
 * @param {string} identifier - The identifier to check
 * @param {LockoutConfig} [config]
 * @returns {{ locked: boolean, remainingMs?: number, attempts?: number }}
 */
export function checkLockout(identifier, config = DEFAULT_CONFIG) {
  const state = getState(identifier);
  if (!state) {
    return { locked: false, attempts: 0 };
  }

  // Check if lockout has expired
  if (state.lockedUntil && Date.now() < state.lockedUntil) {
    return {
      locked: true,
      remainingMs: state.lockedUntil - Date.now(),
      attempts: state.attempts.length,
    };
  }

  // Clear expired lockout
  if (state.lockedUntil) {
    state.lockedUntil = null;
    state.attempts = [];
    persistState(identifier, state);
  }

  // Count recent attempts within the window
  const windowStart = Date.now() - config.attemptWindowMs;
  const recentAttempts = state.attempts.filter((t) => t > windowStart);
  state.attempts = recentAttempts;
  persistState(identifier, state);

  return { locked: false, attempts: recentAttempts.length };
}

/**
 * Record a failed attempt. Returns whether the identifier is now locked out.
 *
 * @param {string} identifier
 * @param {LockoutConfig} [config]
 * @returns {{ locked: boolean, remainingMs?: number }}
 */
export function recordFailedAttempt(identifier, config = DEFAULT_CONFIG) {
  let state = getState(identifier);
  if (!state) {
    state = { attempts: [], lockedUntil: null };
  }

  // Clean old attempts
  const windowStart = Date.now() - config.attemptWindowMs;
  state.attempts = state.attempts.filter((t) => t > windowStart);

  // Record new attempt
  state.attempts.push(Date.now());

  // Check if threshold exceeded
  if (state.attempts.length >= config.maxAttempts) {
    state.lockedUntil = Date.now() + config.lockoutDurationMs;
    persistState(identifier, state);
    return {
      locked: true,
      remainingMs: config.lockoutDurationMs,
    };
  }

  persistState(identifier, state);
  return { locked: false };
}

/**
 * Record a successful login — clears history for identifier.
 *
 * @param {string} identifier
 */
export function recordSuccess(identifier) {
  lockoutCache.delete(identifier);
  try {
    deleteLockoutState(identifier);
  } catch {
    // Non-critical
  }
}

/**
 * Force-unlock an identifier (admin action).
 *
 * @param {string} identifier
 */
export function forceUnlock(identifier) {
  lockoutCache.delete(identifier);
  try {
    deleteLockoutState(identifier);
  } catch {
    // Non-critical
  }
}

/**
 * Get all currently locked identifiers (for monitoring).
 *
 * @returns {Array<{ identifier: string, lockedUntil: number, remainingMs: number }>}
 */
export function getLockedIdentifiers() {
  const now = Date.now();

  // Merge cache and DB
  try {
    const fromDb = loadAllLockedIdentifiers();
    for (const entry of fromDb) {
      if (!lockoutCache.has(entry.identifier)) {
        lockoutCache.set(entry.identifier, {
          attempts: [],
          lockedUntil: entry.lockedUntil,
        });
      }
    }
  } catch {
    // Use cache only
  }

  const locked = [];
  for (const [id, state] of lockoutCache.entries()) {
    if (state.lockedUntil && state.lockedUntil > now) {
      locked.push({
        identifier: id,
        lockedUntil: state.lockedUntil,
        remainingMs: state.lockedUntil - now,
      });
    }
  }

  return locked;
}
