/**
 * `@platform-modules/util/password` — PBKDF2-SHA-512 password hashing.
 *
 * Stored format: `pbkdf2sha512$<iter>$<saltB64>$<hashB64>` (salt and hash are standard base64).
 */
import { timingSafeEqual } from './crypto'

const ALGO = 'SHA-512'
// workerd hard-caps PBKDF2 at 100_000 iterations — deriveBits throws above it
// (https://github.com/cloudflare/workerd/issues/1346). OWASP recommends higher, but
// every workerd context (handler, DO, Queue) shares this ceiling.
const ITERATIONS = 100_000
const KEY_BYTES = 64
const SALT_BYTES = 16

export type DeriveBitsFn = (
  plain: string,
  salt: Uint8Array,
  iterations: number,
  hash: string,
  keyBytes: number,
) => Promise<ArrayBuffer>

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,
  hash: string,
  keyBytes: 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 },
    keyMaterial,
    keyBytes * 8,
  )
}

/** Returns `pbkdf2sha512$<iter>$<saltB64>$<hashB64>`. */
export async function hashPassword(
  plain: string,
  opts?: { deriveBits?: DeriveBitsFn },
): Promise<string> {
  const derive = opts?.deriveBits ?? pbkdf2Derive
  const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES))
  const bits = await derive(plain, salt, ITERATIONS, ALGO, KEY_BYTES)
  return `pbkdf2sha512$${ITERATIONS}$${toB64(salt)}$${toB64(bits)}`
}

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

  const [, iterStr, saltB64, hashB64] = parts
  const iterations = Number(iterStr)
  if (!Number.isInteger(iterations) || iterations <= 0) return false

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

  const bits = await derive(plain, salt, iterations, ALGO, KEY_BYTES)
  return timingSafeEqual(toB64(bits), hashB64!)
}
