// Stateless WebAuthn challenge store backed by HMAC-signed cookies.
//
// Why not an in-memory Map: Nuxt runs under PM2 cluster mode (multiple
// workers). A challenge stored in worker A's memory is invisible to worker B,
// so login-options and login-verify can land on different workers and fail
// with "Challenge expired or invalid". Signing the challenge into a cookie is
// stateless across workers and processes.
//
// Single-use: the cookie is deleted on every consumeChallenge() call, so a
// challenge cannot be replayed within the same browser. Cross-browser replay
// is bounded by the 5-minute TTL, same as the previous Map-based store.

import { createHmac, timingSafeEqual as nodeTimingSafeEqual } from 'node:crypto'

const CHALLENGE_TTL_MS = 5 * 60 * 1000
const COOKIE_PATH = '/api/idempiere-auth/webauthn'

export type ChallengeKind = 'register' | 'login'

interface SignedPayload {
  c: string            // challenge (the base64url string @simplewebauthn issued)
  k: ChallengeKind
  u?: number           // userId (only set for register)
  e: number            // expiresAt (ms epoch)
}

// Domain-separated subkey: don't reuse the AES-GCM key bytes as an HMAC key.
function getSigningKey(): Buffer {
  const hex = process.env.CREDENTIAL_ENCRYPTION_KEY
  if (!hex) {
    throw new Error('CREDENTIAL_ENCRYPTION_KEY env var is not set — cannot sign WebAuthn challenges')
  }
  return createHmac('sha256', Buffer.from(hex, 'hex')).update('wa-challenge-v1').digest()
}

function b64urlEncode(buf: Buffer): string {
  return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

function b64urlDecode(s: string): Buffer {
  const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))
  return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64')
}

function sign(payloadB64: string): string {
  return b64urlEncode(createHmac('sha256', getSigningKey()).update(payloadB64).digest())
}

function safeEqual(a: string, b: string): boolean {
  const ab = Buffer.from(a)
  const bb = Buffer.from(b)
  if (ab.length !== bb.length) return false
  return nodeTimingSafeEqual(ab, bb)
}

function cookieName(kind: ChallengeKind): string {
  return kind === 'register' ? 'logship_wa_reg' : 'logship_wa_login'
}

export function issueChallenge(
  event: any,
  challenge: string,
  opts: { kind: ChallengeKind; userId?: number },
): void {
  const payload: SignedPayload = {
    c: challenge,
    k: opts.kind,
    u: opts.userId,
    e: Date.now() + CHALLENGE_TTL_MS,
  }
  const payloadB64 = b64urlEncode(Buffer.from(JSON.stringify(payload), 'utf8'))
  const value = payloadB64 + '.' + sign(payloadB64)
  setCookie(event, cookieName(opts.kind), value, {
    httpOnly: true,
    sameSite: 'strict',
    secure: !import.meta.dev,
    path: COOKIE_PATH,
    maxAge: Math.floor(CHALLENGE_TTL_MS / 1000),
  })
}

export function consumeChallenge(
  event: any,
  kind: ChallengeKind,
  expectedChallenge: string,
): { userId?: number } | null {
  const name = cookieName(kind)
  const raw = getCookie(event, name)
  // Single-use: clear the cookie regardless of whether verification succeeds.
  deleteCookie(event, name, { path: COOKIE_PATH })
  if (!raw) return null
  const dot = raw.indexOf('.')
  if (dot < 0) return null
  const payloadB64 = raw.slice(0, dot)
  const sig = raw.slice(dot + 1)
  if (!safeEqual(sig, sign(payloadB64))) return null
  let payload: SignedPayload
  try {
    payload = JSON.parse(b64urlDecode(payloadB64).toString('utf8'))
  } catch {
    return null
  }
  if (payload.k !== kind) return null
  if (payload.e < Date.now()) return null
  if (payload.c !== expectedChallenge) return null
  return { userId: payload.u }
}
