/**
 * Raw Web-Crypto HS256 session tokens.
 * Recomputes HMAC over received `header.payload`; never trusts header `alg`.
 */

const ALG = { name: 'HMAC', hash: 'SHA-256' } as const
const SKEW_SECS = 30
const DEFAULT_ACCESS_TTL_SECS = 15 * 60

const enc = new TextEncoder()

const _hmacKeyCache = new Map<string, CryptoKey>()

export type AccessClaims = {
  sub: string
  sid: string
  sv: number
  iat: number
  exp: number
}

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

function base64UrlToBytes(s: string): Uint8Array {
  const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4))
  const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad
  const binary = atob(b64)
  const out = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)
  return out
}

function jsonToBase64Url(obj: unknown): string {
  return bufToBase64Url(enc.encode(JSON.stringify(obj)).buffer)
}

async function importHmacKey(secret: string): Promise<CryptoKey> {
  const cached = _hmacKeyCache.get(secret)
  if (cached) return cached
  const key = await crypto.subtle.importKey('raw', enc.encode(secret), ALG, false, [
    'sign',
    'verify',
  ])
  _hmacKeyCache.set(secret, key)
  return key
}

const HEADER_B64 = jsonToBase64Url({ alg: 'HS256', typ: 'JWT' })

export async function signSession(
  claims: Omit<AccessClaims, 'iat' | 'exp'>,
  secret: string,
  ttlSec = DEFAULT_ACCESS_TTL_SECS,
): Promise<string> {
  const now = Math.floor(Date.now() / 1000)
  const payload: AccessClaims = { ...claims, iat: now, exp: now + ttlSec }
  const headerPayload = `${HEADER_B64}.${jsonToBase64Url(payload)}`
  const key = await importHmacKey(secret)
  const sig = await crypto.subtle.sign(ALG, key, enc.encode(headerPayload))
  return `${headerPayload}.${bufToBase64Url(sig)}`
}

/** Verify against one or more secrets (rotation). Returns null on any failure — never throws. */
export async function verifySession(
  jwt: string,
  secrets: string | string[],
  opts?: { minSessionVersion?: number },
): Promise<AccessClaims | null> {
  try {
    const parts = jwt.split('.')
    if (parts.length !== 3) return null

    const [headerB64, payloadB64, sigB64] = parts as [string, string, string]
    const sigBytes = base64UrlToBytes(sigB64)
    const headerPayload = enc.encode(`${headerB64}.${payloadB64}`)

    const secretList = Array.isArray(secrets) ? secrets : [secrets]
    let verified = false
    for (const secret of secretList) {
      const key = await importHmacKey(secret)
      const ok = await crypto.subtle.verify(
        ALG,
        key,
        sigBytes.buffer as ArrayBuffer,
        headerPayload,
      )
      if (ok) {
        verified = true
        break
      }
    }
    if (!verified) return null

    const raw = JSON.parse(new TextDecoder().decode(base64UrlToBytes(payloadB64))) as Record<
      string,
      unknown
    >
    const now = Math.floor(Date.now() / 1000)

    if (typeof raw.exp !== 'number' || now > raw.exp + SKEW_SECS) return null
    if (typeof raw.iat !== 'number' || raw.iat > now + SKEW_SECS) return null
    if (typeof raw.sub !== 'string' || typeof raw.sid !== 'string' || typeof raw.sv !== 'number') {
      return null
    }

    if (opts?.minSessionVersion !== undefined && raw.sv < opts.minSessionVersion) {
      return null
    }

    return {
      sub: raw.sub,
      sid: raw.sid,
      sv: raw.sv,
      iat: raw.iat,
      exp: raw.exp,
    }
  } catch {
    return null
  }
}

export type CookieSerializeOptions = {
  maxAgeSec: number
  httpOnly?: boolean
  secure?: boolean
  sameSite?: 'Lax' | 'Strict' | 'None'
  path?: string
}

const _cookieReCache = new Map<string, RegExp>()

function cookieRe(name: string): RegExp {
  const cached = _cookieReCache.get(name)
  if (cached) return cached
  const re = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]+)`)
  _cookieReCache.set(name, re)
  return re
}

export function parseCookie(cookieHeader: string, name: string): string | null {
  const m = cookieHeader.match(cookieRe(name))
  return m?.[1]?.trim() ?? null
}

export function serializeCookie(name: string, value: string, opts: CookieSerializeOptions): string {
  const parts = [`${name}=${value}`, `Max-Age=${opts.maxAgeSec}`, `Path=${opts.path ?? '/'}`]
  if (opts.httpOnly !== false) parts.push('HttpOnly')
  if (opts.secure) parts.push('Secure')
  parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`)
  return parts.join('; ')
}

export function clearCookie(name: string, opts?: { secure?: boolean; path?: string }): string {
  return serializeCookie(name, '', {
    maxAgeSec: 0,
    secure: opts?.secure,
    path: opts?.path,
  })
}

export const ACCESS_TOKEN_TTL_SECS = DEFAULT_ACCESS_TTL_SECS
export const REFRESH_TOKEN_TTL_SECS = 30 * 24 * 60 * 60
