/**
 * `@platform-modules/util/crypto` — AES-GCM secrets, SHA-256 hex, portable timing-safe compare.
 *
 * `timingSafeEqual` is safe only for equal-expected-length comparands (hashes, tokens).
 * The length guard returns early — do not use for variable-length secrets.
 */

/** Coerce Uint8Array to BufferSource when DOM + @types/node widen ArrayBufferLike. */
function asBufferSource(bytes: Uint8Array): BufferSource {
  return bytes as BufferSource
}

/**
 * Portable constant-time string comparison (NOT `crypto.subtle.timingSafeEqual` — CF-only).
 * Safe only when `a` and `b` are expected to be the same length (e.g. hex hashes).
 */
export function timingSafeEqual(a: string, b: string): boolean {
  const encoder = new TextEncoder()
  const bufA = encoder.encode(a)
  const bufB = encoder.encode(b)
  if (bufA.byteLength !== bufB.byteLength) return false
  let diff = 0
  for (let i = 0; i < bufA.byteLength; i++) {
    diff |= bufA[i]! ^ bufB[i]!
  }
  return diff === 0
}

const GCM_IV_BYTES = 12

function b64ToBytes(b64: string): Uint8Array {
  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 bytesToB64(bytes: Uint8Array): string {
  let binary = ''
  for (const b of bytes) binary += String.fromCharCode(b)
  return btoa(binary)
}

async function importAesKey(keyB64: string): Promise<CryptoKey> {
  const raw = b64ToBytes(keyB64)
  if (raw.byteLength !== 32) {
    throw new Error('keyB64 must decode to exactly 32 bytes (AES-256)')
  }
  return crypto.subtle.importKey('raw', asBufferSource(raw), { name: 'AES-GCM' }, false, [
    'encrypt',
    'decrypt',
  ])
}

/** AES-256-GCM — returns base64( iv(12B) ‖ ciphertext+tag ). IV is random per call. */
export async function encryptSecret(plaintext: string, keyB64: string): Promise<string> {
  const key = await importAesKey(keyB64)
  const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_BYTES))
  const ct = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: asBufferSource(iv) },
    key,
    new TextEncoder().encode(plaintext),
  )
  const ctBytes = new Uint8Array(ct)
  const combined = new Uint8Array(iv.byteLength + ctBytes.byteLength)
  combined.set(iv, 0)
  combined.set(ctBytes, iv.byteLength)
  return bytesToB64(combined)
}

/** Decrypts base64( iv(12B) ‖ ciphertext+tag ). Throws on tamper, wrong key, or malformed input. */
export async function decryptSecret(blob: string, keyB64: string): Promise<string> {
  const key = await importAesKey(keyB64)
  const combined = b64ToBytes(blob)
  if (combined.byteLength <= GCM_IV_BYTES) {
    throw new Error('ciphertext too short')
  }
  const iv = combined.subarray(0, GCM_IV_BYTES)
  const ct = combined.subarray(GCM_IV_BYTES)
  const pt = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: asBufferSource(iv) },
    key,
    asBufferSource(ct),
  )
  return new TextDecoder().decode(pt)
}

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

/** URL-safe base64 without padding (`+`→`-`, `/`→`_`). */
export function base64url(bytes: ArrayBuffer | Uint8Array): string {
  const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
  let binary = ''
  for (const b of arr) binary += String.fromCharCode(b)
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

async function importHmacKey(secret: string, usages: KeyUsage[]): Promise<CryptoKey> {
  return crypto.subtle.importKey(
    'raw',
    asBufferSource(new TextEncoder().encode(secret)),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    usages,
  )
}

/**
 * HMAC-SHA-256 of `message` under `secret`, returned as URL-safe base64 (no padding).
 * Fail-closed on minting: throws on an empty `secret` rather than signing under a weak key.
 */
export async function hmacSign(message: string, secret: string): Promise<string> {
  if (!secret) throw new Error('hmacSign: secret must be non-empty')
  const key = await importHmacKey(secret, ['sign'])
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message))
  return base64url(sig)
}

/**
 * Constant-time verify of an `hmacSign` signature over `message` under `secret`.
 * Fail-closed: returns false on an empty `secret` or any mismatch — never throws.
 * Comparison is timing-safe (recompute-and-compare via {@link timingSafeEqual}).
 */
export async function hmacVerify(
  message: string,
  secret: string,
  signature: string,
): Promise<boolean> {
  if (!secret) return false
  const expected = await hmacSign(message, secret)
  return timingSafeEqual(expected, signature)
}
