import { createPublicKey, verify as verifySignature } from "node:crypto";

/**
 * "Sign in with Apple" identity-token verification for the native iOS app.
 *
 * Unlike Google (an authorization-code + browser-redirect flow, see
 * `google.ts`), the native `AuthenticationServices` flow hands the app a
 * pre-signed JWT ("identity token") directly on-device — there is no
 * authorization code to exchange server-side. All the server has to do is
 * verify that JWT was really signed by Apple and says what the client
 * claims it says. Hand-rolled with `node:crypto` only (no `jsonwebtoken`/
 * `jose` dependency) — RS256 verification against a JWK is a handful of
 * lines with `crypto.createPublicKey({ format: "jwk" })`, which has been
 * supported since Node 15.12, so pulling in a library for it would just be
 * another supply-chain dependency for something the runtime already does.
 */

const APPLE_KEYS_ENDPOINT = "https://appleid.apple.com/auth/keys";
const APPLE_ISSUER = "https://appleid.apple.com";
/** Apple rotates signing keys rarely; an hour keeps this well within that. */
const KEYS_CACHE_TTL_MS = 60 * 60 * 1000;

interface AppleJWK {
  kty: string;
  kid: string;
  use: string;
  alg: string;
  n: string;
  e: string;
}

let cachedKeys: { keys: AppleJWK[]; fetchedAt: number } | null = null;

async function fetchApplePublicKeys(): Promise<AppleJWK[]> {
  if (cachedKeys && Date.now() - cachedKeys.fetchedAt < KEYS_CACHE_TTL_MS) {
    return cachedKeys.keys;
  }
  const response = await fetch(APPLE_KEYS_ENDPOINT);
  if (!response.ok) {
    throw new Error(`Apple JWKS fetch failed (${response.status})`);
  }
  const body = (await response.json()) as { keys: AppleJWK[] };
  cachedKeys = { keys: body.keys, fetchedAt: Date.now() };
  return body.keys;
}

function base64UrlDecode(input: string): Buffer {
  return Buffer.from(input, "base64url");
}

/** Apple sends some boolean claims as either a real boolean or a string. */
function parseAppleBool(value: unknown): boolean {
  return value === true || value === "true";
}

export interface AppleIdentity {
  /** Stable per-app Apple account identifier — the value stored, not the email. */
  sub: string;
  /** Null only in the (very rare) case Apple omits the claim entirely. */
  email: string | null;
  emailVerified: boolean;
  /** True when this is an Apple "Hide My Email" relay address. Informational only. */
  isPrivateRelayEmail: boolean;
}

export class AppleTokenError extends Error {}

/**
 * Verifies an identity token's signature and standard claims, and returns
 * the identity it vouches for. Throws `AppleTokenError` (safe to show a
 * generic "sign-in failed" for) on anything wrong — malformed token, bad
 * signature, wrong issuer/audience, expired.
 *
 * `fetchKeys` is injectable purely so this can be unit-tested against a
 * fake JWKS/self-signed token without hitting Apple's real endpoint or
 * needing a physical device — production callers should never pass it.
 */
export async function verifyAppleIdentityToken(
  identityToken: string,
  expectedAudience: string,
  fetchKeys: () => Promise<AppleJWK[]> = fetchApplePublicKeys
): Promise<AppleIdentity> {
  const parts = identityToken.split(".");
  if (parts.length !== 3) throw new AppleTokenError("malformed identity token");
  const [headerB64, payloadB64, signatureB64] = parts;

  let header: { alg?: string; kid?: string };
  let payload: {
    iss?: string;
    aud?: string | string[];
    exp?: number;
    sub?: string;
    email?: string;
    email_verified?: boolean | string;
    is_private_email?: boolean | string;
  };
  try {
    header = JSON.parse(base64UrlDecode(headerB64).toString("utf8"));
    payload = JSON.parse(base64UrlDecode(payloadB64).toString("utf8"));
  } catch {
    throw new AppleTokenError("malformed identity token");
  }

  if (header.alg !== "RS256" || !header.kid) {
    throw new AppleTokenError("unexpected token header");
  }

  const keys = await fetchKeys();
  const jwk = keys.find((key) => key.kid === header.kid);
  if (!jwk) throw new AppleTokenError("no matching Apple signing key");

  const publicKey = createPublicKey({
    key: { kty: jwk.kty, n: jwk.n, e: jwk.e },
    format: "jwk",
  });

  const signedData = Buffer.from(`${headerB64}.${payloadB64}`);
  const signature = base64UrlDecode(signatureB64);
  const signatureValid = verifySignature("RSA-SHA256", signedData, publicKey, signature);
  if (!signatureValid) throw new AppleTokenError("invalid signature");

  if (payload.iss !== APPLE_ISSUER) throw new AppleTokenError("unexpected issuer");

  const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
  if (!audiences.includes(expectedAudience)) throw new AppleTokenError("unexpected audience");

  if (!payload.exp || payload.exp * 1000 < Date.now()) {
    throw new AppleTokenError("expired identity token");
  }

  if (!payload.sub) throw new AppleTokenError("missing sub claim");

  return {
    sub: payload.sub,
    email: payload.email ?? null,
    emailVerified: parseAppleBool(payload.email_verified),
    isPrivateRelayEmail: parseAppleBool(payload.is_private_email),
  };
}
