// Reference aes128gcm (RFC 8291 + RFC 8188) encryptor using ONLY Deno WebCrypto.
// Goal: reproduce the RFC 8291 §5 ciphertext byte-for-byte.
function b64d(s: string): Uint8Array {
  s = s.replace(/-/g, "+").replace(/_/g, "/");
  s += "=".repeat((4 - (s.length % 4)) % 4);
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
}
function b64u(b: Uint8Array): string {
  return btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function cat(...arrs: Uint8Array[]): Uint8Array {
  const out = new Uint8Array(arrs.reduce((n, a) => n + a.length, 0));
  let o = 0; for (const a of arrs) { out.set(a, o); o += a.length; } return out;
}
const enc = new TextEncoder();

async function hkdf(salt: Uint8Array, ikm: Uint8Array, info: Uint8Array, len: number) {
  const k = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
  const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, k, len * 8);
  return new Uint8Array(bits);
}

async function importPriv(d: Uint8Array, pub65: Uint8Array) {
  return await crypto.subtle.importKey("jwk", {
    kty: "EC", crv: "P-256", ext: true,
    d: b64u(d), x: b64u(pub65.slice(1, 33)), y: b64u(pub65.slice(33, 65)),
  }, { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
}

async function encryptAes128gcm(opts: {
  plaintext: Uint8Array; uaPublic: Uint8Array; authSecret: Uint8Array;
  asPrivate: Uint8Array; asPublic: Uint8Array; salt: Uint8Array; rs?: number;
}) {
  const rs = opts.rs ?? 4096;
  const priv = await importPriv(opts.asPrivate, opts.asPublic);
  const peer = await crypto.subtle.importKey("raw", opts.uaPublic, { name: "ECDH", namedCurve: "P-256" }, false, []);
  const ecdhSecret = new Uint8Array(await crypto.subtle.deriveBits({ name: "ECDH", public: peer }, priv, 256));

  // RFC 8291 §3.4: key_info = "WebPush: info" || 0x00 || ua_public || as_public
  const keyInfo = cat(enc.encode("WebPush: info"), new Uint8Array([0]), opts.uaPublic, opts.asPublic);
  const ikm = await hkdf(opts.authSecret, ecdhSecret, keyInfo, 32);

  // RFC 8188 §2.2/2.3
  const cek = await hkdf(opts.salt, ikm, cat(enc.encode("Content-Encoding: aes128gcm"), new Uint8Array([0])), 16);
  const nonce = await hkdf(opts.salt, ikm, cat(enc.encode("Content-Encoding: nonce"), new Uint8Array([0])), 12);

  const aesKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
  // single record, last-record delimiter 0x02
  const record = cat(opts.plaintext, new Uint8Array([0x02]));
  const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, tagLength: 128 }, aesKey, record));

  const rsBuf = new Uint8Array(4); new DataView(rsBuf.buffer).setUint32(0, rs);
  const header = cat(opts.salt, rsBuf, new Uint8Array([opts.asPublic.length]), opts.asPublic);
  return { body: cat(header, ct), ikm, cek, nonce, ecdhSecret };
}

export { encryptAes128gcm, b64d, b64u };
