/**
 * Native VAPID / Web Push sender
 *
 * Implements the Web Push Protocol using only Node.js built-in modules — no
 * third-party web-push package needed.
 *
 * Signing:    VAPID JWT using ES256 (ECDSA P-256 / SHA-256) — RFC 8292.
 * Encryption: Message Encryption for Web Push — RFC 8291 keying + RFC 8188
 *             "aes128gcm" content encoding (the modern, single-record scheme
 *             that current Chrome/Firefox/Edge and FCM implement).
 *
 * Server-only — never import from client components.
 */

import crypto from "node:crypto";

// ── Types ────────────────────────────────────────────────────────────────────

export interface PushKeys {
  p256dh: string; // base64url ECDH public key (65-byte uncompressed point)
  auth: string; // base64url 16-byte auth secret
}

export interface PushPayload {
  title: string;
  body: string;
  url: string;
  icon?: string;
}

export interface SendPushResult {
  endpoint: string;
  status: number;
  /** true if the subscription is expired and should be deleted */
  gone: boolean;
}

// ── base64url helpers ─────────────────────────────────────────────────────────

function b64urlDecode(str: string): Buffer {
  const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
  return Buffer.from(padded, "base64");
}

function b64urlEncode(buf: Buffer | Uint8Array): string {
  return Buffer.from(buf)
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

// ── HKDF (SHA-256) ────────────────────────────────────────────────────────────
// Single-block HKDF (Extract + one Expand block). Valid for length <= 32, which
// covers every derivation below (IKM=32, CEK=16, nonce=12).
function hkdf(salt: Buffer, ikm: Buffer, info: Buffer, length: number): Buffer {
  const prk = crypto.createHmac("sha256", salt).update(ikm).digest();
  const output = crypto
    .createHmac("sha256", prk)
    .update(Buffer.concat([info, Buffer.from([0x01])]))
    .digest();
  return output.subarray(0, length);
}

// ── VAPID JWT (RFC 8292) ──────────────────────────────────────────────────────

function buildVapidJwt(audience: string, subject: string, privateKeyDer: Buffer): string {
  const now = Math.floor(Date.now() / 1000);
  const exp = now + 12 * 3600; // 12-hour expiry

  const header = b64urlEncode(Buffer.from(JSON.stringify({ typ: "JWT", alg: "ES256" })));
  const payload = b64urlEncode(
    Buffer.from(JSON.stringify({ aud: audience, exp, sub: subject }))
  );
  const signingInput = `${header}.${payload}`;

  const privateKey = crypto.createPrivateKey({
    key: privateKeyDer,
    format: "der",
    type: "pkcs8",
  });

  // ES256 JWT signatures MUST be the raw r||s pair (64 bytes), NOT DER.
  // dsaEncoding "ieee-p1363" makes Node emit raw r||s directly — no manual
  // DER parsing (which is easy to get wrong for 33-byte, high-bit-set integers).
  const signature = crypto.sign("sha256", Buffer.from(signingInput), {
    key: privateKey,
    dsaEncoding: "ieee-p1363",
  });

  return `${signingInput}.${b64urlEncode(signature)}`;
}

// ── Payload encryption: RFC 8291 keying + RFC 8188 aes128gcm ───────────────────

/**
 * Encrypt a Web Push message body per RFC 8188 ("aes128gcm") with RFC 8291 key
 * derivation. Returns the complete request body:
 *
 *   salt(16) | rs(4, BE) | idlen(1) | keyid=as_public(65) | ciphertext(+tag)
 */
function encryptPayload(
  clientPublicKeyB64url: string,
  authSecretB64url: string,
  plaintext: Buffer
): Buffer {
  const uaPublic = b64urlDecode(clientPublicKeyB64url); // 65-byte uncompressed point
  const authSecret = b64urlDecode(authSecretB64url); // 16-byte auth secret

  // Ephemeral server ("application server") EC key pair. createECDH yields raw
  // uncompressed keys directly (0x04||X||Y), avoiding SPKI/DER offset pitfalls.
  const ecdh = crypto.createECDH("prime256v1");
  const asPublic = ecdh.generateKeys(); // 65-byte uncompressed point
  const ecdhSecret = ecdh.computeSecret(uaPublic); // 32-byte shared secret

  const salt = crypto.randomBytes(16);

  // RFC 8291 §3.4: IKM = HKDF(auth_secret, ecdh_secret, key_info, 32)
  //   key_info = "WebPush: info" || 0x00 || ua_public || as_public
  const keyInfo = Buffer.concat([
    Buffer.from("WebPush: info\0", "utf8"),
    uaPublic,
    asPublic,
  ]);
  const ikm = hkdf(authSecret, ecdhSecret, keyInfo, 32);

  // RFC 8188 §2.2: derive CEK and nonce from the (random) salt and IKM.
  const cek = hkdf(salt, ikm, Buffer.from("Content-Encoding: aes128gcm\0", "utf8"), 16);
  const nonce = hkdf(salt, ikm, Buffer.from("Content-Encoding: nonce\0", "utf8"), 12);

  // Single record: plaintext followed by the 0x02 last-record delimiter.
  const record = Buffer.concat([plaintext, Buffer.from([0x02])]);
  const cipher = crypto.createCipheriv("aes-128-gcm", cek, nonce);
  const encrypted = Buffer.concat([cipher.update(record), cipher.final()]);
  const authTag = cipher.getAuthTag();
  const ciphertext = Buffer.concat([encrypted, authTag]);

  // RFC 8188 §2.1 header.
  const rs = Buffer.alloc(4);
  rs.writeUInt32BE(4096, 0);
  const idlen = Buffer.from([asPublic.length]); // 65
  const header = Buffer.concat([salt, rs, idlen, asPublic]);

  return Buffer.concat([header, ciphertext]);
}

// ── PKCS#8 DER reconstruction for the raw 32-byte VAPID private scalar ─────────

function buildPkcs8FromRaw(rawPrivate: Buffer, rawPublic: Buffer): Buffer {
  const pkcs8Header = Buffer.from([
    0x30, 0x81, 0x87, // SEQUENCE
    0x02, 0x01, 0x00, // version: 0
    0x30, 0x13, // SEQUENCE (AlgorithmIdentifier)
    0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID ecPublicKey
    0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID prime256v1
    0x04, 0x6d, // OCTET STRING (ECPrivateKey follows)
    0x30, 0x6b, // SEQUENCE (ECPrivateKey)
    0x02, 0x01, 0x01, // version: 1
    0x04, 0x20, // OCTET STRING (private key, 32 bytes)
  ]);
  const publicKeySection = Buffer.concat([
    Buffer.from([0xa1, 0x44, 0x03, 0x42, 0x00]), // [1] EXPLICIT BIT STRING (66 bytes)
    rawPublic,
  ]);
  return Buffer.concat([pkcs8Header, rawPrivate, publicKeySection]);
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Send a single Web Push notification to one subscription endpoint.
 * Returns the HTTP status and whether the subscription is gone (expired).
 */
export async function sendWebPushNotification(
  endpoint: string,
  keys: PushKeys,
  payload: PushPayload
): Promise<SendPushResult> {
  const vapidPublicKeyB64 = process.env.VAPID_PUBLIC_KEY;
  const vapidPrivateKeyB64 = process.env.VAPID_PRIVATE_KEY;
  const vapidSubject = process.env.VAPID_SUBJECT || "mailto:admin@qutoai.com";

  if (!vapidPublicKeyB64 || !vapidPrivateKeyB64) {
    throw new Error("VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY must be set in environment.");
  }

  const rawPrivate = b64urlDecode(vapidPrivateKeyB64); // 32-byte scalar
  const rawPublic = b64urlDecode(vapidPublicKeyB64); // 65-byte uncompressed point
  const privateKeyDer = buildPkcs8FromRaw(rawPrivate, rawPublic);

  // VAPID audience is the ORIGIN of the push endpoint.
  const endpointUrl = new URL(endpoint);
  const audience = `${endpointUrl.protocol}//${endpointUrl.host}`;
  const jwt = buildVapidJwt(audience, vapidSubject, privateKeyDer);

  // Encrypt the payload (aes128gcm — self-describing body).
  const plaintextBuffer = Buffer.from(JSON.stringify(payload));
  const body = encryptPayload(keys.p256dh, keys.auth, plaintextBuffer);

  let status = 0;
  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        // RFC 8292 §3: VAPID auth carries the JWT (t=) and the app-server key (k=).
        Authorization: `vapid t=${jwt},k=${vapidPublicKeyB64}`,
        "Content-Type": "application/octet-stream",
        "Content-Encoding": "aes128gcm",
        TTL: "86400", // 24 hours
      },
      body: new Uint8Array(body) as unknown as BodyInit,
    });
    status = response.status;
  } catch {
    status = 0; // network error
  }

  return {
    endpoint,
    status,
    gone: status === 410 || status === 404,
  };
}
