import { create } from 'zustand';
import { AppState, NativeModules, Platform } from 'react-native';
import type { RealtimeChannel } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
import { apiFetch } from '../lib/api';
import { startRingback, stopRingback } from '../lib/ringback';
import * as callkit from '../lib/callkit';

// ─── Voice-Call-Engine (1:1, Audio-only, foreground-only / Phase 1) ──────────
//
// Signaling läuft über Supabase Realtime Broadcast — KEIN eigener Signaling-
// Server nötig:
//   • Ring-Channel  `call-ring:<userId>`  → nur die initiale Einladung/Abbruch.
//   • Call-Channel  `call:<callId>`       → ready/offer/answer/ice/decline/hangup.
//
// Handshake-Reihenfolge (verhindert "offer kommt bevor Callee subscribed"-Race):
//   Caller: ring → join call-channel → WARTET auf `ready` → erst dann offer.
//   Callee: accept → join call-channel → `ready` → wartet auf offer → answer.
//
// react-native-webrtc ist ein natives Modul → LAZY require + Guard. In einem
// Build ohne das Modul (z.B. der aktuelle Dev-Build) ist isWebRTCAvailable()
// false und der Call-Button meldet "Rebuild nötig" statt zu crashen.

export function isWebRTCAvailable(): boolean {
  return !!(NativeModules as any).WebRTCModule;
}

export type CallStatus =
  | 'idle'
  | 'outgoing'   // wir rufen an, warten auf Annahme
  | 'incoming'   // es klingelt bei uns
  | 'connecting' // angenommen, ICE/DTLS baut auf
  | 'connected'  // Audio läuft
  | 'ended';

export type CallPeer = { id: string; nickname: string; avatar: string | null };
export type CallEndReason = 'declined' | 'ended' | 'failed' | 'unanswered' | 'busy' | null;

const UNANSWERED_MS = 35_000;
// Callee-seitiges Auto-Cancel: Wenn ein eingehender Anruf in dieser Zeit
// weder angenommen noch durch Caller-cancel beendet wird (z.B. weil die App
// im Background war, der Caller schon aufgelegt hat und das ring-cancel-
// broadcast wegen fehlender Realtime-Subscription nicht ankam), räumen wir
// den stale 'incoming'-State auf — sonst sieht der User beim nächsten App-
// Open einen "Geist-Anruf".
const INCOMING_TIMEOUT_MS = 45_000;

// Nicht-reaktive Handles (gehören nicht in den Zustand-State).
let pc: any = null;
let localStream: any = null;
let callChan: RealtimeChannel | null = null;
let pendingRemoteIce: any[] = [];
let unansweredTimer: ReturnType<typeof setTimeout> | null = null;
let incomingTimer: ReturnType<typeof setTimeout> | null = null;
// Self-Heal-Fallback: setzt den Store nach einem beendeten Call zurück auf 'idle',
// FALLS der /call-Screen das nicht tut (iOS-Banner-only Incoming der ohne Mount
// des Screens endet). Ohne das bleibt status='ended' hängen und blockt jeden
// Folge-Call (RING-Guard `!== idle`).
let endedResetTimer: ReturnType<typeof setTimeout> | null = null;
let selfMe: CallPeer | null = null;       // für Caller-Side DM-Logging
let currentRole: 'caller' | 'callee' | null = null;
let loggedCallId: string | null = null;   // Idempotenz-Guard für DM-Log

type CallState = {
  status: CallStatus;
  peer: CallPeer | null;
  callId: string | null;
  muted: boolean;
  speaker: boolean;
  startedAt: number | null;
  endReason: CallEndReason;

  startCall: (peer: CallPeer, me: CallPeer) => Promise<void>;
  receiveIncoming: (callId: string, from: CallPeer) => void;
  acceptCall: () => Promise<void>;
  declineCall: () => void;
  hangup: (reason?: CallEndReason) => void;
  toggleMute: () => void;
  toggleSpeaker: () => void;
  _clear: () => void;
};

function rtc() {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  return require('react-native-webrtc');
}

// Audio-Routing (Earpiece vs. Lautsprecher) über AVAudioSession (iOS) /
// AudioManager (Android). react-native-webrtc exposed das nicht direkt.
function inCall() {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  return require('react-native-incall-manager').default;
}

// Diagnose-Logging (taucht in Metro-Konsole + adb logcat ReactNativeJS auf).
function clog(...args: any[]) {
  console.log('[CALL]', ...args);
}

// Fire-and-forget: einem noch klingelnden Callee (noch nicht im Call-Channel)
// den Abbruch auf seinem Ring-Channel signalisieren.
function fireRingCancel(peerId: string, callId: string) {
  const chan = supabase.channel(`call-ring:${peerId}`);
  chan.subscribe((s: string) => {
    if (s === 'SUBSCRIBED') {
      chan.send({ type: 'broadcast', event: 'cancel', payload: { callId } });
      setTimeout(() => supabase.removeChannel(chan), 300);
    }
  });
}

function teardown() {
  if (unansweredTimer) { clearTimeout(unansweredTimer); unansweredTimer = null; }
  if (incomingTimer) { clearTimeout(incomingTimer); incomingTimer = null; }
  try { pc?.close?.(); } catch {}
  try { localStream?.getTracks?.().forEach((t: any) => t.stop()); } catch {}
  if (callChan) { supabase.removeChannel(callChan); callChan = null; }
  // Audio-Session beenden → normales Audio-Routing wiederherstellen.
  // stopRingtone/stopRingback sind no-ops wenn nichts läuft — safe.
  try { inCall().stopRingtone(); } catch {}
  void stopRingback();
  try { inCall().stop(); } catch {}
  pc = null;
  localStream = null;
  pendingRemoteIce = [];
  // Self-Heal: Wenn der Call auf 'ended' steht (hangup/decline/unanswered) und
  // der /call-Screen ihn nicht innerhalb seiner 1300ms-Logik auf 'idle' zieht
  // (weil er bei iOS-Incoming-Banner nie gemountet wurde), würde der Store ewig
  // 'ended' bleiben → ALLE Folge-Calls blockiert. Entkoppelter Fallback-Reset.
  if (endedResetTimer) { clearTimeout(endedResetTimer); endedResetTimer = null; }
  endedResetTimer = setTimeout(() => {
    endedResetTimer = null;
    const s = useCallStore.getState();
    if (s.status === 'ended') {
      useCallStore.setState({
        status: 'idle', peer: null, callId: null,
        muted: false, speaker: false, startedAt: null, endReason: null,
      });
    }
  }, 2500);
}

// DM-Log nach Call-Ende. Nur der CALLER schreibt (verhindert Duplikate).
// Format: attachmentType='call', attachmentName='<kind>:<state>:<durSec>'.
async function logCallToChat(
  peerId: string,
  callId: string,
  state: 'ended' | 'unanswered' | 'declined' | 'failed' | 'busy',
  startedAt: number | null,
) {
  // Caller loggt jeden Call. Callee loggt NUR missed/declined/unanswered
  // wenn der Caller dazu nicht kommt (z.B. Callee drückt 'decline' bevor der
  // Caller-DM-Call durchgeht). Verbundene Calls werden vom Caller geloggt.
  if (currentRole === 'callee' && state === 'ended') return;
  if (loggedCallId === callId) return;
  loggedCallId = callId;
  const durSec = startedAt ? Math.max(0, Math.round((Date.now() - startedAt) / 1000)) : 0;
  const meta = `audio:${state}:${durSec}`;
  try {
    await apiFetch('/api/chat/dm', {
      method: 'POST',
      body: {
        receiverId: peerId,
        content: '',
        attachmentType: 'call',
        attachmentName: meta,
      },
    });
    clog('logged call DM →', peerId, meta);
  } catch (e: any) {
    clog('logCallToChat FAILED', e?.message ?? e);
  }
}

export const useCallStore = create<CallState>((set, get) => {
  // ─── WebRTC-Setup gemeinsam für beide Seiten ──────────────────────────────
  async function buildPeer() {
    const { RTCPeerConnection } = rtc();
    clog('buildPeer: fetching ice-servers…');
    let ice: { iceServers: any[]; iceTransportPolicy?: string };
    try {
      ice = await apiFetch('/api/calls/ice-servers');
    } catch (e: any) {
      clog('buildPeer: ICE-FETCH FAILED', e?.message ?? e, e?.statusCode ?? '');
      throw e;
    }
    clog('buildPeer: ice ok', JSON.stringify({
      servers: ice.iceServers?.length,
      policy: ice.iceTransportPolicy,
      urls: ice.iceServers?.[0]?.urls,
    }));
    pc = new RTCPeerConnection({
      iceServers: ice.iceServers,
      iceTransportPolicy: (ice.iceTransportPolicy as any) ?? 'relay',
    });

    pc.addEventListener('icecandidate', (e: any) => {
      if (e.candidate && callChan) {
        callChan.send({
          type: 'broadcast',
          event: 'ice',
          payload: {
            candidate: e.candidate.candidate,
            sdpMid: e.candidate.sdpMid,
            sdpMLineIndex: e.candidate.sdpMLineIndex,
          },
        });
      }
    });
    pc.addEventListener('icecandidateerror', (e: any) => {
      clog('ICE-CANDIDATE-ERROR', e?.errorCode, e?.errorText, e?.url);
    });
    pc.addEventListener('iceconnectionstatechange', () => {
      clog('iceConnectionState =', pc?.iceConnectionState);
    });
    pc.addEventListener('icegatheringstatechange', () => {
      clog('iceGatheringState =', pc?.iceGatheringState);
    });

    pc.addEventListener('connectionstatechange', () => {
      const st = pc?.connectionState;
      clog('connectionState =', st);
      if (st === 'connected') {
        if (get().status !== 'connected') {
          set({ status: 'connected', startedAt: Date.now() });
        }
        // Ringback aus — Verbindung steht.
        void stopRingback();
        // CallKit/ConnectionService: Call als aktiv markieren (Android needs this
        // für Mute/Speaker-Controls).
        try {
          const cid = get().callId;
          if (cid) callkit.reportConnected(cid);
        } catch {}
        // WebRTC hat seine eigene AVAudioSession aktiviert und unser früherer
        // InCallManager-Call ist evtl. überschrieben worden — Speaker-Route
        // jetzt erneut setzen, damit der User-Toggle wirklich greift.
        try {
          inCall().setForceSpeakerphoneOn(get().speaker);
          clog('post-connect: speaker route applied =', get().speaker);
        } catch (e: any) {
          clog('post-connect setForceSpeakerphoneOn failed:', e?.message ?? e);
        }
      } else if (st === 'failed') {
        get().hangup('failed');
      }
    });

    // Mikrofon holen + Track anhängen.
    clog('buildPeer: getUserMedia(audio)…');
    const { mediaDevices } = rtc();
    try {
      localStream = await mediaDevices.getUserMedia({ audio: true, video: false });
    } catch (e: any) {
      clog('buildPeer: getUserMedia FAILED', e?.message ?? e);
      throw e;
    }
    localStream.getTracks().forEach((t: any) => pc.addTrack(t, localStream));
    // Audio-Session in den "In-Call"-Mode bringen (richtet Routing, Bluetooth,
    // Audio-Focus etc. ein). Default = Earpiece, NICHT Speaker.
    try {
      inCall().start({ media: 'audio', auto: false });
      inCall().setForceSpeakerphoneOn(get().speaker);
    } catch (e: any) {
      clog('InCallManager start failed:', e?.message ?? e);
    }
    clog('buildPeer: done (track added)');
  }

  function addRemoteIce(payload: any) {
    const { RTCIceCandidate } = rtc();
    const cand = new RTCIceCandidate(payload);
    if (pc?.remoteDescription) {
      pc.addIceCandidate(cand).catch(() => {});
    } else {
      pendingRemoteIce.push(payload);
    }
  }

  function drainIce() {
    const { RTCIceCandidate } = rtc();
    pendingRemoteIce.forEach((p) => pc?.addIceCandidate(new RTCIceCandidate(p)).catch(() => {}));
    pendingRemoteIce = [];
  }

  // Per-Call-Channel beitreten + Events verdrahten. role steuert offer/answer.
  function joinCallChannel(callId: string, role: 'caller' | 'callee') {
    const chan = supabase.channel(`call:${callId}`, {
      config: { broadcast: { self: false } },
    });

    chan.on('broadcast', { event: 'ready' }, async () => {
      clog('recv ready (role=' + role + ', pc=' + !!pc + ')');
      if (role !== 'caller' || !pc) return;
      const offer = await pc.createOffer({});
      await pc.setLocalDescription(offer);
      clog('caller: sending offer');
      chan.send({ type: 'broadcast', event: 'offer', payload: { type: offer.type, sdp: offer.sdp } });
    });

    chan.on('broadcast', { event: 'offer' }, async (msg: any) => {
      clog('recv offer (role=' + role + ', pc=' + !!pc + ')');
      if (role !== 'callee' || !pc) return;
      const { RTCSessionDescription } = rtc();
      await pc.setRemoteDescription(new RTCSessionDescription(msg.payload));
      drainIce();
      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);
      clog('callee: sending answer');
      chan.send({ type: 'broadcast', event: 'answer', payload: { type: answer.type, sdp: answer.sdp } });
    });

    chan.on('broadcast', { event: 'answer' }, async (msg: any) => {
      clog('recv answer (role=' + role + ')');
      if (role !== 'caller' || !pc) return;
      const { RTCSessionDescription } = rtc();
      await pc.setRemoteDescription(new RTCSessionDescription(msg.payload));
      drainIce();
    });

    chan.on('broadcast', { event: 'ice' }, (msg: any) => addRemoteIce(msg.payload));
    chan.on('broadcast', { event: 'decline' }, () => { clog('recv decline'); get().hangup('declined'); });
    chan.on('broadcast', { event: 'hangup' }, () => { clog('recv hangup'); get().hangup('ended'); });

    callChan = chan;
    return chan;
  }

  return {
    status: 'idle',
    peer: null,
    callId: null,
    muted: false,
    speaker: false,
    startedAt: null,
    endReason: null,

    // ─── Caller ──────────────────────────────────────────────────────────
    startCall: async (peer, me) => {
      if (get().status !== 'idle') return;
      if (!isWebRTCAvailable()) throw new Error('webrtc_unavailable');
      const callId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
      clog('startCall → callee', peer.id, 'callId', callId);
      selfMe = me;
      currentRole = 'caller';
      loggedCallId = null;
      set({ status: 'outgoing', peer, callId, muted: false, speaker: false, startedAt: null, endReason: null });

      // 1) Callee anklingeln (ephemerer Ring-Channel des Callees).
      const ring = supabase.channel(`call-ring:${peer.id}`);
      await new Promise<void>((res) => ring.subscribe((s: string) => s === 'SUBSCRIBED' && res()));
      clog('ring channel subscribed → sending ring');
      ring.send({
        type: 'broadcast',
        event: 'ring',
        payload: { callId, from: { id: me.id, nickname: me.nickname, avatar: me.avatar } },
      });
      setTimeout(() => supabase.removeChannel(ring), 300);

      // Background-Push an den Callee (deckt den Fall ab, dass die App nicht
      // im Foreground ist — Realtime ist dann nicht subscribed). Fire-and-forget:
      // ein fehlgeschlagener Push darf den Call nicht blocken.
      apiFetch('/api/calls/ring', {
        method: 'POST',
        body: { peerId: peer.id, callId },
      }).catch((e: any) => clog('ring-push failed', e?.message ?? e));

      // CallKit: User-Seite in Recents/UI als ausgehender Call sichtbar machen.
      try { callkit.startOutgoingCall(callId, peer.nickname || 'ReBreak'); } catch {}

      // Ringback-Ton für den Anrufer — eigenes mp3-Asset (EU-Standard 425 Hz,
      // 1s an / 4s aus). InCallManager.start() ohne `ringback`-Param läuft
      // parallel für die AVAudioSession-Aktivierung, der eigentliche Ton kommt
      // von expo-av.
      try {
        inCall().start({ media: 'audio', auto: false });
      } catch (e: any) { clog('inCall start (caller) failed', e?.message ?? e); }
      void startRingback();

      // 2) Call-Channel join (Handler setzen) → PeerConnection bauen → ERST DANN
      //    subscriben. Sonst könnte ein schnelles `ready` den offer-Handler treffen,
      //    bevor pc existiert → kein offer → Deadlock.
      const chan = joinCallChannel(callId, 'caller');
      try {
        await buildPeer();
      } catch (e: any) {
        clog('startCall: buildPeer FAILED →', e?.message ?? e);
        get().hangup('failed');
        throw e;
      }
      chan.subscribe((s: string) => clog('caller call-channel:', s));

      // 3) Timeout: keine Annahme → auflegen (hangup signalisiert Ring-Cancel).
      unansweredTimer = setTimeout(() => {
        if (get().status === 'outgoing') get().hangup('unanswered');
      }, UNANSWERED_MS);
    },

    // ─── Callee: Klingeln empfangen ───────────────────────────────────────
    receiveIncoming: (callId, from) => {
      // Schon im Gespräch / oder bereits am Klingeln mit derselben callId?
      // → ignorieren (dedup: Realtime + VoIP-Push können beide feuern).
      const cur = get();
      if (cur.status === 'ended') {
        // Stale 'ended' von einem vorherigen Call (z.B. iOS-Banner-Incoming der
        // ohne /call-Screen endete) → als 'idle' behandeln, sonst blockt er den
        // neuen Call. Self-Heal-Timer abbrechen, wir übernehmen jetzt.
        clog('receiveIncoming: clearing stale ended-state for new call', callId);
        if (endedResetTimer) { clearTimeout(endedResetTimer); endedResetTimer = null; }
      } else if (cur.status !== 'idle') {
        if (cur.status === 'incoming' && cur.callId === callId) {
          clog('receiveIncoming dedup (already incoming for', callId, ')');
        }
        return;
      }
      clog('receiveIncoming from', from.id, 'callId', callId);
      currentRole = 'callee';
      loggedCallId = null;
      set({ status: 'incoming', peer: from, callId, muted: false, speaker: false, startedAt: null, endReason: null });
      // CallKit-/ConnectionService-UI hochziehen — ABER nur wenn App NICHT im
      // Vordergrund. Im Foreground kümmert sich der In-App /call-Screen darum,
      // sonst gibt es Doppel-UI (System-Banner + Fullscreen).
      //
      // iOS-Hinweis: Bei VoIP-Push hat AppDelegate.swift bereits
      // `RNCallKeep.reportNewIncomingCall` aufgerufen — wir dürfen das hier
      // NICHT erneut tun (gleiche UUID → CallKit zeigt Banner doppelt /
      // verhält sich kaputt). Auf iOS gibt es keinen sinnvollen JS-Pfad
      // für Background-Ring, da Supabase-Realtime im Background nicht läuft;
      // jeder bg-Ring kommt zwingend via PushKit → AppDelegate.
      if (Platform.OS !== 'ios' && AppState.currentState !== 'active') {
        try { callkit.displayIncomingCall(callId, from.nickname || 'ReBreak'); } catch {}
      } else {
        clog('receiveIncoming: skipping JS-side CallKit show (foreground OR iOS-AppDelegate already handled)');
      }
      // Auto-Cancel-Timer: Wenn nach 45s noch immer 'incoming' (kein Accept,
      // kein Caller-Cancel angekommen), als 'unanswered' aufräumen damit nicht
      // beim nächsten App-Open ein toter Call-Screen sichtbar wird.
      if (incomingTimer) clearTimeout(incomingTimer);
      incomingTimer = setTimeout(() => {
        const st = get();
        if (st.status === 'incoming' && st.callId === callId) {
          clog('receiveIncoming: stale incoming timeout → hangup(unanswered)');
          st.hangup('unanswered');
        }
      }, INCOMING_TIMEOUT_MS);
      // CallKit (iOS) + ConnectionService (Android) spielen ihren eigenen
      // Ringtone — KEIN InCallManager.startRingtone() hier, sonst doppeltes
      // Klingeln. InCallManager.start() bleibt aus demselben Grund weg; der
      // CallKit-Pfad aktiviert die AVAudioSession selbst.
    },

    acceptCall: async () => {
      const { callId, status } = get();
      if (status !== 'incoming' || !callId) return;
      if (!isWebRTCAvailable()) throw new Error('webrtc_unavailable');
      clog('acceptCall callId', callId);
      // Klingelton stoppen — wir nehmen ab.
      try { inCall().stopRingtone(); } catch {}
      set({ status: 'connecting' });
      const chan = joinCallChannel(callId, 'callee');
      await new Promise<void>((res) => chan.subscribe((s: string) => s === 'SUBSCRIBED' && res()));
      clog('callee call-channel subscribed');
      try {
        await buildPeer();
      } catch (e: any) {
        clog('acceptCall: buildPeer FAILED →', e?.message ?? e);
        get().hangup('failed');
        throw e;
      }
      clog('callee: sending ready');
      chan.send({ type: 'broadcast', event: 'ready', payload: {} });
    },

    declineCall: () => {
      const { callId, peer, startedAt, status } = get();
      clog('declineCall called — status=', status, 'callId=', callId);
      if (callId) {
        // CallKit/ConnectionService aus dem Lockscreen-UI entfernen.
        try { callkit.endCall(callId); } catch {}
        // Kurz joinen um decline zu senden (Callee war noch nicht im Channel).
        const chan = supabase.channel(`call:${callId}`);
        chan.subscribe((s: string) => {
          if (s === 'SUBSCRIBED') {
            chan.send({ type: 'broadcast', event: 'decline', payload: {} });
            setTimeout(() => supabase.removeChannel(chan), 300);
          }
        });
      }
      // Callee logged abgelehnten Call in eigenen Chat-Verlauf.
      if (peer && callId) {
        logCallToChat(peer.id, callId, 'declined', startedAt);
      }
      set({ status: 'ended', endReason: 'declined' });
      teardown();
    },

    hangup: (reason = 'ended') => {
      const { status, peer, callId, startedAt } = get();
      clog('hangup called — reason=', reason, 'status=', status, 'callId=', callId);
      if (status === 'idle' || status === 'ended') {
        teardown();
        return;
      }
      // CallKit/ConnectionService schließen.
      if (callId) {
        try {
          callkit.endCall(callId);
          callkit.reportEnded(callId, reason as any);
        } catch {}
      }
      // Peer im Call-Channel informieren (falls schon verbunden).
      callChan?.send({ type: 'broadcast', event: 'hangup', payload: {} });
      // Klingelt der Callee noch (noch nicht im Call-Channel)? → Ring abbrechen.
      if (peer && callId && (status === 'outgoing' || status === 'connecting')) {
        fireRingCancel(peer.id, callId);
      }
      // Call als DM ins Chat-Log schreiben (nur Caller, fire-and-forget).
      if (peer && callId && reason) {
        const state = (reason === null ? 'ended' : reason) as 'ended' | 'unanswered' | 'declined' | 'failed' | 'busy';
        logCallToChat(peer.id, callId, state, startedAt);
      }
      set({ status: 'ended', endReason: reason });
      teardown();
    },

    toggleMute: () => {
      const next = !get().muted;
      try {
        localStream?.getAudioTracks?.().forEach((t: any) => { t.enabled = !next; });
      } catch {}
      set({ muted: next });
    },

    toggleSpeaker: () => {
      const next = !get().speaker;
      try {
        // Sicherheitshalber start() erneut aufrufen — falls die AVAudioSession
        // von WebRTC zwischendurch deaktiviert wurde, sonst greift
        // setForceSpeakerphoneOn auf iOS nicht.
        inCall().start({ media: 'audio', auto: false });
        inCall().setForceSpeakerphoneOn(next);
        clog('toggleSpeaker →', next);
      } catch (e: any) {
        clog('setForceSpeakerphoneOn failed:', e?.message ?? e);
      }
      set({ speaker: next });
    },

    _clear: () => {
      teardown();
      set({ status: 'idle', peer: null, callId: null, muted: false, speaker: false, startedAt: null, endReason: null });
    },
  };
});
