import { hmacSign, hmacVerify } from '@platform-modules/util/crypto'
import type { AntiSpamConfig, AntiSpamVerdict, ChallengeAdapter, FormDef } from './types.js'

export type { AntiSpamConfig, AntiSpamVerdict, ChallengeAdapter }

/**
 * Server-issued, HMAC-signed render token. The timestamp is signed so a client cannot
 * forge `renderedAt` — `verifyRenderToken` recomputes the MAC over the EMBEDDED timestamp
 * before trusting it. Host emits at form-render, embeds as a hidden field.
 * Fail-closed: throws on an empty secret (delegated to `hmacSign`).
 */
export async function issueRenderToken(secret: string, now: number): Promise<string> {
  const t = String(now)
  const sig = await hmacSign(t, secret)
  return `${t}.${sig}`
}

/**
 * Constant-time verify of a render token. Recomputes the HMAC over the embedded timestamp
 * and compares (timing-safe) BEFORE reading the age — a forged/tampered token yields
 * `{ ok: false }` with NO `ageMs` leaked. Fail-closed on empty secret.
 */
export async function verifyRenderToken(
  secret: string,
  token: string,
  now: number,
): Promise<{ ok: boolean; ageMs?: number }> {
  const dot = token.indexOf('.')
  if (dot <= 0) return { ok: false }
  const t = token.slice(0, dot)
  const sig = token.slice(dot + 1)
  const ok = await hmacVerify(t, secret, sig) // verify BEFORE trusting t
  if (!ok) return { ok: false }
  const issued = Number(t)
  if (!Number.isFinite(issued)) return { ok: false }
  return { ok: true, ageMs: now - issued }
}

/**
 * Run the configured anti-spam checks. Returns a verdict — the CALLER decides reject-vs-quarantine.
 * Policy is FLAG, not silently drop (data-loss floor): `recordSubmission` stores spam=true.
 * The time-trap reads `ageMs` from the signed render token, never a client `renderedAt`.
 */
export async function checkAntiSpam(
  form: FormDef,
  raw: Record<string, unknown>,
  ctx: { renderToken?: string; secret?: string; now: number; challengeToken?: string; ip?: string },
): Promise<AntiSpamVerdict> {
  const cfg = form.antispam
  if (!cfg) return { ok: true }

  // 1. honeypot — a trap field that must stay empty
  if (cfg.honeypot) {
    const v = raw[cfg.honeypot]
    if (v !== undefined && v !== null && String(v) !== '') {
      return { ok: false, reason: 'honeypot' }
    }
  }

  // 2. time-trap — signed render token; age from verifyRenderToken, never a client renderedAt
  if (cfg.minFillMs !== undefined) {
    if (!ctx.renderToken || !ctx.secret) return { ok: false, reason: 'bad-token' }
    const v = await verifyRenderToken(ctx.secret, ctx.renderToken, ctx.now)
    if (!v.ok || v.ageMs === undefined) return { ok: false, reason: 'bad-token' }
    if (v.ageMs < cfg.minFillMs) return { ok: false, reason: 'too-fast' }
  }

  // 3. challenge adapter (captcha/turnstile) — adopter-supplied, never bundled
  if (cfg.challenge) {
    const passed = await cfg.challenge.verify(ctx.challengeToken ?? '', { ip: ctx.ip })
    if (!passed) return { ok: false, reason: 'challenge-failed' }
  }

  return { ok: true }
}
