import { timingSafeEqual } from '@platform-modules/util/crypto'
import type { DeriveBitsFn } from '@platform-modules/util/password'

const ALGO = 'SHA-512'
const ITERATIONS = 100_000
const KEY_BYTES = 64
const SALT_BYTES = 16
const HMAC_BYTES = 32 // HMAC-SHA-256 digest length (see hmacPepper)
export const SCHEME = 'pbkdf2sha512-pep'

export type PepperBinding = {
  /** Active pepper version label, e.g. `v1`. */
  currentVersion: string
  /** Version → secret from a CF binding — never stored in the DB. */
  secrets: Record<string, string>
}

function asBufferSource(bytes: Uint8Array): BufferSource {
  return bytes as BufferSource
}

function toB64(bytes: Uint8Array | ArrayBuffer): string {
  const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
  return btoa(String.fromCharCode(...arr))
}

function fromB64(b64: string): Uint8Array {
  return new Uint8Array(atob(b64).split('').map((c) => c.charCodeAt(0)))
}

async function pbkdf2Derive(
  plain: string,
  salt: Uint8Array,
  iterations: number,
): Promise<ArrayBuffer> {
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(plain),
    'PBKDF2',
    false,
    ['deriveBits'],
  )
  return crypto.subtle.deriveBits(
    { name: 'PBKDF2', salt: asBufferSource(salt), iterations, hash: ALGO },
    keyMaterial,
    KEY_BYTES * 8,
  )
}

/** P1a — after-KDF pepper: HMAC-SHA256(pbkdf2_digest, pepper). */
async function hmacPepper(derivedBits: ArrayBuffer, pepper: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(pepper),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sig = await crypto.subtle.sign('HMAC', key, derivedBits)
  return toB64(sig)
}

/**
 * Returns `pbkdf2sha512-pep$<pepperVer>$<iter>$<saltB64>$<hmacB64>`.
 * Pepper is applied AFTER PBKDF2 — never pbkdf2(HMAC(pw, pepper)).
 */
export async function hashPassword(
  plain: string,
  pepper: PepperBinding,
  opts?: { deriveBits?: DeriveBitsFn },
): Promise<string> {
  const derive = opts?.deriveBits ?? pbkdf2Derive
  const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES))
  const derived = await derive(plain, salt, ITERATIONS, ALGO, KEY_BYTES)
  const pepperSecret = pepper.secrets[pepper.currentVersion]
  if (!pepperSecret) {
    throw new Error(`pepper version not configured: ${pepper.currentVersion}`)
  }
  const hmacB64 = await hmacPepper(derived, pepperSecret)
  return `${SCHEME}$${pepper.currentVersion}$${ITERATIONS}$${toB64(salt)}$${hmacB64}`
}

/**
 * Builds a well-formed stored-hash string that `verifyPassword` evaluates in FULL (canonical
 * `ITERATIONS`, correct salt + hmac byte-lengths, the active pepper version) yet which no real
 * password can match. `signIn` runs it on the unknown/disabled-account branch so login latency is
 * constant-time regardless of whether the email exists — closing the account-enumeration timing
 * oracle. Derived from the module's own `ITERATIONS`, so the sentinel can NEVER drift from the
 * canonical KDF cost (e.g. a future P1b iteration bump): iteration-parity is structural, not copied.
 */
export function buildVerificationSentinel(pepperVersion: string): string {
  const salt = toB64(new Uint8Array(SALT_BYTES))
  const hmac = toB64(new Uint8Array(HMAC_BYTES))
  return `${SCHEME}$${pepperVersion}$${ITERATIONS}$${salt}$${hmac}`
}

/**
 * Returns true when the stored hash should be re-hashed on next successful login:
 * the pepper version is stale OR the stored work factor is below canonical ITERATIONS —
 * but never when the stored factor exceeds ITERATIONS (never silently downgrade a hash
 * imported at higher cost — raise ITERATIONS instead).
 *
 * Pepper rotation runbook:
 *   1. Add new version to secrets; set currentVersion to it.
 *   2. Monitor via countStalePasswordHashes until 0 old-version hashes remain.
 *   3. Only then remove the old version from secrets.
 * Skipping step 2 creates a timing gap: old-version hashes hit the early-return in verifyPassword
 * while the sentinel runs the full KDF, leaking which emails have un-migrated accounts.
 */
export function shouldUpgradeHash(stored: string, currentPepperVersion: string): boolean {
  const parts = stored.split('$')
  if (parts.length !== 5 || parts[0] !== SCHEME) return false
  const storedVer = parts[1]
  const storedIter = Number(parts[2])
  if (!Number.isInteger(storedIter) || storedIter > ITERATIONS) return false
  return storedVer !== currentPepperVersion || storedIter < ITERATIONS
}

/** Parses iter, salt, and pepper version from `stored` — never from module constants alone. */
export async function verifyPassword(
  plain: string,
  stored: string,
  pepper: PepperBinding,
  opts?: { deriveBits?: DeriveBitsFn },
): Promise<boolean> {
  const derive = opts?.deriveBits ?? pbkdf2Derive
  const parts = stored.split('$')
  if (parts.length !== 5 || parts[0] !== SCHEME) return false

  const [, pepperVer, iterStr, saltB64, expectedHmacB64] = parts
  const pepperSecret = pepper.secrets[pepperVer!]
  if (!pepperSecret) return false

  const iterations = Number(iterStr)
  if (!Number.isInteger(iterations) || iterations <= 0) return false

  let salt: Uint8Array
  try {
    salt = fromB64(saltB64!)
  } catch {
    return false
  }

  const derived = await derive(plain, salt, iterations, ALGO, KEY_BYTES)
  const hmacB64 = await hmacPepper(derived, pepperSecret)
  return timingSafeEqual(hmacB64, expectedHmacB64!)
}
