/**
 * iOS-VoIP-Push via PushKit + APNs HTTP/2.
 *
 * Unterschied zu services/push.ts (Expo Server SDK):
 *   - Reguläre Pushes laufen über Expo's Proxy (token = ExponentPushToken[xxx])
 *   - VoIP-Pushes MÜSSEN direkt an APNs gehen (Apple verbietet Proxy für PushKit)
 *   - Nutzt das VoIP-Services-Cert (.p12) das wir bei Apple beantragt haben
 *   - Wake-from-killed-State: einziger Weg ein iOS-App aufzuwecken um CallKit
 *     UI anzuzeigen
 *
 * Env-Vars (aus Infisical):
 *   APNS_VOIP_P12_PATH       Pfad zur .p12-Datei (z.B. /root/.secrets/rebreak-voip.p12)
 *   APNS_VOIP_P12_PASSWORD   Cert-Password
 *   APNS_VOIP_TOPIC          Bundle-ID + ".voip" (org.rebreak.app.voip)
 *   APNS_VOIP_PRODUCTION     "true" für Production-APNs-Endpoint
 *
 * Wenn ENV-Vars fehlen: Service no-op (kein Error, nur Log-Warnung beim ersten Send).
 * → Macht reguläre Pushes nicht kaputt wenn das VoIP-Setup noch unfertig ist.
 */
import fs from "node:fs";

// node-apn dynamisch laden (await import statt static import). Grund: nitro/rollup
// bricht beim Bundlen von node-apn → undici → `Class extends value [object Module]`
// at push.mjs. Dynamic import wird nicht statisch getraced, lädt Modul zur Laufzeit
// aus node_modules wo es korrekt als CJS funktioniert.
type ApnModule = typeof import("@parse/node-apn");
type ApnProvider = InstanceType<ApnModule["Provider"]>;
type ApnNotification = InstanceType<ApnModule["Notification"]>;

let apnMod: ApnModule | null = null;
let providerProd: ApnProvider | null = null;
let providerSandbox: ApnProvider | null = null;
let initialized = false;
let topic: string | null = null;
/** Token → "prod" | "sandbox" Memoization. Vermeidet 2-Round-Trip pro Push. */
const tokenEnvCache = new Map<string, "prod" | "sandbox">();

async function ensureInit(): Promise<boolean> {
  if (initialized) return providerProd !== null || providerSandbox !== null;
  initialized = true;

  const p12Path = process.env.APNS_VOIP_P12_PATH;
  const p12Pass = process.env.APNS_VOIP_P12_PASSWORD;
  const tpc = process.env.APNS_VOIP_TOPIC;

  if (!p12Path || !p12Pass || !tpc) {
    console.warn(
      "[voip-push] disabled — missing env (APNS_VOIP_P12_PATH/PASSWORD/TOPIC)",
    );
    return false;
  }
  if (!fs.existsSync(p12Path)) {
    console.warn(`[voip-push] disabled — p12 file not found at ${p12Path}`);
    return false;
  }

  try {
    const mod = (await import("@parse/node-apn")) as unknown as ApnModule & {
      default?: ApnModule;
    };
    apnMod = mod.default ?? mod;
    topic = tpc;
    // VoIP-Services-Cert ist Universal — wir bauen BEIDE Provider (Production +
    // Sandbox) und wählen pro Token via Memoization. So funktioniert es für
    // Xcode-Dev-Builds (Sandbox) UND TestFlight/AppStore (Production) parallel.
    providerProd = new apnMod.Provider({
      pfx: p12Path,
      passphrase: p12Pass,
      production: true,
    });
    providerSandbox = new apnMod.Provider({
      pfx: p12Path,
      passphrase: p12Pass,
      production: false,
    });
    console.log(`[voip-push] initialized (topic=${tpc}, prod+sandbox providers ready)`);
    return true;
  } catch (err) {
    console.error("[voip-push] init failed:", err);
    return false;
  }
}

export interface VoIPCallPayload {
  /** VoIP-PushKit-Token des Empfänger-Devices (64-char hex, NICHT der Expo-Token) */
  voipToken: string;
  /** Eindeutige Call-ID (matcht callIdToUuid() im Client) */
  callId: string;
  /** Display-Name für CallKit-UI */
  callerName: string;
  /** User-ID des Anrufers — Client braucht das um peer-Profile zu fetchen */
  callerId: string;
  callerNickname: string;
  callerAvatar: string | null;
}

/**
 * Sendet einen VoIP-Push an genau ein iOS-Device.
 *
 * Apple-Vorgaben (verifiziert via developer.apple.com/documentation/pushkit):
 *   - apns-push-type: voip
 *   - apns-topic: bundle-id + ".voip" (NICHT der reguläre bundle-id)
 *   - apns-priority: 10
 *   - apns-expiration: 0 (kein Storage — wenn Device offline, verworfen)
 *
 * @returns true bei Erfolg (oder no-op wenn Service disabled), false bei Fehler.
 */
export async function sendVoIPPush(payload: VoIPCallPayload): Promise<boolean> {
  const ok = await ensureInit();
  if (!ok || !topic || !apnMod) return true; // no-op, regulärer Push übernimmt

  const note: ApnNotification = new apnMod.Notification();
  note.topic = topic;
  note.expiry = 0; // sofort verwerfen wenn Device unreachable
  note.priority = 10;
  note.pushType = "voip";
  note.payload = {
    type: "call",
    callId: payload.callId,
    callerName: payload.callerName,
    from: {
      id: payload.callerId,
      nickname: payload.callerNickname,
      avatar: payload.callerAvatar,
    },
  };

  // Reihenfolge: zuerst memoized Env (falls bekannt), sonst Production first →
  // Sandbox als Fallback.
  const cached = tokenEnvCache.get(payload.voipToken);
  const order: Array<"prod" | "sandbox"> =
    cached === "sandbox" ? ["sandbox", "prod"] : ["prod", "sandbox"];

  for (const env of order) {
    const prov = env === "prod" ? providerProd : providerSandbox;
    if (!prov) continue;
    try {
      const result = await prov.send(note, payload.voipToken);
      if (result.failed.length === 0) {
        tokenEnvCache.set(payload.voipToken, env);
        console.log(`[voip-push] sent token=${payload.voipToken.slice(0, 8)}… env=${env} callId=${payload.callId}`);
        return true;
      }
      const f = result.failed[0];
      const reason = (f.response as { reason?: string })?.reason;
      // BadDeviceToken → nur dann Failover, sonst Abbruch (z.B. Unregistered).
      if (reason === "BadDeviceToken") {
        if (env === order[order.length - 1]) {
          console.warn(
            `[voip-push] failed token=${payload.voipToken.slice(0, 8)}… BadDeviceToken on both envs`,
          );
          return false;
        }
        // try next env
        continue;
      }
      console.warn(
        `[voip-push] failed token=${payload.voipToken.slice(0, 8)}… env=${env} reason=`,
        f.response ?? f.error,
      );
      return false;
    } catch (err) {
      console.error("[voip-push] send threw:", err);
      return false;
    }
  }
  return false;
}

/** Cleanup bei Server-Shutdown — wichtig wegen HTTP/2-Verbindung an Apple. */
export function shutdownVoIPProvider(): void {
  if (providerProd) {
    providerProd.shutdown();
    providerProd = null;
  }
  if (providerSandbox) {
    providerSandbox.shutdown();
    providerSandbox = null;
  }
  initialized = false;
}
