/** Workers runtime extends SubtleCrypto with timingSafeEqual (not in DOM lib types). */
type WorkerSubtleCrypto = SubtleCrypto & {
  timingSafeEqual(a: BufferSource, b: BufferSource): boolean
}

function workerSubtle(): WorkerSubtleCrypto {
  return crypto.subtle as WorkerSubtleCrypto
}

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

/**
 * Timing-safe string comparison.
 * Use for ALL token / HMAC / secret / hash comparisons — never use === on secrets.
 * Cloudflare Workers runtime exposes crypto.subtle.timingSafeEqual (non-standard extension).
 */
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
  return workerSubtle().timingSafeEqual(bufA, bufB)
}

/**
 * AES-256-GCM secret encryption — foundation-auth-rbac.
 *
 * Used to encrypt `admin_users.totp_secret` at rest with `ADMIN_ENCRYPTION_KEY`
 * (a base64 32-byte key). Output format: base64( iv(12B) || ciphertext+tag ).
 * A fresh random 12-byte IV is generated per call.
 */
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('ADMIN_ENCRYPTION_KEY must decode to exactly 32 bytes (AES-256)')
  }
  return workerSubtle().importKey('raw', asBufferSource(raw), { name: 'AES-GCM' }, false, [
    'encrypt',
    'decrypt',
  ])
}

/**
 * Encrypt `plain` with AES-256-GCM under `keyB64`.
 * Returns base64( iv(12B) || ciphertext+tag ). IV is random per call.
 */
export async function encryptSecret(plain: string, keyB64: string): Promise<string> {
  const key = await importAesKey(keyB64)
  const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_BYTES))
  const ct = await workerSubtle().encrypt(
    { name: 'AES-GCM', iv: asBufferSource(iv) },
    key,
    new TextEncoder().encode(plain),
  )
  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)
}

/**
 * Decrypt a base64( iv(12B) || ciphertext+tag ) blob produced by encryptSecret.
 * Throws on tamper / wrong key (GCM tag mismatch) or malformed input.
 */
export async function decryptSecret(cipher: string, keyB64: string): Promise<string> {
  const key = await importAesKey(keyB64)
  const combined = b64ToBytes(cipher)
  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 workerSubtle().decrypt(
    { name: 'AES-GCM', iv: asBufferSource(iv) },
    key,
    asBufferSource(ct),
  )
  return new TextDecoder().decode(pt)
}

// ── Adapter-credential AES-256-GCM helpers ────────────────────────────────
// Used by system-communications-notifications to store per-tenant bot tokens
// (Telegram, Slack, Gmail, etc.) with separate IV + auth-tag columns so GCM
// round-trips cleanly without concatenation.

const CRED_GCM_IV_BYTES = 12
const CRED_GCM_TAG_BYTES = 16 // GCM tag is always 128 bits

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

/**
 * Encrypt `plaintext` with AES-256-GCM under `keyB64` (base64 32-byte key).
 * Returns three separate base64 strings: ciphertext (without IV/tag), iv, authTag.
 * A fresh random 12-byte IV is generated per call.
 */
export async function encryptCredential(
  plaintext: string,
  keyB64: string,
): Promise<{ ciphertext: string; iv: string; authTag: string }> {
  const key = await importAesCredKey(keyB64)
  const ivBytes = crypto.getRandomValues(new Uint8Array(CRED_GCM_IV_BYTES))
  const ctWithTag = await workerSubtle().encrypt(
    { name: 'AES-GCM', iv: asBufferSource(ivBytes), tagLength: CRED_GCM_TAG_BYTES * 8 },
    key,
    new TextEncoder().encode(plaintext),
  )
  const ctWithTagBytes = new Uint8Array(ctWithTag)
  // GCM ciphertext = ctWithTag[0 .. len-16]; auth tag = ctWithTag[len-16 ..]
  const ctLen = ctWithTagBytes.byteLength - CRED_GCM_TAG_BYTES
  const cipherBytes = ctWithTagBytes.subarray(0, ctLen)
  const tagBytes = ctWithTagBytes.subarray(ctLen)

  return {
    ciphertext: bytesToB64(cipherBytes),
    iv: bytesToB64(ivBytes),
    authTag: bytesToB64(tagBytes),
  }
}

/**
 * Decrypt a credential blob produced by `encryptCredential`.
 * Throws on tamper / wrong key (GCM tag mismatch) or malformed input.
 */
export async function decryptCredential(
  blob: { ciphertext: string; iv: string; authTag: string },
  keyB64: string,
): Promise<string> {
  const key = await importAesCredKey(keyB64)
  const cipherBytes = b64ToBytes(blob.ciphertext)
  const ivBytes = b64ToBytes(blob.iv)
  const tagBytes = b64ToBytes(blob.authTag)
  // Re-assemble ciphertext+tag for WebCrypto
  const ctWithTag = new Uint8Array(cipherBytes.byteLength + tagBytes.byteLength)
  ctWithTag.set(cipherBytes, 0)
  ctWithTag.set(tagBytes, cipherBytes.byteLength)
  const pt = await workerSubtle().decrypt(
    { name: 'AES-GCM', iv: asBufferSource(ivBytes), tagLength: CRED_GCM_TAG_BYTES * 8 },
    key,
    asBufferSource(ctWithTag),
  )
  return new TextDecoder().decode(pt)
}
