/**
 * Signed session token — an HMAC-SHA256 MAC over a short-TTL expiry claim. The cookie value is
 * `<expEpochSec>.<macBase64url>`; the MAC covers the expiry so a client cannot extend its own
 * session. Verification is constant-time and rejects tampering, wrong-secret, and past-expiry
 * tokens. Self-contained (node:crypto only) — no external signing dependency.
 */
import { createHmac } from 'node:crypto'
import { constantTimeEqualBytes } from './compare.js'

/** Session lifetime (seconds): 12h — long enough to avoid constant re-auth, short enough to bound replay. */
export const SESSION_TTL_SEC = 12 * 60 * 60

function macFor(secret: string, payload: string): Buffer {
  return createHmac('sha256', secret).update(payload, 'utf8').digest()
}

function b64url(buf: Buffer): string {
  return buf.toString('base64url')
}

/**
 * Mint a session token that expires `ttlSec` after `nowMs`. The expiry is embedded and signed.
 */
export function createSessionToken(secret: string, nowMs: number, ttlSec: number = SESSION_TTL_SEC): string {
  const expSec = Math.floor(nowMs / 1000) + ttlSec
  const payload = String(expSec)
  return `${payload}.${b64url(macFor(secret, payload))}`
}

/**
 * True iff `token` is a well-formed, correctly-signed, unexpired session token.
 * Any structural problem, MAC mismatch, or past-expiry ⇒ false (deny-by-default).
 */
export function verifySessionToken(secret: string, token: string | undefined, nowMs: number): boolean {
  if (typeof token !== 'string' || token.length === 0) return false
  const dot = token.indexOf('.')
  if (dot <= 0 || dot === token.length - 1) return false
  const payload = token.slice(0, dot)
  const presentedMacB64 = token.slice(dot + 1)
  // Expiry must be a positive integer string; reject anything else before touching crypto.
  if (!/^\d+$/.test(payload)) return false

  let presentedMac: Buffer
  try {
    presentedMac = Buffer.from(presentedMacB64, 'base64url')
  } catch {
    return false
  }
  const expectedMac = macFor(secret, payload)
  if (!constantTimeEqualBytes(presentedMac, expectedMac)) return false

  const expSec = Number(payload)
  return Number.isSafeInteger(expSec) && expSec * 1000 > nowMs
}
