/**
 * VAPID JWT signing + Web Push payload encryption — RFC 8292/8030.
 *
 * Implements:
 *  - RFC 8292: VAPID (Voluntary Application Server Identification) using ES256
 *    JWT over P-256 ECDSA. The JWT is used as the Authorization header.
 *  - RFC 8030 / draft-ietf-webpush-encryption-08: aes128gcm content encryption.
 *
 * All crypto via WebCrypto (Cloudflare Workers compatible).
 */

// ── VAPID JWT ────────────────────────────────────────────────────────────────

function base64urlEncode(bytes: Uint8Array): string {
  const binary = String.fromCharCode(...bytes)
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}

function base64urlDecode(b64url: string): Uint8Array {
  const padded = b64url.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice((b64url.length + 3) & 3)
  const binary = atob(padded)
  const bytes = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i)
  }
  return bytes
}

/**
 * Import a P-256 ECDSA private key from a base64url-encoded raw PKCS#8 or
 * JWK-formatted private key for ES256 signing.
 * Expects base64url-encoded PKCS#8 DER from `wrangler secret put`.
 */
async function importVapidPrivateKey(vapidPrivKeyB64: string): Promise<CryptoKey> {
  const keyBytes = base64urlDecode(vapidPrivKeyB64)
  return crypto.subtle.importKey(
    'pkcs8',
    keyBytes,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false,
    ['sign'],
  )
}

/**
 * Build and sign a VAPID JWT for the given push endpoint origin.
 * Returns the signed JWT string.
 */
export async function buildVapidJwt(opts: {
  audience: string // push endpoint origin (e.g. https://fcm.googleapis.com)
  subject: string // mailto: or https: contact
  vapidPrivKeyB64: string
  vapidPubKeyB64: string
  expiresInSeconds?: number
}): Promise<string> {
  const now = Math.floor(Date.now() / 1000)
  const exp = now + (opts.expiresInSeconds ?? 43200) // 12h default

  const header = { alg: 'ES256', typ: 'JWT' }
  const payload = {
    aud: opts.audience,
    sub: opts.subject,
    exp,
    iat: now,
  }

  const encodedHeader = base64urlEncode(new TextEncoder().encode(JSON.stringify(header)))
  const encodedPayload = base64urlEncode(new TextEncoder().encode(JSON.stringify(payload)))
  const signingInput = `${encodedHeader}.${encodedPayload}`

  const key = await importVapidPrivateKey(opts.vapidPrivKeyB64)
  const sigBuf = await crypto.subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' },
    key,
    new TextEncoder().encode(signingInput),
  )

  const sig = base64urlEncode(new Uint8Array(sigBuf))
  return `${signingInput}.${sig}`
}

// ── aes128gcm Content Encryption (RFC 8030 / draft-ietf-webpush-encryption-08) ──

const SALT_BYTES = 16
const KEY_INFO = new TextEncoder().encode('Content-Encoding: aes128gcm\0')
const NONCE_INFO = new TextEncoder().encode('Content-Encoding: nonce\0')

/**
 * Encrypt a Web Push notification payload using aes128gcm.
 *
 * `sub.p256dh` is the client's P-256 public key (base64url).
 * `sub.auth` is the client's auth secret (base64url, 16 bytes).
 *
 * Returns an ArrayBuffer ready to POST as the encrypted push body.
 */
export async function encryptWebPushPayload(opts: {
  payload: string
  clientP256dh: string // base64url
  clientAuth: string // base64url
}): Promise<ArrayBuffer> {
  const payloadBytes = new TextEncoder().encode(opts.payload)
  const clientKey = base64urlDecode(opts.clientP256dh)
  const clientAuth = base64urlDecode(opts.clientAuth)

  // Generate ephemeral ECDH key pair (server side)
  const serverKeyPair = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveBits'],
  ) as CryptoKeyPair

  // Import client public key
  const clientPublicKey = await crypto.subtle.importKey(
    'raw',
    clientKey,
    { name: 'ECDH', namedCurve: 'P-256' },
    false,
    [],
  )

  // ECDH shared secret
  const sharedBits = await crypto.subtle.deriveBits(
    { name: 'ECDH', $public: clientPublicKey },
    serverKeyPair.privateKey,
    256,
  )

  // Export server public key (uncompressed)
  const serverPublicKeyRaw = await crypto.subtle.exportKey('raw', serverKeyPair.publicKey) as ArrayBuffer

  // Random salt
  const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES))

  // HKDF to derive IKM, then content encryption key + nonce
  const ikm = await deriveIkm(new Uint8Array(sharedBits), clientAuth, new Uint8Array(serverPublicKeyRaw), clientKey)
  const contentKey = await hkdfExpand(ikm, salt, KEY_INFO, 16)
  const nonce = await hkdfExpand(ikm, salt, NONCE_INFO, 12)

  // Encrypt with AES-128-GCM
  const cryptoKey = await crypto.subtle.importKey('raw', contentKey, { name: 'AES-GCM' }, false, ['encrypt'])
  const encryptedContent = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: nonce, tagLength: 128 },
    cryptoKey,
    // Add padding delimiter byte (0x02) per RFC 8291
    (() => {
      const padded = new Uint8Array(payloadBytes.length + 1)
      padded.set(payloadBytes)
      padded[payloadBytes.length] = 0x02
      return padded
    })(),
  )

  // Build the aes128gcm content-encoding header + ciphertext
  // Header: salt(16) + rs(4) + idlen(1) + serverPublicKey(65)
  const serverPubKey = new Uint8Array(serverPublicKeyRaw)
  const headerLen = 16 + 4 + 1 + serverPubKey.byteLength
  const output = new Uint8Array(headerLen + encryptedContent.byteLength)
  let offset = 0
  output.set(salt, offset); offset += 16
  // rs = 4096 (default record size)
  const rs = 4096
  output[offset++] = (rs >> 24) & 0xff
  output[offset++] = (rs >> 16) & 0xff
  output[offset++] = (rs >> 8) & 0xff
  output[offset++] = rs & 0xff
  // idlen = server public key length
  output[offset++] = serverPubKey.byteLength
  output.set(serverPubKey, offset); offset += serverPubKey.byteLength
  output.set(new Uint8Array(encryptedContent), offset)

  return output.buffer
}

async function deriveIkm(
  sharedSecret: Uint8Array,
  clientAuth: Uint8Array,
  serverPublicKey: Uint8Array,
  clientPublicKey: Uint8Array,
): Promise<Uint8Array> {
  const info = new Uint8Array([
    ...new TextEncoder().encode('WebPush: info\0'),
    ...clientPublicKey,
    ...serverPublicKey,
  ])

  const hkdfKey = await crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false, ['deriveBits'])
  const prk = await crypto.subtle.deriveBits(
    { name: 'HKDF', hash: 'SHA-256', salt: clientAuth, info },
    hkdfKey,
    256,
  )
  return new Uint8Array(prk)
}

async function hkdfExpand(
  prk: Uint8Array,
  salt: Uint8Array,
  info: Uint8Array,
  length: number,
): Promise<Uint8Array> {
  const key = await crypto.subtle.importKey('raw', prk, 'HKDF', false, ['deriveBits'])
  const bits = await crypto.subtle.deriveBits(
    { name: 'HKDF', hash: 'SHA-256', salt, info },
    key,
    length * 8,
  )
  return new Uint8Array(bits)
}
