import { useCallback, useEffect, useRef } from "preact/hooks";
import type { ClientMessage } from "../../types";
import { BACKEND_URL } from "./backend";
import type { AppendLine, RTCCallbacks, RTCRefs } from "./types";

const WEBRTC_STUN_URL = import.meta.env.VITE_WEBRTC_STUN_URL?.trim() ?? "";
const LOCAL_ICE_GATHER_TIMEOUT_MS = 350;

function stopMediaTracks(tracks: Iterable<MediaStreamTrack | null | undefined>): void {
  const seen = new Set<MediaStreamTrack>();
  for (const track of tracks) {
    if (!track || seen.has(track)) continue;
    seen.add(track);
    track.stop();
  }
}

async function acquireMicStream(): Promise<MediaStream> {
  try {
    return await navigator.mediaDevices.getUserMedia({
      audio: {
        channelCount: 1,
        sampleRate: 48000,
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: false,
      },
      video: false,
    });
  } catch {
    return navigator.mediaDevices.getUserMedia({ audio: true, video: false });
  }
}

function waitForIceComplete(pc: RTCPeerConnection): Promise<void> {
  return new Promise<void>((resolve) => {
    if (pc.iceGatheringState === "complete") {
      resolve();
      return;
    }
    const check = () => {
      if (pc.iceGatheringState === "complete") {
        pc.removeEventListener("icegatheringstatechange", check);
        resolve();
      }
    };
    pc.addEventListener("icegatheringstatechange", check);
    setTimeout(() => {
      pc.removeEventListener("icegatheringstatechange", check);
      resolve();
    }, LOCAL_ICE_GATHER_TIMEOUT_MS);
  });
}

async function exchangeSdp(
  localDesc: RTCSessionDescription,
): Promise<{ sdp: string; rtcType: string }> {
  const rtcUrl = BACKEND_URL ? `${BACKEND_URL}/rtc/offer` : "/rtc/offer";
  const resp = await fetch(rtcUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sdp: localDesc.sdp, rtcType: localDesc.type }),
  });
  if (!resp.ok) throw new Error(`/rtc/offer returned ${resp.status}`);
  return resp.json() as Promise<{ sdp: string; rtcType: string }>;
}

async function runConnect(
  refs: RTCRefs,
  cbs: RTCCallbacks,
  opts: { textOnly: boolean },
): Promise<void> {
  if (refs.pcRef.current) return;
  if (!window.RTCPeerConnection) {
    cbs.showStatus("WebRTC unavailable in this browser.", 4000);
    return;
  }
  cbs.setConnecting(true);
  cbs.showStatus("Connecting...");

  let micStream: MediaStream | null = null;
  try {
    refs.localTracksRef.current = [];
    if (!opts.textOnly) {
      micStream = await acquireMicStream();
      const audioTracks = micStream.getAudioTracks();
      audioTracks.forEach((track) => {
        track.enabled = false;
      });
      refs.localTracksRef.current = audioTracks;
    }

    const pc = new RTCPeerConnection(
      WEBRTC_STUN_URL ? { iceServers: [{ urls: WEBRTC_STUN_URL }] } : undefined,
    );
    refs.pcRef.current = pc;

    const newRemoteStream = new MediaStream();
    cbs.setRemoteStream(newRemoteStream);
    if (refs.remoteAudioRef.current) {
      refs.remoteAudioRef.current.srcObject = newRemoteStream;
      refs.remoteAudioRef.current.play().catch(() => {});
    }

    pc.ontrack = (event) => {
      if (event.track.kind !== "audio") return;
      newRemoteStream.addTrack(event.track);
      refs.remoteAudioRef.current?.play().catch(() => {});
    };

    const dc = pc.createDataChannel("app", { ordered: true });
    refs.dcRef.current = dc;
    dc.onopen = () => {
      cbs.setConnected(true);
      cbs.setConnecting(false);
      cbs.showStatus(opts.textOnly ? "Text-only mode enabled" : "Hold anywhere to talk", 2500);
      cbs.appendLine("system", "Connected.", new Date().toISOString());
      cbs.onDcOpen();
    };
    dc.onclose = () => {
      cbs.appendLine("system", "Disconnected.", new Date().toISOString());
      cbs.closePC();
    };
    dc.onmessage = (e) => cbs.onDcMessage(e.data as string);

    refs.micSendersRef.current = [];
    if (micStream) {
      micStream.getAudioTracks().forEach((track) => {
        pc.addTrack(track, micStream as MediaStream);
      });
      refs.micSendersRef.current = pc.getSenders().filter((s) => s.track?.kind === "audio");
    }

    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    await waitForIceComplete(pc);

    const localDesc = pc.localDescription;
    if (!localDesc) throw new Error("No local description after ICE gathering");
    const answer = await exchangeSdp(localDesc);
    await pc.setRemoteDescription({ type: answer.rtcType as RTCSdpType, sdp: answer.sdp });
  } catch (err) {
    cbs.appendLine("system", `Connection failed: ${err}`, new Date().toISOString());
    cbs.showStatus("Connection failed.", 3000);
    cbs.closePC();
  }
}

export function useRtcRefs(): RTCRefs {
  return {
    pcRef: useRef<RTCPeerConnection | null>(null),
    dcRef: useRef<RTCDataChannel | null>(null),
    remoteAudioRef: useRef<HTMLAudioElement | null>(null),
    micSendersRef: useRef<RTCRtpSender[]>([]),
    localTracksRef: useRef<MediaStreamTrack[]>([]),
  };
}

export function useRemoteAudioBindings({
  textOnly,
  connected,
  showStatus,
  remoteAudioRef,
  micSendersRef,
  dcRef,
  textOnlyRef,
}: {
  textOnly: boolean;
  connected: boolean;
  showStatus: (text: string, persistMs?: number) => void;
  remoteAudioRef: { current: HTMLAudioElement | null };
  micSendersRef: { current: RTCRtpSender[] };
  dcRef: { current: RTCDataChannel | null };
  textOnlyRef: { current: boolean };
}) {
  useEffect(() => {
    textOnlyRef.current = textOnly;
  }, [textOnly, textOnlyRef]);

  useEffect(() => {
    const audio = new Audio();
    audio.autoplay = true;
    (audio as HTMLAudioElement & { playsInline: boolean }).playsInline = true;
    audio.muted = textOnlyRef.current;
    remoteAudioRef.current = audio;
    return () => {
      audio.srcObject = null;
    };
  }, [remoteAudioRef, textOnlyRef]);

  useEffect(() => {
    const handler = (e: Event) => {
      const enabled = (e as CustomEvent<{ enabled: boolean }>).detail?.enabled ?? false;
      micSendersRef.current.forEach((sender) => {
        if (sender.track) sender.track.enabled = enabled && !textOnlyRef.current;
      });
    };
    window.addEventListener("nanobot-mic-enable", handler);
    return () => window.removeEventListener("nanobot-mic-enable", handler);
  }, [micSendersRef, textOnlyRef]);

  useEffect(() => {
    if (remoteAudioRef.current) {
      remoteAudioRef.current.muted = textOnly;
      if (textOnly) remoteAudioRef.current.pause();
      else remoteAudioRef.current.play().catch(() => {});
    }
    micSendersRef.current.forEach((sender) => {
      if (sender.track) sender.track.enabled = false;
    });
    if (textOnly) {
      const dc = dcRef.current;
      if (dc?.readyState === "open") {
        dc.send(JSON.stringify({ type: "voice-ptt", pressed: false } satisfies ClientMessage));
      }
    }
    if (connected) showStatus(textOnly ? "Text-only mode enabled" : "Hold anywhere to talk", 2000);
  }, [connected, dcRef, micSendersRef, remoteAudioRef, showStatus, textOnly]);
}

export function usePeerConnectionControls({
  textOnly,
  connected,
  appendLine,
  onDcMessage,
  loadPersistedCards,
  loadWorkbench,
  showStatus,
  refs,
  setConnected,
  setConnecting,
  setRemoteStream,
  textOnlyRef,
}: {
  textOnly: boolean;
  connected: boolean;
  appendLine: AppendLine;
  onDcMessage: (raw: string) => void;
  loadPersistedCards: () => Promise<void>;
  loadWorkbench: () => Promise<void>;
  showStatus: (text: string, persistMs?: number) => void;
  refs: RTCRefs;
  setConnected: (value: boolean) => void;
  setConnecting: (value: boolean) => void;
  setRemoteStream: (stream: MediaStream | null) => void;
  textOnlyRef: { current: boolean };
}) {
  const closePC = useCallback(() => {
    const dc = refs.dcRef.current;
    const pc = refs.pcRef.current;
    const localTracks = refs.localTracksRef.current;
    const senderTracks = refs.micSendersRef.current.map((sender) => sender.track);

    refs.dcRef.current = null;
    refs.pcRef.current = null;
    refs.micSendersRef.current = [];
    refs.localTracksRef.current = [];

    stopMediaTracks([...senderTracks, ...localTracks]);
    dc?.close();
    pc?.close();

    setConnected(false);
    setConnecting(false);
    if (refs.remoteAudioRef.current) refs.remoteAudioRef.current.srcObject = null;
    setRemoteStream(null);
  }, [refs, setConnected, setConnecting, setRemoteStream]);
  const closePCRef = useRef(closePC);

  useEffect(() => {
    closePCRef.current = closePC;
  }, [closePC]);

  const connect = useCallback(async () => {
    await runConnect(
      refs,
      {
        setConnected,
        setConnecting,
        setRemoteStream,
        showStatus,
        appendLine,
        onDcMessage,
        onDcOpen: () => {
          loadPersistedCards().catch(() => {});
          loadWorkbench().catch(() => {});
        },
        closePC,
      },
      { textOnly: textOnlyRef.current },
    );
  }, [
    appendLine,
    closePC,
    loadPersistedCards,
    loadWorkbench,
    onDcMessage,
    refs,
    setConnected,
    setConnecting,
    setRemoteStream,
    showStatus,
    textOnlyRef,
  ]);

  useEffect(() => {
    if (textOnly || !connected || refs.micSendersRef.current.length > 0) return;
    closePC();
    connect().catch(() => {});
  }, [closePC, connect, connected, refs.micSendersRef, textOnly]);

  useEffect(() => {
    return () => {
      closePCRef.current();
    };
  }, []);

  return { closePC, connect };
}
