// APNS sender (docs/CLOUD_INFRA.md §3). Token-based auth: an ES256 JWT signed in-Worker with
// WebCrypto, posted over HTTP/2 to Apple. The push is **content-free** — the relay can't read the
// approval, so it carries none of it; the phone wakes and pulls the real approval over the
// encrypted channel.

import type { Env } from "./env";

export interface ApnsConfig {
  keyP8: string; // PKCS#8 PEM of the .p8 auth key
  keyId: string;
  teamId: string;
  topic: string; // xyz.blakeslee.nucleic.remote
  env: "sandbox" | "production";
}

/** Build a config from the Worker secrets, or null if APNS isn't provisioned. */
export function apnsConfig(env: Env): ApnsConfig | null {
  if (!env.APNS_KEY_P8 || !env.APNS_KEY_ID || !env.APNS_TEAM_ID || !env.APNS_TOPIC) return null;
  return {
    keyP8: env.APNS_KEY_P8,
    keyId: env.APNS_KEY_ID,
    teamId: env.APNS_TEAM_ID,
    topic: env.APNS_TOPIC,
    env: env.APNS_ENV === "production" ? "production" : "sandbox",
  };
}

const enc = new TextEncoder();

function b64url(bytes: Uint8Array): string {
  let s = "";
  for (const b of bytes) s += String.fromCharCode(b);
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

function pemToDer(pem: string): Uint8Array {
  const body = pem
    .replace(/-----BEGIN [^-]+-----/, "")
    .replace(/-----END [^-]+-----/, "")
    .replace(/\s+/g, "");
  const bin = atob(body);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

export interface JwtCache {
  token?: string;
  iat?: number;
}
const moduleCache: JwtCache = {};

/** Signed APNS provider JWT. Cached ~40 min (Apple requires refresh < 60 min). */
export async function apnsJwt(cfg: ApnsConfig, now = Date.now(), cache: JwtCache = moduleCache): Promise<string> {
  const iat = Math.floor(now / 1000);
  if (cache.token && cache.iat && iat - cache.iat < 40 * 60) return cache.token;

  const key = await crypto.subtle.importKey(
    "pkcs8",
    pemToDer(cfg.keyP8),
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["sign"],
  );
  const header = b64url(enc.encode(JSON.stringify({ alg: "ES256", kid: cfg.keyId })));
  const claims = b64url(enc.encode(JSON.stringify({ iss: cfg.teamId, iat })));
  const input = `${header}.${claims}`;
  // WebCrypto returns the raw r||s signature (JOSE/JWS format) — exactly what APNS wants.
  const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, enc.encode(input));
  const token = `${input}.${b64url(new Uint8Array(sig))}`;
  cache.token = token;
  cache.iat = iat;
  return token;
}

/** A wake "tickle": carries no code, transcript, or approval detail. */
export function approvalPayload(): Record<string, unknown> {
  return {
    aps: {
      alert: { "loc-key": "approval.pending" },
      "interruption-level": "time-sensitive",
      sound: "default",
    },
    nudge: 1,
  };
}

export interface ApnsResult {
  status: number;
  id?: string;
  reason?: string;
}

export async function sendApns(
  cfg: ApnsConfig,
  deviceToken: string,
  payload: Record<string, unknown>,
  now = Date.now(),
  fetchImpl: typeof fetch = fetch,
): Promise<ApnsResult> {
  const host = cfg.env === "production" ? "https://api.push.apple.com" : "https://api.sandbox.push.apple.com";
  const jwt = await apnsJwt(cfg, now);
  const res = await fetchImpl(`${host}/3/device/${deviceToken}`, {
    method: "POST",
    headers: {
      authorization: `bearer ${jwt}`,
      "apns-topic": cfg.topic,
      "apns-push-type": "alert",
      "apns-priority": "10",
    },
    body: JSON.stringify(payload),
  });
  const id = res.headers.get("apns-id") ?? undefined;
  if (res.status === 200) return { status: 200, id };
  let reason: string | undefined;
  try {
    reason = ((await res.json()) as { reason?: string })?.reason;
  } catch {
    /* non-JSON body */
  }
  return { status: res.status, id, reason };
}
