/**
 * App Attest one-time registration: verify the attestation object and store
 * the hardware-backed public key.
 *
 * The app asks the Worker for a challenge, calls
 * DCAppAttestService.attestKey over its SHA-256, and POSTs the result here.
 * The handler consumes the challenge exactly once (challenge.ts), then the
 * verifier follows Apple's documented procedure: check the certificate chain
 * to the Apple App Attestation Root CA, the nonce Apple embeds in the
 * credential certificate, the key identifier, and the authenticator data.
 * Only then does the public key land in the key record (attestkey.ts), where
 * appattest.ts looks it up on every request.
 */
import type { AppAttestConfig } from "./appattest.ts";
import type { AttestKeyStore } from "./attestkey.ts";
import type { ChallengeStore } from "./challenge.ts";
import { verifyChainToAnchor } from "./applejws.ts";
import {
  base64ToBytes,
  bytesEqual,
  bytesToBase64,
  concatBytes,
} from "./bytes.ts";

export interface AttestationInput {
  keyId: string;
  /** Base64 CBOR attestation object from DCAppAttestService.attestKey. */
  attestation: string;
  /** SHA-256 of the challenge the key was attested over. */
  clientDataHash: ArrayBuffer;
}

export type AttestationConfig = AppAttestConfig & {
  /** The Apple App Attestation Root CA certificate (PEM or base64 DER). */
  attestRootCa: string;
};

/** Resolves with the verified public key (SPKI base64); throws otherwise. */
export type AttestationVerifier = (
  input: AttestationInput,
  cfg: AttestationConfig,
) => Promise<{ spkiBase64: string }>;

/** Apple's marker for the App Attest authenticator in authData. */
const AAGUIDS = ["appattest\0\0\0\0\0\0\0", "appattestdevelop"];
/** The credential-certificate extension Apple reserves for the nonce. */
const NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2";

export const verifyAttestation: AttestationVerifier = async (input, cfg) => {
  const cbor = await import("cbor-x");
  const decoded = cbor.decode(base64ToBytes(input.attestation)) as {
    fmt?: string;
    attStmt?: { x5c?: Uint8Array[] };
    authData?: Uint8Array;
  };
  if (decoded.fmt !== "apple-appattest") {
    throw new Error("not an App Attest attestation");
  }
  const x5c = decoded.attStmt?.x5c;
  const authData = decoded.authData;
  if (!Array.isArray(x5c) || !authData || authData.length < 55) {
    throw new Error("malformed attestation object");
  }

  // 1. The chain: credCert <- intermediate, anchored to the configured Apple
  //    App Attestation Root CA. The x5c omits the root, which is why the
  //    anchor is a certificate rather than a fingerprint.
  const certs = await verifyChainToAnchor(x5c, cfg.attestRootCa);
  const credCert = certs[0];

  // 2. The nonce: SHA-256(authData || clientDataHash) must appear in the
  //    credential certificate's reserved extension. This binds the
  //    certificate Apple issued to this exact attestation.
  const nonce = new Uint8Array(
    await crypto.subtle.digest(
      "SHA-256",
      concatBytes(authData, new Uint8Array(input.clientDataHash)),
    ),
  );
  const ext = credCert.getExtension(NONCE_EXTENSION_OID);
  if (!ext || !containsOctetString(new Uint8Array(ext.value), nonce)) {
    throw new Error("attestation nonce mismatch");
  }

  // 3. The key identifier: SHA-256 of the credential public key (the
  //    uncompressed P-256 point) must equal the keyId the app submitted.
  const spki = new Uint8Array(credCert.publicKey.rawData);
  const point = spki.slice(spki.length - 65);
  if (spki.length < 65 || point[0] !== 0x04) {
    throw new Error("unexpected public key format");
  }
  const keyIdHash = new Uint8Array(await crypto.subtle.digest("SHA-256", point));
  if (!bytesEqual(keyIdHash, base64ToBytes(input.keyId))) {
    throw new Error("key identifier does not match the attested key");
  }

  // 4. The authenticator data: bound to this App ID, counter at zero, the
  //    App Attest aaguid, and a credentialId equal to the key identifier.
  const appId = `${cfg.teamId}.${cfg.bundleId}`;
  const expectedRpId = new Uint8Array(
    await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appId)),
  );
  if (!bytesEqual(authData.slice(0, 32), expectedRpId)) {
    throw new Error("attestation rpId does not match this app");
  }
  const counter = new DataView(
    authData.buffer,
    authData.byteOffset + 33,
    4,
  ).getUint32(0);
  if (counter !== 0) {
    throw new Error("attestation counter must be zero");
  }
  const aaguid = new TextDecoder().decode(authData.slice(37, 53));
  if (!AAGUIDS.includes(aaguid)) {
    throw new Error("unexpected attestation aaguid");
  }
  const credIdLen = (authData[53] << 8) | authData[54];
  if (authData.length < 55 + credIdLen) {
    throw new Error("malformed attestation object");
  }
  const credentialId = authData.slice(55, 55 + credIdLen);
  if (!bytesEqual(credentialId, base64ToBytes(input.keyId))) {
    throw new Error("credentialId does not match the key identifier");
  }

  return { spkiBase64: bytesToBase64(spki) };
};

export async function handleRegistration(
  request: Request,
  cfg: AttestationConfig,
  verifier: AttestationVerifier,
  keys: AttestKeyStore,
  challenges: ChallengeStore,
): Promise<Response> {
  let body: { keyId?: unknown; attestation?: unknown; challenge?: unknown };
  try {
    body = (await request.json()) as typeof body;
  } catch {
    return new Response("malformed registration", { status: 400 });
  }
  const { keyId, attestation, challenge } = body;
  if (
    typeof keyId !== "string" ||
    typeof attestation !== "string" ||
    typeof challenge !== "string"
  ) {
    return new Response("malformed registration", { status: 400 });
  }

  // The challenge is consumed before verification, so one issued challenge
  // buys one attempt; a failed attempt starts over with a fresh challenge.
  if (!(await challenges.consume(challenge))) {
    return new Response("unknown or used challenge", { status: 401 });
  }

  const clientDataHash = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(challenge),
  );
  try {
    const { spkiBase64 } = await verifier(
      { keyId, attestation, clientDataHash },
      cfg,
    );
    await keys.putPublicKey(keyId, spkiBase64);
  } catch {
    return new Response("invalid attestation", { status: 401 });
  }
  return new Response(null, { status: 204 });
}

/**
 * Scan DER for an OCTET STRING holding exactly `needle`. Apple wraps the
 * nonce as a single tagged octet string inside a small sequence; scanning for
 * the tagged value avoids a full ASN.1 parser for one field.
 */
function containsOctetString(der: Uint8Array, needle: Uint8Array): boolean {
  for (let i = 0; i + 2 + needle.length <= der.length; i++) {
    if (
      der[i] === 0x04 &&
      der[i + 1] === needle.length &&
      bytesEqual(der.slice(i + 2, i + 2 + needle.length), needle)
    ) {
      return true;
    }
  }
  return false;
}
