/**
 * Push-Notifications via Expo Server SDK.
 *
 * Verwendet von:
 *   - backend/server/db/chat.ts            sendDirectMessage()  → triggert nach Insert
 *   - backend/server/db/chat-rooms.ts      createRoomMessage()  → triggert nach Insert
 *
 * Versand ist fire-and-forget (kein Block des HTTP-Response): Errors werden geloggt
 * aber nicht propagiert, damit Chat-Send nicht failed wenn Expo-Server down ist.
 *
 * Token-Cleanup: Bei DeviceNotRegistered Receipts werden Tokens automatisch
 * disabled (PushToken.enabled = false) — Re-Enable nur durch Re-Registrierung
 * vom Client.
 *
 * IMPORT-HINWEIS: expo-server-sdk v6+ ist type:"module" und importiert undici (CJS)
 * intern via ESM-Syntax. Nitro/Rollup kann diesen ESM→CJS-Interop beim statischen
 * Bundlen nicht korrekt auflösen → `Class extends value [object Module] is not a
 * constructor` zur Laufzeit. Workaround: dynamic import (analog @parse/node-apn in
 * voip-push.ts), sodass das Modul erst zur Laufzeit aus node_modules geladen wird.
 */
import { usePrisma } from "../utils/prisma";
import { sendVoIPPush } from "./voip-push";

// expo-server-sdk lazy-geladen via dynamic import — verhindert Rollup-ESM/CJS-Interop-Bug.
type ExpoModule = typeof import("expo-server-sdk");
type ExpoInstance = InstanceType<ExpoModule["Expo"]>;
type ExpoPushMessage = ExpoModule["ExpoPushMessage"];

let _expoMod: ExpoModule | null = null;
let _expo: ExpoInstance | null = null;

async function ensureExpo(): Promise<{ expo: ExpoInstance; Expo: ExpoModule["Expo"] }> {
  if (_expo && _expoMod) return { expo: _expo, Expo: _expoMod.Expo };
  const mod = (await import("expo-server-sdk")) as unknown as ExpoModule & {
    default?: ExpoModule;
  };
  _expoMod = mod.default ?? mod;
  _expo = new _expoMod.Expo();
  return { expo: _expo, Expo: _expoMod.Expo };
}

export interface ChatPushPayload {
  /** Empfänger-User-ID (kann mehrere Tokens haben) */
  receiverId: string;
  /** Sender-Display-Name (für Titel "Max sendete: …") */
  senderName: string;
  /** Nachrichten-Vorschau (max ~100 Zeichen) */
  preview: string;
  /** Deep-Link-Daten — Client navigiert beim Tap */
  data: {
    type: "dm" | "room";
    /** Bei DM: senderId, bei Room: roomId */
    targetId: string;
    messageId: string;
  };
}

export async function sendChatPush(payload: ChatPushPayload): Promise<void> {
  try {
    const db = usePrisma();
    const { expo, Expo } = await ensureExpo();

    // 1) Profile-Opt-out prüfen
    const profile = await db.profile.findUnique({
      where: { id: payload.receiverId },
      select: { chatPushEnabled: true, deletedAt: true },
    });
    if (!profile || profile.deletedAt || !profile.chatPushEnabled) {
      return;
    }

    // 2) Alle aktiven Tokens des Receivers
    const tokens = await db.pushToken.findMany({
      where: { userId: payload.receiverId, enabled: true },
      select: { id: true, token: true },
    });
    if (tokens.length === 0) return;

    // 3) Messages bauen (nur valide Expo-Tokens)
    const messages: ExpoPushMessage[] = [];
    const validTokenIds: string[] = [];

    for (const t of tokens) {
      if (!Expo.isExpoPushToken(t.token)) {
        // Ungültiger Token-Format → disablen
        await db.pushToken
          .update({ where: { id: t.id }, data: { enabled: false } })
          .catch(() => {});
        continue;
      }
      messages.push({
        to: t.token,
        sound: "default",
        title: payload.senderName,
        body: payload.preview,
        data: payload.data,
        // Channel-ID für Android — wird in app.config.ts/native registriert
        channelId: "chat",
      });
      validTokenIds.push(t.id);
    }

    if (messages.length === 0) return;

    // 4) Senden in Chunks (Expo akzeptiert max 100 pro Request)
    const chunks = expo.chunkPushNotifications(messages);
    for (const chunk of chunks) {
      try {
        await expo.sendPushNotificationsAsync(chunk);
      } catch (err) {
        console.error("[push] chunk send failed:", err);
      }
    }

    // 5) lastUsedAt bumpen (best-effort)
    await db.pushToken
      .updateMany({
        where: { id: { in: validTokenIds } },
        data: { lastUsedAt: new Date() },
      })
      .catch(() => {});

    // Hinweis: Receipt-Polling (DeviceNotRegistered → token disablen) wird in
    // einem separaten Cron-Job gemacht (TODO: scripts/push-receipts-cleanup.ts).
    // Für MVP: Tokens bleiben aktiv bis sie explizit vom Client deregistriert
    // werden oder Expo dauerhaft DeviceNotRegistered meldet.
  } catch (err) {
    console.error("[push] sendChatPush failed:", err);
  }
}

/**
 * Helper für Username-Lookup (für Push-Titel).
 * Fällt auf "Jemand" zurück wenn kein nickname/username gesetzt.
 */
export async function getDisplayName(userId: string): Promise<string> {
  const db = usePrisma();
  const p = await db.profile.findUnique({
    where: { id: userId },
    select: { nickname: true, username: true },
  });
  return p?.nickname || p?.username || "Jemand";
}

/**
 * Truncate für Push-Body (Expo hat 4kb hard limit, aber UI zeigt eh nur ~2 Zeilen).
 */
export function truncatePreview(text: string, max = 100): string {
  if (text.length <= max) return text;
  return text.slice(0, max - 1) + "…";
}

export interface DeviceAddedPushPayload {
  /** Account-Owner — bekommt den Push auf seine mobilen Geräte */
  userId: string;
  /** Anzeigename des neu verbundenen Geräts (Hostname/Label) */
  deviceLabel: string;
  /** "macos" | "windows" | … — für den Plattform-Hinweis */
  platform: string;
}

function devicePlatformLabel(platform: string): string {
  const p = platform.toLowerCase();
  if (p.startsWith("mac")) return "Mac";
  if (p.startsWith("win")) return "Windows";
  if (p.startsWith("ios")) return "iPhone";
  if (p.startsWith("android")) return "Android";
  return "Gerät";
}

/**
 * Push bei neuer Geräte-Bindung („Neues Gerät verbunden").
 *
 * Account-Security-Signal: geht an ALLE aktiven Push-Tokens des Users (nur
 * mobile Geräte haben welche — Mac/Windows-Magic registriert keine Tokens).
 * Bewusst NICHT durch `chatPushEnabled` gegated — das ist eine sicherheits-
 * relevante Account-Benachrichtigung, kein Social-Push.
 *
 * Fire-and-forget wie die übrigen Push-Pfade.
 */
export async function sendDeviceAddedPush(
  payload: DeviceAddedPushPayload,
): Promise<void> {
  try {
    const db = usePrisma();
    const { expo, Expo } = await ensureExpo();

    const profile = await db.profile.findUnique({
      where: { id: payload.userId },
      select: { deletedAt: true },
    });
    if (!profile || profile.deletedAt) return;

    const tokens = await db.pushToken.findMany({
      where: { userId: payload.userId, enabled: true },
      select: { id: true, token: true },
    });
    if (tokens.length === 0) return;

    const platformLabel = devicePlatformLabel(payload.platform);
    const messages: ExpoPushMessage[] = [];
    const validTokenIds: string[] = [];

    for (const t of tokens) {
      if (!Expo.isExpoPushToken(t.token)) {
        await db.pushToken
          .update({ where: { id: t.id }, data: { enabled: false } })
          .catch(() => {});
        continue;
      }
      messages.push({
        to: t.token,
        sound: "default",
        title: "Neues Gerät verbunden",
        body: `${payload.deviceLabel} (${platformLabel}) ist jetzt geschützt.`,
        data: { type: "device_added", platform: payload.platform },
        // Dedizierter Channel (HIGH) — siehe usePushTokenRegistration.ts.
        // Fällt vor App-Re-Registrierung graceful auf Default-Channel zurück.
        channelId: "devices",
      });
      validTokenIds.push(t.id);
    }

    if (messages.length === 0) return;

    const chunks = expo.chunkPushNotifications(messages);
    for (const chunk of chunks) {
      try {
        await expo.sendPushNotificationsAsync(chunk);
      } catch (err) {
        console.error("[push] device-added chunk send failed:", err);
      }
    }

    await db.pushToken
      .updateMany({
        where: { id: { in: validTokenIds } },
        data: { lastUsedAt: new Date() },
      })
      .catch(() => {});
  } catch (err) {
    console.error("[push] sendDeviceAddedPush failed:", err);
  }
}

export interface CallRingPushPayload {
  /** Empfänger (Callee) — bekommt den Push */
  receiverId: string;
  /** Caller-Display-Name (für Titel) */
  callerName: string;
  /** Caller-Profil (für Show-Screen nach Tap) */
  callerId: string;
  callerNickname: string;
  callerAvatar: string | null;
  /** Call-ID — muss matchen damit Callee an die richtige Session andocken kann */
  callId: string;
}

/**
 * Push für eingehenden Voice-Call.
 *
 * Foreground-Calls funktionieren via Supabase Realtime; dieser Push deckt den
 * Background/locked-screen-Fall ab. Beim Tap navigiert der Client direkt in
 * den /call-Screen und triggert `useCall.receiveIncoming(callId, from)` —
 * die normale Accept/Decline-UI greift dann.
 *
 * Wichtig: KEIN VoIPPushKit (Apple-only, braucht CallKit). Wir nutzen den
 * regulären APNs-Alert-Push mit `priority: high` + Notifications-Sound.
 * Trade-off: Auf iOS wacht ein force-quit-killed Process NICHT auf — das ist
 * dasselbe Verhalten wie reguläre Chat-Pushes. Für echte CallKit-Integration
 * → Phase 2 (eigene Initiative).
 */
export async function sendCallRingPush(payload: CallRingPushPayload): Promise<void> {
  try {
    const db = usePrisma();
    const { expo, Expo } = await ensureExpo();

    const profile = await db.profile.findUnique({
      where: { id: payload.receiverId },
      select: { chatPushEnabled: true, deletedAt: true },
    });
    if (!profile || profile.deletedAt || !profile.chatPushEnabled) return;

    const tokens = await db.pushToken.findMany({
      where: { userId: payload.receiverId, enabled: true },
      select: { id: true, token: true, voipToken: true, platform: true },
    });
    console.log(
      `[call-ring] receiver=${payload.receiverId.slice(0,8)} tokens=${tokens.length} ` +
      `(ios=${tokens.filter(t=>t.platform==='ios').length}/voip=${tokens.filter(t=>t.platform==='ios'&&t.voipToken).length}, ` +
      `android=${tokens.filter(t=>t.platform==='android').length})`,
    );
    if (tokens.length === 0) return;

    // ─── 1) VoIP-Pushes (iOS, CallKit-Wake-from-killed-State) ─────────────
    // Läuft parallel zum regulären Push. Wenn voipToken NULL ist (Android,
    // alte iOS-Builds ohne PushKit-Setup) wird das Device hier übersprungen
    // und fällt auf den normalen Lockscreen-Banner zurück.
    const voipHandledTokenIds = new Set<string>();
    for (const t of tokens) {
      if (t.platform === "ios" && t.voipToken) {
        voipHandledTokenIds.add(t.id);
        void sendVoIPPush({
          voipToken: t.voipToken,
          callId: payload.callId,
          callerName: payload.callerName,
          callerId: payload.callerId,
          callerNickname: payload.callerNickname,
          callerAvatar: payload.callerAvatar,
        });
      }
    }

    // ─── 2) Reguläre Expo-Pushes (Android FCM + iOS-Fallback) ────────────
    // Wichtig: iOS-Devices die schon einen VoIP-Push bekommen haben werden
    // hier ÜBERSPRUNGEN — sonst doppeltes Klingeln (CallKit + Banner).
    const messages: ExpoPushMessage[] = [];
    const validTokenIds: string[] = [];

    for (const t of tokens) {
      if (voipHandledTokenIds.has(t.id)) continue;
      if (!Expo.isExpoPushToken(t.token)) {
        await db.pushToken
          .update({ where: { id: t.id }, data: { enabled: false } })
          .catch(() => {});
        continue;
      }
      messages.push({
        to: t.token,
        sound: "default",
        title: `📞 ${payload.callerName}`,
        body: "Eingehender Anruf",
        priority: "high",
        // Android: dedizierter Calls-Channel (im Client mit Importance.MAX +
        // Vibration + Bypass-DND zu konfigurieren)
        channelId: "calls",
        // iOS: zeigt als alert auch im Lockscreen (interruption-level)
        // 'time-sensitive' wäre besser, braucht aber Critical-Alerts-Entitlement.
        // 'active' (default) reicht für Phase 1.
        data: {
          type: "call",
          callId: payload.callId,
          from: {
            id: payload.callerId,
            nickname: payload.callerNickname,
            avatar: payload.callerAvatar,
          },
        },
      });
      validTokenIds.push(t.id);
    }

    if (messages.length === 0) return;
    console.log(`[call-ring] expo-push to ${messages.length} non-voip token(s) for receiver=${payload.receiverId.slice(0,8)}`);

    const chunks = expo.chunkPushNotifications(messages);
    for (const chunk of chunks) {
      try {
        await expo.sendPushNotificationsAsync(chunk);
      } catch (err) {
        console.error("[push] call-ring chunk send failed:", err);
      }
    }

    await db.pushToken
      .updateMany({
        where: { id: { in: validTokenIds } },
        data: { lastUsedAt: new Date() },
      })
      .catch(() => {});
  } catch (err) {
    console.error("[push] sendCallRingPush failed:", err);
  }
}
