/**
 * Opaque & signed tokens — foundation-auth-rbac.
 *
 *  - generateOpaqueToken: 32-byte random, base64url. The PLAINTEXT is emailed
 *    (invitations, refresh tokens, reset links); only its hash is stored.
 *  - hashToken: SHA-256 hex of the plaintext — the DB column value.
 *  - verifyToken: timing-safe compare of hashToken(plain) against storedHash.
 *  - signSignedToken / verifySignedToken: short-lived JWTs (jose) for
 *    password-reset and email-verification links (no DB row needed).
 */
import { SignJWT, jwtVerify } from 'jose'
import { timingSafeEqual } from './crypto'

function bytesToBase64Url(bytes: Uint8Array): string {
  let binary = ''
  for (const b of bytes) binary += String.fromCharCode(b)
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

/** N cryptographically-random bytes (default 32), base64url-encoded (no padding). */
export function generateOpaqueToken(byteLength = 32): string {
  const bytes = crypto.getRandomValues(new Uint8Array(byteLength))
  return bytesToBase64Url(bytes)
}

/** SHA-256 hex digest of `plain` — the value stored in the DB. */
export async function hashToken(plain: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(plain))
  return Array.from(new Uint8Array(digest))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

/**
 * True when `plain` hashes to `storedHash`. Uses timingSafeEqual — never `===`.
 */
export async function verifyToken(plain: string, storedHash: string): Promise<boolean> {
  const computed = await hashToken(plain)
  return timingSafeEqual(computed, storedHash)
}

/**
 * Sign a short-lived claims JWT (HS256) valid for `ttlSeconds`.
 * Used for 1h password-reset / email-verification links.
 */
export async function signSignedToken(
  claims: Record<string, unknown>,
  secret: string,
  ttlSeconds: number,
): Promise<string> {
  const secretKey = new TextEncoder().encode(secret)
  return new SignJWT({ ...claims })
    .setProtectedHeader({ alg: 'HS256' })
    .setJti(generateOpaqueToken(16))
    .setIssuedAt()
    .setExpirationTime(`${ttlSeconds}s`)
    .sign(secretKey)
}

/** KV key prefix for consumed password-reset JWT jti values. */
export const PASSWORD_RESET_JTI_PREFIX = 'pwd_reset_jti'

/** KV key prefix for consumed email-verification JWT jti values. */
export const VERIFY_EMAIL_JTI_PREFIX = 'email_verify_jti'

/**
 * Mark a signed-token jti as consumed in KV (single-use guard).
 * Returns false when the jti was already consumed.
 */
export async function consumeSignedTokenJti(
  kv: KVNamespace,
  prefix: string,
  jti: string,
  ttlSeconds: number,
): Promise<boolean> {
  const key = `${prefix}:${jti}`
  if (await kv.get(key)) return false
  await kv.put(key, '1', { expirationTtl: Math.max(1, ttlSeconds) })
  return true
}

/** Verify a signed token JWT. Throws on invalid/expired/tampered token. */
export async function verifySignedToken(
  token: string,
  secret: string,
): Promise<Record<string, unknown>> {
  const secretKey = new TextEncoder().encode(secret)
  const { payload } = await jwtVerify(token, secretKey)
  return payload as Record<string, unknown>
}
