import { hash, verify } from '@node-rs/argon2';
import type { PasswordHasher } from '@i-learned/core';

/**
 * `Algorithm.Argon2id` from @node-rs/argon2. Inlined as its numeric value
 * because the library declares it as an ambient `const enum`, which cannot be
 * imported under `verbatimModuleSyntax`. Asserted by a unit test so a library
 * change cannot silently downgrade us to Argon2i or Argon2d.
 */
const ARGON2ID = 2;

/**
 * Argon2id password hashing (SPEC.md §5, §25).
 *
 * Parameters follow the OWASP Password Storage Cheat Sheet's Argon2id
 * recommendation: 19 MiB of memory, 2 iterations, 1 degree of parallelism.
 * Memory cost is the parameter that matters against GPU attack, which is why
 * it is set well above the library default.
 *
 * The `@node-rs` binding ships prebuilt binaries, so no C toolchain is needed
 * on a Windows workstation — relevant because the institution's machines are
 * unlikely to have one.
 */
export const ARGON2_OPTIONS = {
  algorithm: ARGON2ID,
  memoryCost: 19_456, // KiB
  timeCost: 2,
  parallelism: 1,
  outputLen: 32,
} as const;

export class Argon2PasswordHasher implements PasswordHasher {
  async hash(plaintext: string): Promise<string> {
    return hash(plaintext, ARGON2_OPTIONS);
  }

  /**
   * Verifies a password. Returns `false` rather than throwing when the stored
   * value is not a valid Argon2 hash — a malformed row must read as "wrong
   * password", never as a server error that reveals the account exists.
   */
  async verify(hashValue: string, plaintext: string): Promise<boolean> {
    try {
      return await verify(hashValue, plaintext, ARGON2_OPTIONS);
    } catch {
      return false;
    }
  }

  /**
   * Whether the stored hash was produced with weaker parameters than current
   * policy. Callers re-hash transparently on the next successful login, so
   * raising the cost factors upgrades existing accounts without a reset.
   */
  needsRehash(hashValue: string): boolean {
    if (!hashValue.startsWith('$argon2id$')) return true;

    const memoryMatch = /\bm=(\d+)/.exec(hashValue);
    const timeMatch = /\bt=(\d+)/.exec(hashValue);
    if (!memoryMatch || !timeMatch) return true;

    return (
      Number(memoryMatch[1]) < ARGON2_OPTIONS.memoryCost ||
      Number(timeMatch[1]) < ARGON2_OPTIONS.timeCost
    );
  }
}

export const passwordHasher = new Argon2PasswordHasher();

/**
 * Password strength policy (SPEC.md §25).
 *
 * Length is the dominant factor, so the minimum is 12 rather than the more
 * common 8. Composition rules beyond that are kept deliberately mild: they
 * push users toward predictable substitutions without adding real entropy.
 */
export const PASSWORD_POLICY = {
  minLength: 12,
  maxLength: 128,
  requireLetter: true,
  requireNumber: true,
} as const;

export interface PasswordCheck {
  readonly valid: boolean;
  readonly problems: readonly string[];
}

export function checkPasswordStrength(password: string): PasswordCheck {
  const problems: string[] = [];

  if (password.length < PASSWORD_POLICY.minLength) {
    problems.push(`Use at least ${PASSWORD_POLICY.minLength} characters.`);
  }
  if (password.length > PASSWORD_POLICY.maxLength) {
    problems.push(`Use at most ${PASSWORD_POLICY.maxLength} characters.`);
  }
  if (PASSWORD_POLICY.requireLetter && !/[a-zA-Z]/.test(password)) {
    problems.push('Include at least one letter.');
  }
  if (PASSWORD_POLICY.requireNumber && !/\d/.test(password)) {
    problems.push('Include at least one number.');
  }
  if (/^(.)\1+$/.test(password)) {
    problems.push('Do not use a single repeated character.');
  }

  return { valid: problems.length === 0, problems };
}
