/**
 * Water Log — PIN hashing.
 *
 * PINs in TYLER OS are 4-digit numerics that field workers type on a wet
 * phone screen. That makes them both *very* low-entropy and *very*
 * frequently entered. A slow KDF (scrypt) gives us a defense-in-depth
 * layer: even if the database leaks, an attacker still has to do real
 * work per guess.
 *
 * Format: `scrypt$N$r$p$salt_b64$hash_b64` — self-describing so we can
 * upgrade the parameters later without forking the data. No third-party
 * deps; Node's built-in `crypto.scryptSync` is plenty.
 */
import "server-only";
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";

const N = 16384; // CPU/memory cost
const r = 8;
const p = 1;
const KEY_LEN = 32;
const SALT_LEN = 16;

export const PIN_MIN = 4;
export const PIN_MAX = 8;

/**
 * Validate raw PIN format. Returns null on success, an error string on failure.
 * Used by API entry points and unit tests alike.
 */
export function validatePin(rawPin: unknown): string | null {
  if (typeof rawPin !== "string") return "PIN must be a string";
  if (rawPin.length < PIN_MIN || rawPin.length > PIN_MAX) {
    return `PIN must be ${PIN_MIN}–${PIN_MAX} digits`;
  }
  if (!/^\d+$/.test(rawPin)) return "PIN must be numeric";
  // Catch obviously weak PINs (1111, 1234, 0000, …). These are still
  // possible to use, but we warn. We don't reject — the customer
  // explicitly asked for simple PINs.
  return null;
}

/** True if the PIN is "weak" (all-same, monotonic, or palindromic). */
export function isWeakPin(rawPin: string): boolean {
  if (/^(.)\1+$/.test(rawPin)) return true; // 1111, 2222
  if ("0123456789".includes(rawPin)) return true; // 1234, 5678
  if ("9876543210".includes(rawPin)) return true; // 4321, 8765
  if (rawPin === rawPin.split("").reverse().join("")) return true; // 1221
  return false;
}

export function hashPin(rawPin: string): string {
  const salt = randomBytes(SALT_LEN);
  const hash = scryptSync(rawPin.normalize("NFKC"), salt, KEY_LEN, {
    N,
    r,
    p,
  });
  return `scrypt$${N}$${r}$${p}$${salt.toString("base64")}$${hash.toString(
    "base64",
  )}`;
}

export function verifyPin(rawPin: string, stored: string): boolean {
  if (typeof stored !== "string" || !stored.startsWith("scrypt$")) {
    return false;
  }
  const parts = stored.split("$");
  if (parts.length !== 6) return false;
  const [, nStr, rStr, pStr, saltB64, hashB64] = parts;
  const n = parseInt(nStr, 10);
  const r = parseInt(rStr, 10);
  const p = parseInt(pStr, 10);
  if (!Number.isFinite(n) || !Number.isFinite(r) || !Number.isFinite(p)) {
    return false;
  }
  let salt: Buffer;
  let expected: Buffer;
  try {
    salt = Buffer.from(saltB64, "base64");
    expected = Buffer.from(hashB64, "base64");
  } catch {
    return false;
  }
  if (expected.length !== KEY_LEN) return false;

  let candidate: Buffer;
  try {
    candidate = scryptSync(rawPin.normalize("NFKC"), salt, KEY_LEN, {
      N: n,
      r,
      p,
    });
  } catch {
    return false;
  }
  return candidate.length === expected.length && timingSafeEqual(candidate, expected);
}

/** Generate a random 4-digit PIN. Used by the create-user action. */
export function generatePin(): string {
  // Reject obviously weak PINs at generation time so the admin never has
  // to hand a brand-new worker a PIN of "1234".
  for (let attempt = 0; attempt < 16; attempt++) {
    const candidate = (
      Math.floor(Math.random() * 10000) + 0
    )
      .toString()
      .padStart(4, "0");
    if (!isWeakPin(candidate)) return candidate;
  }
  // Fallback: still return something — this branch is essentially impossible
  return Math.floor(Math.random() * 10000)
    .toString()
    .padStart(4, "0");
}
