import 'server-only'

import { randomBytes, randomInt, scrypt, timingSafeEqual } from 'node:crypto'

/**
 * Password hashing for admin-set passwords (2026-09-11).
 *
 * scrypt from Node's standard library — no new dependency. Memory-hard, so a
 * stolen table is expensive to brute-force. Parameters are stored with each
 * hash so they can be raised later without invalidating existing passwords.
 *
 * Format: scrypt$<N>$<r>$<p>$<salt b64>$<hash b64>
 */

const N = 32768 // 2^15 — ~32 MB per hash, a few tens of ms
const R = 8
const P = 1
const KEYLEN = 64
const MAXMEM = 64 * 1024 * 1024

function derive(password: string, salt: Buffer, n: number, r: number, p: number): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    scrypt(password, salt, KEYLEN, { N: n, r, p, maxmem: MAXMEM }, (err, key) =>
      err ? reject(err) : resolve(key),
    )
  })
}

export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16)
  const key = await derive(password, salt, N, R, P)
  return ['scrypt', N, R, P, salt.toString('base64'), key.toString('base64')].join('$')
}

/** Constant-time check. Any malformed hash is a failed login, never an error. */
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const parts = stored.split('$')
  if (parts.length !== 6 || parts[0] !== 'scrypt') return false
  const [, n, r, p, saltB64, hashB64] = parts
  const expected = Buffer.from(hashB64, 'base64')
  if (expected.length !== KEYLEN) return false
  try {
    const actual = await derive(password, Buffer.from(saltB64, 'base64'), Number(n), Number(r), Number(p))
    return timingSafeEqual(actual, expected)
  } catch {
    return false
  }
}

/**
 * A temporary password an admin reads out to a user on a support call.
 * Unambiguous characters only (no 0/O, 1/l/I), grouped so it can be spoken:
 * e.g. "Kq7m-Xp2t-Hn4w-Rb9c" — 16 characters, ~79 bits.
 */
const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'

export function generateTemporaryPassword(): string {
  // Re-draw until it satisfies the same rule a typed password must meet: with
  // only 8 digits in a 55-character alphabet, about 1 draw in 12 has no digit.
  // Re-drawing (rather than forcing a digit into a fixed slot) keeps every
  // position uniformly random.
  for (;;) {
    const groups: string[] = []
    for (let g = 0; g < 4; g++) {
      let group = ''
      for (let i = 0; i < 4; i++) group += ALPHABET[randomInt(ALPHABET.length)]
      groups.push(group)
    }
    const candidate = groups.join('-')
    if (passwordProblem(candidate) === null) return candidate
  }
}

/** Minimum rules for a password an admin types in themselves. */
export function passwordProblem(password: string): string | null {
  if (password.length < 10) return 'Use at least 10 characters.'
  if (password.length > 200) return 'Use at most 200 characters.'
  if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) {
    return 'Use at least one letter and one number.'
  }
  return null
}
