import {
  PASSWORD_MAX_LENGTH,
  PASSWORD_MIN_LENGTH,
  PASSWORD_POLICY_MESSAGE,
} from "../constants/index.js";

export interface PasswordContext {
  email?: string | null;
  name?: string | null;
}

export type PasswordCheckResult = { ok: true } | { ok: false; reason: string };

// Curated list of >=12-char passwords that pass the length gate but are
// known weak (rockyou top entries, predictable seasonal patterns, common
// admin defaults). Stored lower-cased for case-insensitive matching. The
// vast majority of weak passwords are <12 chars and already rejected by
// length, so this list focuses on what would otherwise *pass* min-length.
const COMMON_PASSWORDS: ReadonlySet<string> = new Set([
  "passwordpassword",
  "password1234",
  "password12345",
  "password123456",
  "password1234567",
  "passwordpassword1",
  "passw0rdpassw0rd",
  "1234567890ab",
  "1234567890abc",
  "12345678901",
  "123456789012",
  "1234567891234",
  "qwertyuiop12",
  "qwertyuiop123",
  "qwertyuiop1234",
  "qwertyuiopas",
  "asdfghjkl123",
  "asdfghjkl1234",
  "iloveyou1234",
  "iloveyou12345",
  "iloveyouabc1",
  "iloveyouforever",
  "ilovemybabies",
  "letmein123456",
  "letmein12345",
  "welcome12345",
  "welcome123456",
  "welcome1234567",
  "admin1234567",
  "admin12345678",
  "administrator1",
  "administrator123",
  "changeme1234",
  "changeme12345",
  "default1234567",
  "football1234",
  "baseball1234",
  "trustno112345",
  "summer2023!",
  "summer2024!",
  "summer2025!",
  "summer2026!",
  "winter2023!",
  "winter2024!",
  "winter2025!",
  "winter2026!",
  "spring2023!",
  "spring2024!",
  "spring2025!",
  "spring2026!",
  "autumn2023!",
  "autumn2024!",
  "autumn2025!",
  "autumn2026!",
  "welcome2023!",
  "welcome2024!",
  "welcome2025!",
  "welcome2026!",
  "password2023!",
  "password2024!",
  "password2025!",
  "password2026!",
  "p@ssw0rd1234",
  "p@ssword1234",
]);

function isSingleCharRepeated(pw: string): boolean {
  if (pw.length === 0) return false;
  const first = pw[0]!;
  for (let i = 1; i < pw.length; i++) {
    if (pw[i] !== first) return false;
  }
  return true;
}

// Detect monotonically +/-1 character runs. A password where >=60% of
// characters belong to a run of length >=5 (e.g. "abcdefgh1234") is
// rejected as a keyboard / numeric sequence. The 60% threshold lets
// passwords with a sequential prefix and a long random suffix pass.
function isSequentialPassword(pw: string): boolean {
  if (pw.length < 8) return false;
  const lower = pw.toLowerCase();
  let runChars = 0;
  let i = 0;
  while (i < lower.length) {
    let runLen = 1;
    while (i + runLen < lower.length) {
      const delta = lower.charCodeAt(i + runLen) - lower.charCodeAt(i + runLen - 1);
      if (delta === 1 || delta === -1) {
        runLen++;
      } else {
        break;
      }
    }
    if (runLen >= 5) runChars += runLen;
    i += runLen;
  }
  return runChars * 100 >= lower.length * 60;
}

// Reject passwords that are a 2-6 char pattern repeated >=3 times
// (e.g. "abcabcabcabc", "12121212"). Length divisibility check first
// keeps the constant-factor work tiny.
function isShortPatternRepeated(pw: string): boolean {
  for (let patLen = 2; patLen <= 6; patLen++) {
    if (pw.length % patLen !== 0) continue;
    const repeats = pw.length / patLen;
    if (repeats < 3) continue;
    const pattern = pw.slice(0, patLen);
    let allMatch = true;
    for (let r = 1; r < repeats; r++) {
      if (pw.slice(r * patLen, (r + 1) * patLen) !== pattern) {
        allMatch = false;
        break;
      }
    }
    if (allMatch) return true;
  }
  return false;
}

// Substrings shorter than 4 chars are ignored: short names ("ed") or
// generic email locals ("ab") would otherwise reject too many legitimate
// passwords by coincidence.
function containsIdentity(pw: string, ctx: PasswordContext | undefined): boolean {
  if (!ctx) return false;
  const lower = pw.toLowerCase();
  if (ctx.email) {
    const local = ctx.email.split("@")[0]?.toLowerCase().trim() ?? "";
    if (local.length >= 4 && lower.includes(local)) return true;
  }
  if (ctx.name) {
    const lowered = ctx.name.toLowerCase().trim();
    const fullNoSpaces = lowered.replace(/\s+/g, "");
    if (fullNoSpaces.length >= 4 && lower.includes(fullNoSpaces)) return true;
    // Also reject if the password embeds an individual name component
    // (e.g. given-name or surname). "Hartmut Noerenberg" → check both
    // "hartmut" and "noerenberg" so a password like "MyHartmutPass1!"
    // is rejected, not only the unrealistic "MyHartmutNoerenbergPass1!".
    for (const part of lowered.split(/\s+/)) {
      if (part.length >= 4 && lower.includes(part)) return true;
    }
  }
  return false;
}

/**
 * Validate a password against the policy. Pure synchronous function so it
 * can be called from server (tRPC mutations, server actions) and client
 * (pre-submit validation) with identical results.
 */
export function checkPasswordPolicy(password: string, ctx?: PasswordContext): PasswordCheckResult {
  if (password.length < PASSWORD_MIN_LENGTH) {
    return { ok: false, reason: PASSWORD_POLICY_MESSAGE };
  }
  if (password.length > PASSWORD_MAX_LENGTH) {
    return {
      ok: false,
      reason: `Password must be no more than ${PASSWORD_MAX_LENGTH} characters.`,
    };
  }
  if (isSingleCharRepeated(password)) {
    return { ok: false, reason: "Password must not be a single character repeated." };
  }
  if (isShortPatternRepeated(password)) {
    return { ok: false, reason: "Password must not be a short pattern repeated." };
  }
  if (isSequentialPassword(password)) {
    return {
      ok: false,
      reason: "Password must not be a keyboard or numeric sequence.",
    };
  }
  if (COMMON_PASSWORDS.has(password.toLowerCase())) {
    return {
      ok: false,
      reason: "Password is in the list of commonly used or breached passwords.",
    };
  }
  if (containsIdentity(password, ctx)) {
    return {
      ok: false,
      reason: "Password must not contain your email or name.",
    };
  }
  return { ok: true };
}
