import { base64url } from '@platform-modules/util/crypto'
import type { ChannelAdapter, ChannelResult, RenderedMessage } from './index.js'

const RECORD_SIZE = 4096

export type WebPushSubscription = {
  endpoint: string
  keys: { p256dh: string; auth: string }
}

export type WebPushVapidKeys = {
  subject: string
  publicKey: string
  privateKey: string
}

export type EncryptKeyMaterial = {
  payload: string | Uint8Array
  clientP256dh: string
  clientAuth: string
  salt: Uint8Array
  asKeyPair: CryptoKeyPair
}

export type WebPushEncryptResult = {
  body: Uint8Array
  salt: Uint8Array
  serverPublicKey: Uint8Array
}

export type WebPushCryptoIntermediates = {
  ecdhSecret: Uint8Array
  prkKey: Uint8Array
  ikm: Uint8Array
  prk: Uint8Array
  cek: Uint8Array
  nonce: Uint8Array
}

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

function concat(...parts: Uint8Array[]): Uint8Array {
  const total = parts.reduce((n, p) => n + p.length, 0)
  const out = new Uint8Array(total)
  let offset = 0
  for (const part of parts) {
    out.set(part, offset)
    offset += part.length
  }
  return out
}

function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
}

async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    toArrayBuffer(key),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sig = await crypto.subtle.sign('HMAC', cryptoKey, toArrayBuffer(data))
  return new Uint8Array(sig)
}

async function hkdfExpand(prk: Uint8Array, info: Uint8Array, length: number): Promise<Uint8Array> {
  const t1 = await hmacSha256(prk, concat(info, new Uint8Array([1])))
  return t1.slice(0, length)
}

function payloadBytes(payload: string | Uint8Array): Uint8Array {
  return typeof payload === 'string' ? new TextEncoder().encode(payload) : payload
}

export async function importUncompressedP256PublicKey(b64: string): Promise<CryptoKey> {
  const raw = base64urlDecode(b64.replace(/\s+/g, ''))
  return crypto.subtle.importKey(
    'raw',
    toArrayBuffer(raw),
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    [],
  )
}

async function vapidSigningKey(vapid: WebPushVapidKeys): Promise<CryptoKey> {
  const d = base64urlDecode(vapid.privateKey)
  const pub = base64urlDecode(vapid.publicKey.replace(/\s+/g, ''))
  const jwk: JsonWebKey = {
    kty: 'EC',
    crv: 'P-256',
    d: base64url(d),
    x: base64url(pub.slice(1, 33)),
    y: base64url(pub.slice(33, 65)),
  }
  return crypto.subtle.importKey(
    'jwk',
    jwk,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false,
    ['sign'],
  )
}

async function vapidVerifyKey(vapidPublicKey: string): Promise<CryptoKey> {
  const pub = base64urlDecode(vapidPublicKey.replace(/\s+/g, ''))
  const jwk: JsonWebKey = {
    kty: 'EC',
    crv: 'P-256',
    x: base64url(pub.slice(1, 33)),
    y: base64url(pub.slice(33, 65)),
  }
  return crypto.subtle.importKey(
    'jwk',
    jwk,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false,
    ['verify'],
  )
}

export async function deriveEcdhSecret(
  privateKey: CryptoKey,
  peerPublicKey: CryptoKey,
): Promise<Uint8Array> {
  const bits = await crypto.subtle.deriveBits(
    { name: 'ECDH', public: peerPublicKey },
    privateKey,
    256,
  )
  return new Uint8Array(bits)
}

export async function computeWebPushIntermediates(opts: {
  ecdhSecret: Uint8Array
  authSecret: Uint8Array
  salt: Uint8Array
  uaPublic: Uint8Array
  asPublic: Uint8Array
}): Promise<WebPushCryptoIntermediates> {
  const keyInfo = concat(
    new TextEncoder().encode('WebPush: info'),
    new Uint8Array([0]),
    opts.uaPublic,
    opts.asPublic,
  )
  const prkKey = await hmacSha256(opts.authSecret, opts.ecdhSecret)
  const ikm = await hkdfExpand(prkKey, keyInfo, 32)
  const prk = await hmacSha256(opts.salt, ikm)
  const cekInfo = concat(new TextEncoder().encode('Content-Encoding: aes128gcm'), new Uint8Array([0]))
  const nonceInfo = concat(new TextEncoder().encode('Content-Encoding: nonce'), new Uint8Array([0]))
  const cek = await hkdfExpand(prk, cekInfo, 16)
  const nonce = await hkdfExpand(prk, nonceInfo, 12)
  return { ecdhSecret: opts.ecdhSecret, prkKey, ikm, prk, cek, nonce }
}

function buildAes128GcmHeader(salt: Uint8Array, serverPublicKey: Uint8Array): Uint8Array {
  const rs = new Uint8Array(4)
  new DataView(rs.buffer).setUint32(0, RECORD_SIZE, false)
  return concat(salt, rs, new Uint8Array([serverPublicKey.length]), serverPublicKey)
}

/**
 * Deterministic RFC 8291 aes128gcm encryption core — inject fixed salt + AS key pair for KAT.
 */
export async function encryptWebPushPayloadWithKeyMaterial(
  opts: EncryptKeyMaterial,
): Promise<WebPushEncryptResult> {
  const clientPublic = await importUncompressedP256PublicKey(opts.clientP256dh)
  const authSecret = base64urlDecode(opts.clientAuth)
  const uaPublic = base64urlDecode(opts.clientP256dh.replace(/\s+/g, ''))
  const asPublicRaw = await crypto.subtle.exportKey('raw', opts.asKeyPair.publicKey!)
  const asPublic = new Uint8Array(asPublicRaw)

  const ecdhSecret = await deriveEcdhSecret(opts.asKeyPair.privateKey!, clientPublic)
  const { cek, nonce } = await computeWebPushIntermediates({
    ecdhSecret,
    authSecret,
    salt: opts.salt,
    uaPublic,
    asPublic,
  })

  const plain = payloadBytes(opts.payload)
  const padded = concat(plain, new Uint8Array([2]))

  const aesKey = await crypto.subtle.importKey('raw', toArrayBuffer(cek), 'AES-GCM', false, [
    'encrypt',
  ])
  const ciphertext = new Uint8Array(
    await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv: toArrayBuffer(nonce) },
      aesKey,
      toArrayBuffer(padded),
    ),
  )

  const header = buildAes128GcmHeader(opts.salt, asPublic)
  return {
    body: concat(header, ciphertext),
    salt: opts.salt,
    serverPublicKey: asPublic,
  }
}

/** Production path — random salt + ephemeral AS ECDH key pair. */
export async function encryptWebPushPayload(opts: {
  payload: string | Uint8Array
  clientP256dh: string
  clientAuth: string
}): Promise<WebPushEncryptResult> {
  const salt = crypto.getRandomValues(new Uint8Array(16))
  const asKeyPair = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveBits'],
  )
  return encryptWebPushPayloadWithKeyMaterial({ ...opts, salt, asKeyPair })
}

export async function createVapidJwt(
  vapid: WebPushVapidKeys,
  audience: string,
  expSeconds: number,
): Promise<string> {
  const header = base64url(new TextEncoder().encode(JSON.stringify({ typ: 'JWT', alg: 'ES256' })))
  const payload = base64url(
    new TextEncoder().encode(
      JSON.stringify({ aud: audience, exp: expSeconds, sub: vapid.subject }),
    ),
  )
  const signingInput = `${header}.${payload}`
  const signingKey = await vapidSigningKey(vapid)

  const sig = new Uint8Array(
    await crypto.subtle.sign(
      { name: 'ECDSA', hash: 'SHA-256' },
      signingKey,
      new TextEncoder().encode(signingInput),
    ),
  )
  return `${signingInput}.${base64url(sig)}`
}

function rawUncompressedToJwk(raw: Uint8Array, includePrivate: boolean, d?: Uint8Array): JsonWebKey {
  const x = raw.slice(1, 33)
  const y = raw.slice(33, 65)
  const jwk: JsonWebKey = {
    kty: 'EC',
    crv: 'P-256',
    x: base64url(x),
    y: base64url(y),
  }
  if (includePrivate && d) jwk.d = base64url(d)
  return jwk
}

/** Build RFC 8291 AS key pair from fixed scalar + uncompressed public (KAT / tests). */
export async function importRfc8291AsKeyPair(
  privateScalarB64: string,
  publicUncompressedB64: string,
): Promise<CryptoKeyPair> {
  const d = base64urlDecode(privateScalarB64)
  const pub = base64urlDecode(publicUncompressedB64.replace(/\s+/g, ''))
  const jwk = rawUncompressedToJwk(pub, true, d)
  const privateKey = await crypto.subtle.importKey(
    'jwk',
    jwk,
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveBits'],
  )
  const publicKey = await crypto.subtle.importKey(
    'raw',
    toArrayBuffer(pub),
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    [],
  )
  return { privateKey, publicKey }
}

export type WebPushChannelOpts = {
  vapid: WebPushVapidKeys
  ttl?: number
  fetch?: typeof fetch
}

export function createWebPushChannel(opts: WebPushChannelOpts): ChannelAdapter {
  const http = opts.fetch ?? fetch
  const ttl = opts.ttl ?? 2419200

  return {
    channel: 'webpush',
    async send(rendered: RenderedMessage, recipient): Promise<ChannelResult> {
      const sub = recipient as WebPushSubscription
      try {
        const encrypted = await encryptWebPushPayload({
          payload: rendered.body,
          clientP256dh: sub.keys.p256dh,
          clientAuth: sub.keys.auth,
        })

        const aud = new URL(sub.endpoint).origin
        const exp = Math.floor(Date.now() / 1000) + 12 * 60 * 60
        const jwt = await createVapidJwt(opts.vapid, aud, exp)
        const vapidPublic = opts.vapid.publicKey.replace(/\s+/g, '')

        const res = await http(sub.endpoint, {
          method: 'POST',
          headers: {
            Authorization: `vapid t=${jwt}, k=${vapidPublic}`,
            'Content-Encoding': 'aes128gcm',
            TTL: String(ttl),
            'Content-Type': 'application/octet-stream',
          },
          body: toArrayBuffer(encrypted.body),
        })

        if (res.status === 410 || res.status === 404) {
          return {
            ok: false,
            error: {
              message: 'push subscription expired',
              code: 'expired_subscription',
              retryable: false,
            },
          }
        }

        if (!res.ok) {
          return {
            ok: false,
            error: {
              message: `push endpoint returned ${res.status}`,
              retryable: res.status >= 500,
            },
          }
        }

        return { ok: true }
      } catch (err) {
        return {
          ok: false,
          error: {
            message: err instanceof Error ? err.message : String(err),
            retryable: true,
          },
        }
      }
    },
  }
}

/** Verify a VAPID JWT offline (ES256). */
export async function verifyVapidJwt(
  jwt: string,
  vapidPublicKey: string,
  expectedAud: string,
): Promise<boolean> {
  const [headerB64, payloadB64, sigB64] = jwt.split('.')
  if (!headerB64 || !payloadB64 || !sigB64) return false

  const payload = JSON.parse(
    new TextDecoder().decode(base64urlDecode(payloadB64)),
  ) as { aud?: string; exp?: number; sub?: string }
  if (payload.aud !== expectedAud) return false
  if (typeof payload.exp !== 'number' || payload.exp < Math.floor(Date.now() / 1000)) {
    return false
  }

  const verifyKey = await vapidVerifyKey(vapidPublicKey)

  return crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    verifyKey,
    toArrayBuffer(base64urlDecode(sigB64)),
    new TextEncoder().encode(`${headerB64}.${payloadB64}`),
  )
}
