/**
 * password-policy — pure strong-password policy shared by server and client.
 *
 * No DB, no React, no dependency beyond zod. Single source of truth for the
 * minimum length (Better Auth's minPasswordLength mirrors it), the Zod schema
 * used in forms, the server-side gate (validatePassword) and the signup gauge
 * heuristic (scorePassword).
 */
import { z } from "zod";

/** pwgen -y -s 14 equivalent — long enough to resist offline cracking. */
export const MIN_PASSWORD_LENGTH = 14;
/** Mirror of Better Auth's maxPasswordLength (bcrypt-safe upper bound). */
export const MAX_PASSWORD_LENGTH = 128;

/** Stable criterion ids — used as issue messages and gauge hint keys. */
export type PasswordCriterion =
  | "length"
  | "lower"
  | "upper"
  | "digit"
  | "symbol";

const HAS_LOWER = /[a-z]/;
const HAS_UPPER = /[A-Z]/;
const HAS_DIGIT = /[0-9]/;
/** Any non-alphanumeric character counts as a symbol (incl. spaces, accents). */
const HAS_SYMBOL = /[^a-zA-Z0-9]/;

/** Unmet criteria for a candidate password, in stable display order. */
export function missingCriteria(pw: string): PasswordCriterion[] {
  const missing: PasswordCriterion[] = [];
  if (pw.length < MIN_PASSWORD_LENGTH) missing.push("length");
  if (!HAS_LOWER.test(pw)) missing.push("lower");
  if (!HAS_UPPER.test(pw)) missing.push("upper");
  if (!HAS_DIGIT.test(pw)) missing.push("digit");
  if (!HAS_SYMBOL.test(pw)) missing.push("symbol");
  return missing;
}

/**
 * Strong-password schema. Issue messages are the stable criterion ids
 * ("length" | "lower" | "upper" | "digit" | "symbol" | "max") so callers can
 * map them to localized copy.
 */
export const passwordSchema = z
  .string()
  .min(MIN_PASSWORD_LENGTH, "length")
  .max(MAX_PASSWORD_LENGTH, "max")
  .refine((pw) => HAS_LOWER.test(pw), "lower")
  .refine((pw) => HAS_UPPER.test(pw), "upper")
  .refine((pw) => HAS_DIGIT.test(pw), "digit")
  .refine((pw) => HAS_SYMBOL.test(pw), "symbol");

/**
 * Gauge heuristic: one point per criterion met (length≥14, lower, upper,
 * digit, symbol) + length bonuses at ≥18 and ≥24, mapped to a 0–4 score
 * (0–1 weak, 2 fair, 3 good, 4 strong). Deterministic; an invalid password
 * (any criterion missing) can never score above 2.
 */
export function scorePassword(pw: string): {
  score: 0 | 1 | 2 | 3 | 4;
  missing: PasswordCriterion[];
} {
  const missing = missingCriteria(pw);
  let points = 5 - missing.length;
  if (pw.length >= 18) points += 1;
  if (pw.length >= 24) points += 1;
  let score: 0 | 1 | 2 | 3 | 4;
  if (points <= 1) score = 0;
  else if (points === 2) score = 1;
  else if (points <= 4) score = 2;
  else if (points === 5) score = 3;
  else score = 4;
  // Length bonuses never lift an invalid password past "fair".
  if (missing.length > 0 && score > 2) score = 2;
  return { score, missing };
}

/**
 * Policy gate: ok only when every criterion is met and the password is not
 * over the max length. `missing` lists the unmet criterion ids.
 */
export function validatePassword(
  pw: string,
): { ok: true } | { ok: false; missing: PasswordCriterion[] } {
  const missing = missingCriteria(pw);
  if (missing.length > 0 || pw.length > MAX_PASSWORD_LENGTH) {
    return { ok: false, missing };
  }
  return { ok: true };
}
