/**
 * Shared Apple certificate-chain and JWS verification.
 *
 * Three Apple credentials flow through this Worker, and all of them are signed
 * the same way: a StoreKit 2 transaction, an App Store Server Notifications V2
 * payload, and an App Attest attestation each carry an x5c certificate chain.
 * This file owns the chain walk so every caller gets the same checks: every
 * certificate inside its validity window, every link signed by the next, and
 * the trust anchor pinned rather than assumed.
 *
 * Two anchoring modes, because Apple ships two chain shapes. StoreKit and the
 * notifications payload include the root in the chain, so pinning its SHA-256
 * is enough. The App Attest attestation omits the root, so the verifier needs
 * the root certificate itself to check the last link's signature.
 *
 * The crypto dependencies are imported lazily so the pure logic elsewhere
 * stays unit-testable with zero installs. Production dependencies: `jose`,
 * `@peculiar/x509`.
 */
import type { X509Certificate } from "@peculiar/x509";

/** Apple chains are leaf, intermediate, root. Refuse anything outlandish. */
const MAX_CHAIN_LENGTH = 5;

type X509Module = typeof import("@peculiar/x509");

async function chainCerts(
  x509: X509Module,
  x5c: Array<string | Uint8Array>,
): Promise<X509Certificate[]> {
  if (x5c.length < 2 || x5c.length > MAX_CHAIN_LENGTH) {
    throw new Error("unexpected certificate chain length");
  }
  const certs = x5c.map((der) => new x509.X509Certificate(der));
  const now = new Date();
  for (const cert of certs) {
    if (now < cert.notBefore || now > cert.notAfter) {
      throw new Error("certificate outside its validity window");
    }
  }
  // Verify each link in the chain (leaf first, root last).
  for (let i = 0; i < certs.length - 1; i++) {
    const issuerKey = await certs[i + 1].publicKey.export();
    if (!(await certs[i].verify({ publicKey: issuerKey }))) {
      throw new Error("broken certificate chain");
    }
  }
  return certs;
}

/**
 * Verify a chain that carries its own root, and pin that root by SHA-256.
 * Used for StoreKit 2 transactions and server notifications, whose x5c ends
 * at Apple Root CA - G3.
 */
export async function verifyChainAndPin(
  x5c: Array<string | Uint8Array>,
  pinnedSha256: string,
): Promise<X509Certificate[]> {
  const x509 = await import("@peculiar/x509");
  const certs = await chainCerts(x509, x5c);
  const root = certs[certs.length - 1];
  const rootSha256 = toHex(await crypto.subtle.digest("SHA-256", root.rawData));
  if (rootSha256 !== normalizeHex(pinnedSha256)) {
    throw new Error("untrusted root certificate");
  }
  return certs;
}

/**
 * Verify a chain that omits its root, anchoring the last certificate to a
 * configured trust anchor. Used for App Attest attestations, whose x5c stops
 * at the intermediate; `anchor` is the Apple App Attestation Root CA
 * certificate (PEM or base64 DER).
 */
export async function verifyChainToAnchor(
  x5c: Array<string | Uint8Array>,
  anchor: string,
): Promise<X509Certificate[]> {
  const x509 = await import("@peculiar/x509");
  const certs = await chainCerts(x509, x5c);
  const root = new x509.X509Certificate(anchor);
  const rootKey = await root.publicKey.export();
  const last = certs[certs.length - 1];
  if (!(await last.verify({ publicKey: rootKey }))) {
    throw new Error("chain does not anchor to the pinned root");
  }
  return certs;
}

/**
 * Verify an Apple JWS end to end: walk and pin the x5c chain, then verify the
 * signature with the leaf key, accepting only ES256. Returns the payload.
 */
export async function verifyAppleJWS(
  jws: string,
  pinnedRootSha256: string,
): Promise<unknown> {
  const { decodeProtectedHeader, importX509, jwtVerify } = await import("jose");
  const header = decodeProtectedHeader(jws);
  if (!Array.isArray(header.x5c)) {
    throw new Error("JWS is missing an x5c certificate chain");
  }
  const certs = await verifyChainAndPin(header.x5c, pinnedRootSha256);
  const leafKey = await importX509(certs[0].toString("pem"), "ES256");
  const { payload } = await jwtVerify(jws, leafKey, { algorithms: ["ES256"] });
  return payload;
}

export function toHex(buf: ArrayBuffer): string {
  return [...new Uint8Array(buf)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

export function normalizeHex(s: string): string {
  return s.toLowerCase().replace(/[^0-9a-f]/g, "");
}
