import { loadDevicePrivateKey } from '@chat-app/shared/auth';
import { sendEncryptedMessage } from '@chat-app/shared/chat';
import {
  type CallKind,
  type CallSignal,
  fetchLivekitToken,
  signalTopic,
} from '@chat-app/shared/rtc';
import type { RealtimeChannel } from '@supabase/supabase-js';
import {
  ConnectionState,
  type RemoteParticipant,
  type RemoteTrack,
  type RemoteTrackPublication,
  Room,
  RoomEvent,
  Track,
} from 'livekit-client';
import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { useAuth } from './AuthContext';
import { useConversationsContext } from './ConversationsContext';
import { playEndBeep, playJoinBeep, playLeaveBeep } from '../lib/callSounds';
import { notify } from '../lib/osNotify';
import {
  isTauriRuntime,
  registerPttShortcut,
  unregisterPttShortcut,
} from '../lib/globalShortcut';
import { getPttSettings, subscribePttSettings } from '../lib/pttSettings';
import {
  getAudioQualityParams,
  getAudioSettings,
  updateAudioSettings,
} from '../lib/audioSettings';
import {
  createCallE2EE,
  getCallE2EESettings,
  isE2EESupported,
} from '../lib/callE2EE';
import {
  getPresetParams,
  getScreenShareSettings,
} from '../lib/screenShareSettings';
import { devLocalSecretStore } from '../lib/secretStore';
import { supabase } from '../lib/supabase';

export type CallState =
  | { kind: 'idle' }
  | {
      kind: 'outgoing';
      callId: string;
      conversationId: string;
      mediaKind: CallKind;
      ringingSince: string;
    }
  | {
      kind: 'incoming';
      callId: string;
      conversationId: string;
      fromUserId: string;
      mediaKind: CallKind;
    }
  | {
      kind: 'connecting';
      callId: string;
      conversationId: string;
      mediaKind: CallKind;
    }
  | {
      kind: 'connected';
      callId: string;
      conversationId: string;
      mediaKind: CallKind;
      startedAt: string;
    }
  | { kind: 'error'; message: string };

export interface RemoteScreenShare {
  track: RemoteTrack;
  participantId: string;
  participantName: string;
}

// Visual call modes (Discord-style): grid shows all tiles equally, focus pins
// one speaker with others in a strip, fullscreen is cinema mode.
export type CallMode = 'grid' | 'focus' | 'fullscreen';

interface CallContextValue {
  state: CallState;
  room: Room | null;
  remoteParticipants: RemoteParticipant[];
  isMuted: boolean;
  isE2EEActive: boolean;
  isScreenSharing: boolean;
  // Zero or more remote screen shares (LiveKit supports multiple simultaneous).
  remoteScreenShares: RemoteScreenShare[];
  // Remembers the conversation of the last call we left so a sidebar widget
  // can show "still live — rejoin" while peers stay in the room.
  lastCallConversationId: string | null;
  // Clean-Rail UI state — display mode + focused participant id.
  callMode: CallMode;
  focusedId: string | null;
  // Actions:
  startCall: (conversationId: string, mediaKind?: CallKind) => Promise<void>;
  joinActiveCall: (conversationId: string, mediaKind?: CallKind) => Promise<void>;
  acceptIncoming: () => Promise<void>;
  rejectIncoming: () => void;
  hangup: () => Promise<void>;
  toggleMute: () => void;
  toggleScreenShare: () => Promise<void>;
  dismissLastCall: () => void;
  setCallMode: (mode: CallMode) => void;
  setFocusedId: (id: string | null) => void;
  // Runtime mic switch. Persists choice so subsequent calls reuse it, and
  // hot-swaps the input on an active call without a reconnect.
  setAudioInputDevice: (deviceId: string | null) => Promise<void>;
  // Runtime speaker/headphone switch. Persists + applies setSinkId to all
  // currently-attached remote-audio elements.
  setAudioOutputDevice: (deviceId: string | null) => Promise<void>;
}

const CallContext = createContext<CallContextValue | null>(null);

const RING_TIMEOUT_MS = 45_000;
const SOLO_TIMEOUT_MS = 5 * 60_000;

function newCallId(): string {
  return crypto.randomUUID();
}

export function CallProvider({ children }: { children: ReactNode }) {
  const { session, device } = useAuth();
  const { conversations } = useConversationsContext();
  const myId = session?.user.id;

  const [state, setState] = useState<CallState>({ kind: 'idle' });
  const [room, setRoom] = useState<Room | null>(null);
  const [remoteParticipants, setRemoteParticipants] = useState<RemoteParticipant[]>([]);
  const [isMuted, setIsMuted] = useState(false);
  const [isE2EEActive, setIsE2EEActive] = useState(false);
  const [isScreenSharing, setIsScreenSharing] = useState(false);
  const [remoteScreenShares, setRemoteScreenShares] = useState<RemoteScreenShare[]>([]);
  const [lastCallConversationId, setLastCallConversationId] = useState<string | null>(null);
  const [callMode, setCallModeState] = useState<CallMode>('grid');
  const [focusedId, setFocusedIdState] = useState<string | null>(null);

  const signalChannelRef = useRef<RealtimeChannel | null>(null);
  const presenceChannelRef = useRef<RealtimeChannel | null>(null);
  const ringTimerRef = useRef<number | null>(null);
  const soloTimerRef = useRef<number | null>(null);
  const roomRef = useRef<Room | null>(null);
  // Tracks whether the current call was ever in the connected state — needed
  // so hangup/solo-timeout can emit a real duration message vs. "missed".
  const everConnectedRef = useRef<boolean>(false);
  // Peers we invited who haven't answered yet. The outgoing call only goes
  // `declined → idle` once *every* ringer has rejected (not just the first).
  // For 1:1 calls the set has one entry so behaviour is unchanged; for
  // groups, a single reject doesn't terminate the call while others ring.
  const pendingPeersRef = useRef<Set<string>>(new Set());
  const stateRef = useRef<CallState>(state);
  stateRef.current = state;
  // Keep latest conversations accessible from signal-channel closures without
  // re-subscribing the channel on every conversations update.
  const conversationsRef = useRef(conversations);
  conversationsRef.current = conversations;

  // --- Helpers -----------------------------------------------------------

  // Emit a structured call-event message into the conversation so call
  // history shows up in the chat. Caller-side only (to dedupe).
  const emitCallEvent = useCallback(
    async (
      conversationId: string,
      status: 'ended' | 'missed' | 'declined',
      mediaKind: CallKind,
      durationSec: number,
    ) => {
      if (!session?.user.id || !device?.id) return;
      try {
        const priv = await loadDevicePrivateKey(
          devLocalSecretStore,
          session.user.id,
          device.id,
        );
        if (!priv) return;
        const payload = JSON.stringify({
          v: 1,
          type: 'call_event',
          status,
          mediaKind,
          durationSec,
        });
        await sendEncryptedMessage({
          client: supabase,
          conversationId,
          plaintext: payload,
          senderUserId: session.user.id,
          senderDeviceId: device.id,
          senderPrivateKey: priv,
        });
      } catch (err: unknown) {
        console.error('emitCallEvent failed', err);
      }
    },
    [session?.user.id, device?.id],
  );

  const clearRingTimer = useCallback(() => {
    if (ringTimerRef.current !== null) {
      window.clearTimeout(ringTimerRef.current);
      ringTimerRef.current = null;
    }
  }, []);

  const clearSoloTimer = useCallback(() => {
    if (soloTimerRef.current !== null) {
      window.clearTimeout(soloTimerRef.current);
      soloTimerRef.current = null;
    }
  }, []);

  const disconnectRoom = useCallback(async () => {
    const r = roomRef.current;
    if (r) {
      try {
        await r.disconnect();
      } catch {
        /* ignore */
      }
    }
    roomRef.current = null;
    setRoom(null);
    setRemoteParticipants([]);
    setRemoteScreenShares([]);
    setIsScreenSharing(false);
    setIsE2EEActive(false);

    const pres = presenceChannelRef.current;
    if (pres) {
      try {
        await pres.untrack();
      } catch {
        /* ignore */
      }
      // Don't removeChannel — observers (e.g. ActiveCallBanner, RejoinCallBar)
      // on the same topic share this instance via Supabase's topic dedupe.
      // Removing it would break their presence subscription. Untracking is
      // enough: the server drops our presence entry; observers see us leave.
      presenceChannelRef.current = null;
    }
  }, []);

  const sendSignal = useCallback(
    async (toUserId: string, payload: CallSignal) => {
      const ch = supabase.channel(signalTopic(toUserId));
      await ch.subscribe();
      await ch.send({ type: 'broadcast', event: 'signal', payload });
      window.setTimeout(() => {
        void supabase.removeChannel(ch);
      }, 400);
    },
    [],
  );

  const peerUserIdsFor = useCallback(
    (conversationId: string): string[] => {
      const conv = conversations.find((c) => c.id === conversationId);
      if (!conv || !myId) return [];
      return conv.members.filter((m) => m.userId !== myId && m.accepted).map((m) => m.userId);
    },
    [conversations, myId],
  );

  // Transitions to 'connected' once at least one remote participant is in
  // the room. Also cancels the solo-timeout. No-op if already connected.
  const markConnectedIfReady = useCallback(
    (r: Room, conversationId: string, mediaKind: CallKind, callId: string) => {
      const cur = stateRef.current;
      if (cur.kind === 'connected') return;
      if (r.remoteParticipants.size === 0) return;
      clearRingTimer();
      clearSoloTimer();
      everConnectedRef.current = true;
      setState({
        kind: 'connected',
        callId,
        conversationId,
        mediaKind,
        startedAt: new Date().toISOString(),
      });
    },
    [clearRingTimer, clearSoloTimer],
  );

  // --- LiveKit join/leave ------------------------------------------------

  const joinRoom = useCallback(
    async (conversationId: string, mediaKind: CallKind, callId: string) => {
      const { token, url } = await fetchLivekitToken(supabase, conversationId);

      // Discord-style dynamic quality: VP9 + SVC L3T3_KEY emits 3 spatial ×
      // 3 temporal layers from a single encode. LiveKit's congestion control
      // drops temporal/spatial layers per-subscriber when uplink degrades,
      // so a 4K60 publisher downshifts smoothly to 720p30 on weak links
      // without a full renegotiate. `adaptiveStream` pauses video downlink
      // off-screen; `dynacast` stops publishing layers nobody subscribes to;
      // `degradationPreference: balanced` tells WebRTC to trade framerate vs
      // resolution dynamically based on the encoder's CPU + bandwidth.
      // Discord-style dynamic quality: VP9 + SVC L3T3_KEY emits 3 spatial ×
      // 3 temporal layers from a single encode. LiveKit's congestion control
      // drops temporal/spatial layers per-subscriber when uplink degrades,
      // so a 4K60 publisher downshifts smoothly to 720p30 on weak links
      // without a full renegotiate. `adaptiveStream` pauses video downlink
      // off-screen; `dynacast` stops publishing layers nobody subscribes to;
      // `degradationPreference: balanced` tells WebRTC to trade framerate vs
      // resolution dynamically based on the encoder's CPU + bandwidth.
      // Audio: `red` doubles Opus packets for packet-loss concealment;
      // `dtx` is off in hifi so music doesn't get zeroed out between notes;
      // `forceStereo` publishes stereo when the hifi preset captures stereo.
      const ssCfg = getScreenShareSettings();
      const ssParams = getPresetParams(ssCfg.preset);
      const aParams = getAudioQualityParams(getAudioSettings().quality);
      const e2eeCfg = getCallE2EESettings();
      const e2eeUsable = e2eeCfg.enabled && isE2EESupported();
      const e2eeBundle = e2eeUsable ? await createCallE2EE(conversationId) : null;
      const r = new Room({
        adaptiveStream: true,
        dynacast: true,
        publishDefaults: {
          videoCodec: 'vp9',
          scalabilityMode: 'L3T3_KEY',
          degradationPreference: 'balanced',
          backupCodec: true,
          audioPreset: {
            maxBitrate: aParams.bitrateKbps * 1000,
            priority: 'high',
          },
          red: true,
          dtx: aParams.stereo ? false : true,
          forceStereo: aParams.stereo,
          screenShareEncoding: {
            maxBitrate: ssParams.bitrateKbps * 1000,
            maxFramerate: ssParams.framerate,
            priority: 'high',
          },
        },
        ...(e2eeBundle
          ? {
              e2ee: {
                keyProvider: e2eeBundle.keyProvider,
                worker: e2eeBundle.worker,
              },
            }
          : {}),
      });
      roomRef.current = r;
      setRoom(r);

      r.on(RoomEvent.ConnectionStateChanged, (cs) => {
        if (cs === ConnectionState.Disconnected) {
          // Server / network tore us out — reset state cleanly. Remember the
          // conversation so the sidebar "still live — rejoin" widget stays
          // visible: observers will poll presence and detect any remaining
          // peers, which is the only cue this side gets that a call is live.
          clearRingTimer();
          clearSoloTimer();
          const wasInCall =
            stateRef.current.kind === 'connected' ||
            stateRef.current.kind === 'connecting' ||
            stateRef.current.kind === 'outgoing';
          if (wasInCall) {
            setLastCallConversationId(conversationId);
          }
          // Untrack our presence entry so observers see us leave. Keep the
          // channel alive — observers share it via Supabase topic dedupe.
          const pres = presenceChannelRef.current;
          if (pres) {
            try {
              void pres.untrack();
            } catch {
              /* ignore */
            }
            presenceChannelRef.current = null;
          }
          if (stateRef.current.kind !== 'idle' && stateRef.current.kind !== 'error') {
            setState({ kind: 'idle' });
          }
          roomRef.current = null;
          setRoom(null);
          setRemoteParticipants([]);
          setRemoteScreenShares([]);
          setIsScreenSharing(false);
          setIsE2EEActive(false);
        }
      });

      r.on(RoomEvent.ParticipantConnected, () => {
        setRemoteParticipants(Array.from(r.remoteParticipants.values()));
        void playJoinBeep();
        markConnectedIfReady(r, conversationId, mediaKind, callId);
      });

      r.on(RoomEvent.ParticipantDisconnected, () => {
        const remaining = Array.from(r.remoteParticipants.values());
        setRemoteParticipants(remaining);
        void playLeaveBeep();
        // Alone in the room while connected — start the solo-timeout.
        if (
          stateRef.current.kind === 'connected' &&
          remaining.length === 0 &&
          soloTimerRef.current === null
        ) {
          soloTimerRef.current = window.setTimeout(() => {
            void (async () => {
              soloTimerRef.current = null;
              const cur = stateRef.current;
              if (cur.kind === 'connected') {
                const durationSec = Math.max(
                  0,
                  Math.floor((Date.now() - new Date(cur.startedAt).getTime()) / 1000),
                );
                void emitCallEvent(cur.conversationId, 'ended', cur.mediaKind, durationSec);
              }
              await disconnectRoom();
              void playEndBeep();
              // Room was empty by timeout — nothing to rejoin into.
              setLastCallConversationId(null);
              setState({ kind: 'idle' });
            })();
          }, SOLO_TIMEOUT_MS);
        }
      });

      r.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
        attachTrack(track, publication, participant);
        if (
          track.kind === Track.Kind.Video &&
          (track.source === Track.Source.ScreenShare ||
            publication.source === Track.Source.ScreenShare)
        ) {
          const identity = participant.identity;
          const name = participant.name || identity;
          setRemoteScreenShares((prev) => {
            if (prev.some((s) => s.track.sid === track.sid)) return prev;
            return [...prev, { track, participantId: identity, participantName: name }];
          });
        }
      });
      r.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
        detachTrack(track, publication, participant);
        if (track.kind === Track.Kind.Video) {
          setRemoteScreenShares((prev) => prev.filter((s) => s.track.sid !== track.sid));
        }
      });

      // Track my own screen-share state via LocalTrack events so the toggle
      // stays in sync if the user stops sharing via the browser's native UI.
      r.on(RoomEvent.LocalTrackPublished, (publication) => {
        if (publication.source === Track.Source.ScreenShare) {
          setIsScreenSharing(true);
        }
      });
      r.on(RoomEvent.LocalTrackUnpublished, (publication) => {
        if (publication.source === Track.Source.ScreenShare) {
          setIsScreenSharing(false);
        }
      });

      await r.connect(url, token);
      if (e2eeBundle) {
        try {
          await r.setE2EEEnabled(true);
          setIsE2EEActive(true);
        } catch (e2eErr: unknown) {
          console.error('setE2EEEnabled failed — falling back to clear-media', e2eErr);
          setIsE2EEActive(false);
        }
      } else {
        setIsE2EEActive(false);
      }
      try {
        const inputId = getAudioSettings().inputDeviceId;
        await r.localParticipant.setMicrophoneEnabled(true, {
          echoCancellation: aParams.echoCancellation,
          noiseSuppression: aParams.noiseSuppression,
          autoGainControl: aParams.autoGainControl,
          channelCount: aParams.stereo ? 2 : 1,
          sampleRate: aParams.sampleRateHz,
          // Plain string maps to `ideal` — if the device is gone we fall back
          // to OS default instead of throwing NotFoundError.
          ...(inputId ? { deviceId: inputId } : {}),
        });
      } catch (micErr: unknown) {
        console.error('setMicrophoneEnabled failed', micErr);
      }
      if (mediaKind === 'video') {
        try {
          await r.localParticipant.setCameraEnabled(true);
        } catch (camErr: unknown) {
          console.error('setCameraEnabled failed', camErr);
        }
      }
      setIsMuted(false);
      setRemoteParticipants(Array.from(r.remoteParticipants.values()));

      // If peers are already in the room, transition immediately — otherwise
      // stay in outgoing/connecting and wait for ParticipantConnected.
      // Runs BEFORE presence setup so a slow/failing presence channel can't
      // leave the acceptor stuck in "connecting…".
      markConnectedIfReady(r, conversationId, mediaKind, callId);

      // Advertise "we're in this call" on the presence channel so other
      // conversation members can show a Join-button even after leaving.
      // Supabase realtime dedupes channels by topic: if any component (e.g.
      // an observer banner) already subscribed to this topic, we get the
      // existing channel back. In that case .subscribe() would throw — so we
      // branch on channel.state and just track().
      if (myId) {
        const pres = supabase.channel('call-presence:' + conversationId, {
          config: { presence: { key: myId, enabled: true } },
        });
        presenceChannelRef.current = pres;
        const track = () => {
          void pres.track({ userId: myId, joinedAt: new Date().toISOString() });
        };
        if (pres.state === 'joined') {
          track();
        } else if (pres.state === 'closed') {
          pres.subscribe((status) => {
            if (status === 'SUBSCRIBED') track();
          });
        } else {
          // 'joining' — wait briefly for join to complete before tracking.
          const waitId = window.setInterval(() => {
            if (pres.state === 'joined') {
              window.clearInterval(waitId);
              track();
            }
          }, 100);
          window.setTimeout(() => window.clearInterval(waitId), 5000);
        }
      }
    },
    [
      markConnectedIfReady,
      clearRingTimer,
      clearSoloTimer,
      emitCallEvent,
      disconnectRoom,
      myId,
    ],
  );

  // --- Public actions ----------------------------------------------------

  const startCall = useCallback(
    async (conversationId: string, mediaKind: CallKind = 'audio') => {
      if (!myId) return;
      if (stateRef.current.kind !== 'idle') return;

      const callId = newCallId();
      everConnectedRef.current = false;
      setLastCallConversationId(null);
      setState({
        kind: 'outgoing',
        callId,
        conversationId,
        mediaKind,
        ringingSince: new Date().toISOString(),
      });

      const peers = peerUserIdsFor(conversationId);
      pendingPeersRef.current = new Set(peers);
      await Promise.all(
        peers.map((to) =>
          sendSignal(to, {
            type: 'invite',
            callId,
            conversationId,
            fromUserId: myId,
            kind: mediaKind,
            sentAt: new Date().toISOString(),
          }),
        ),
      );

      // Caller joins the room immediately so acceptors hop straight into a
      // live room. Transition to 'connected' happens in ParticipantConnected.
      try {
        await joinRoom(conversationId, mediaKind, callId);
      } catch (err: unknown) {
        setState({
          kind: 'error',
          message: err instanceof Error ? err.message : 'join failed',
        });
        await disconnectRoom();
        return;
      }

      clearRingTimer();
      ringTimerRef.current = window.setTimeout(() => {
        void (async () => {
          // Only trigger if still alone (nobody joined the room yet).
          const cur = stateRef.current;
          if (cur.kind !== 'outgoing' || cur.callId !== callId) return;
          const peers2 = peerUserIdsFor(conversationId);
          await Promise.all(
            peers2.map((to) =>
              sendSignal(to, { type: 'cancel', callId, byUserId: myId }),
            ),
          );
          void emitCallEvent(conversationId, 'missed', mediaKind, 0);
          await disconnectRoom();
          setState({ kind: 'idle' });
        })();
      }, RING_TIMEOUT_MS);
    },
    [myId, peerUserIdsFor, sendSignal, clearRingTimer, emitCallEvent, joinRoom, disconnectRoom],
  );

  // Re-join an already-running call without ringing peers.
  const joinActiveCall = useCallback(
    async (conversationId: string, mediaKind: CallKind = 'audio') => {
      if (!myId) return;
      if (stateRef.current.kind !== 'idle') return;
      const callId = newCallId();
      everConnectedRef.current = false;
      setLastCallConversationId(null);
      setState({ kind: 'connecting', callId, conversationId, mediaKind });
      try {
        await joinRoom(conversationId, mediaKind, callId);
      } catch (err: unknown) {
        setState({
          kind: 'error',
          message: err instanceof Error ? err.message : 'join failed',
        });
        await disconnectRoom();
      }
    },
    [myId, joinRoom, disconnectRoom],
  );

  const acceptIncoming = useCallback(async () => {
    const s = stateRef.current;
    if (s.kind !== 'incoming' || !myId) return;
    const { callId, conversationId, mediaKind } = s;
    everConnectedRef.current = false;
    setLastCallConversationId(null);
    setState({ kind: 'connecting', callId, conversationId, mediaKind });
    try {
      await joinRoom(conversationId, mediaKind, callId);
    } catch (err: unknown) {
      setState({
        kind: 'error',
        message: err instanceof Error ? err.message : 'join failed',
      });
      await disconnectRoom();
    }
  }, [myId, joinRoom, disconnectRoom]);

  const rejectIncoming = useCallback(() => {
    const s = stateRef.current;
    if (s.kind !== 'incoming' || !myId) return;
    void sendSignal(s.fromUserId, { type: 'reject', callId: s.callId, byUserId: myId });
    setState({ kind: 'idle' });
  }, [myId, sendSignal]);

  const hangup = useCallback(async () => {
    const s = stateRef.current;
    clearRingTimer();
    clearSoloTimer();

    if (s.kind === 'outgoing' && myId) {
      // Caller cancelled before anyone picked up — dismiss other sides' rings.
      const peers = peerUserIdsFor(s.conversationId);
      void Promise.all(
        peers.map((to) =>
          sendSignal(to, { type: 'cancel', callId: s.callId, byUserId: myId }),
        ),
      );
      if (!everConnectedRef.current) {
        void emitCallEvent(s.conversationId, 'missed', s.mediaKind, 0);
      }
    }

    let nextLastCallId: string | null = null;
    if (s.kind === 'connected') {
      const r = roomRef.current;
      const amLastOut = !r || r.remoteParticipants.size === 0;
      if (amLastOut) {
        const durationSec = Math.max(
          0,
          Math.floor((Date.now() - new Date(s.startedAt).getTime()) / 1000),
        );
        void emitCallEvent(s.conversationId, 'ended', s.mediaKind, durationSec);
      } else {
        // Peers still in room — keep sidebar rejoin affordance.
        nextLastCallId = s.conversationId;
      }
    }

    // Await disconnect BEFORE dropping state so the tracker presence channel
    // is removed before any RejoinCallBar observer tries to subscribe to the
    // same topic (Supabase realtime dedupes channels by topic).
    await disconnectRoom();
    void playEndBeep();
    setLastCallConversationId(nextLastCallId);
    setState({ kind: 'idle' });
  }, [
    myId,
    peerUserIdsFor,
    sendSignal,
    disconnectRoom,
    clearRingTimer,
    clearSoloTimer,
    emitCallEvent,
  ]);

  const dismissLastCall = useCallback(() => {
    setLastCallConversationId(null);
  }, []);

  const toggleMute = useCallback(() => {
    const r = roomRef.current;
    if (!r) return;
    const lp = r.localParticipant;
    const shouldEnable = !lp.isMicrophoneEnabled;
    void lp.setMicrophoneEnabled(shouldEnable).then(() => {
      setIsMuted(!shouldEnable);
    });
  }, []);

  const toggleScreenShare = useCallback(async () => {
    const r = roomRef.current;
    if (!r) return;
    const lp = r.localParticipant;
    const nextOn = !lp.isScreenShareEnabled;
    try {
      const ssParams = getPresetParams(getScreenShareSettings().preset);
      await lp.setScreenShareEnabled(nextOn, {
        audio: false,
        // Omitting `resolution` lets the browser return native source size —
        // best possible input quality. Fixed presets pass explicit dims so
        // the encoder has a predictable target.
        ...(ssParams.dims
          ? {
              resolution: {
                width: ssParams.dims.width,
                height: ssParams.dims.height,
                frameRate: ssParams.framerate,
              },
            }
          : {}),
        contentHint: 'detail',
      });
      setIsScreenSharing(nextOn);
    } catch (err: unknown) {
      console.error('setScreenShareEnabled failed', err);
      // User cancelled or permission denied — leave state as-is.
    }
  }, []);

  // --- Push-to-talk ------------------------------------------------------
  // While PTT is active + we're in a connected call, the mic is held off
  // except while the configured key is pressed. Under Tauri we also register
  // the key as an OS-level global shortcut so PTT keeps working while the
  // user is focused on another window (Discord-style). The window listener
  // stays as a fallback for the web build and for the case where the global
  // shortcut registration fails (collision with another app).
  useEffect(() => {
    if (state.kind !== 'connected') return;

    let settings = getPttSettings();
    let activeKeyDown = false;
    let globalRegisteredFor: string | null = null;

    const setMic = (on: boolean) => {
      const r = roomRef.current;
      if (!r) return;
      void r.localParticipant.setMicrophoneEnabled(on).then(() => setIsMuted(!on));
    };

    const pressPtt = () => {
      if (activeKeyDown) return;
      activeKeyDown = true;
      setMic(true);
    };
    const releasePtt = () => {
      if (!activeKeyDown) return;
      activeKeyDown = false;
      setMic(false);
    };

    const isTypingTarget = (el: EventTarget | null): boolean => {
      if (!(el instanceof HTMLElement)) return false;
      const tag = el.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA') return true;
      if (el.isContentEditable) return true;
      return false;
    };

    const onKeyDown = (e: KeyboardEvent) => {
      if (!settings.enabled) return;
      if (e.code !== settings.key) return;
      if (isTypingTarget(e.target)) return;
      if (e.repeat) return;
      pressPtt();
    };
    const onKeyUp = (e: KeyboardEvent) => {
      if (!settings.enabled) return;
      if (e.code !== settings.key) return;
      releasePtt();
    };

    const ensureGlobalShortcut = () => {
      if (!isTauriRuntime()) return;
      if (!settings.enabled) {
        if (globalRegisteredFor) {
          const key = globalRegisteredFor;
          globalRegisteredFor = null;
          void unregisterPttShortcut(key);
        }
        return;
      }
      if (globalRegisteredFor === settings.key) return;
      // Hotkey changed — unregister old, register new.
      const oldKey = globalRegisteredFor;
      globalRegisteredFor = settings.key;
      if (oldKey) void unregisterPttShortcut(oldKey);
      void registerPttShortcut(settings.key, pressPtt, releasePtt);
    };

    const applyPttState = () => {
      if (settings.enabled) {
        activeKeyDown = false;
        setMic(false);
      } else {
        setMic(true);
      }
      ensureGlobalShortcut();
    };
    applyPttState();

    const unsubSettings = subscribePttSettings((next) => {
      settings = next;
      applyPttState();
    });

    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);

    return () => {
      unsubSettings();
      window.removeEventListener('keydown', onKeyDown);
      window.removeEventListener('keyup', onKeyUp);
      if (globalRegisteredFor) {
        void unregisterPttShortcut(globalRegisteredFor);
        globalRegisteredFor = null;
      }
    };
  }, [state.kind]);

  // --- Outgoing-channel: listen for accept/reject on our own invite ------
  // and incoming invites from peers.
  useEffect(() => {
    if (!myId) return;
    const me: string = myId; // narrowed capture for the inner closure
    const topic = signalTopic(me);
    const channel = supabase
      .channel(topic, { config: { broadcast: { self: false } } })
      .on('broadcast', { event: 'signal' }, (msg) => {
        const payload = msg.payload as CallSignal;
        handleSignal(payload);
      });
    signalChannelRef.current = channel;
    void channel.subscribe();

    return () => {
      signalChannelRef.current = null;
      void supabase.removeChannel(channel);
    };

    function handleSignal(p: CallSignal) {
      const cur = stateRef.current;
      switch (p.type) {
        case 'invite': {
          if (cur.kind !== 'idle') {
            // Busy — auto-reject.
            void sendSignal(p.fromUserId, {
              type: 'reject',
              callId: p.callId,
              byUserId: me,
            });
            return;
          }
          setState({
            kind: 'incoming',
            callId: p.callId,
            conversationId: p.conversationId,
            fromUserId: p.fromUserId,
            mediaKind: p.kind,
          });
          const conv = conversationsRef.current.find((c) => c.id === p.conversationId) ?? null;
          const callerName =
            conv?.members.find((m) => m.userId === p.fromUserId)?.profile?.displayName ??
            conv?.peer?.displayName ??
            '…';
          const isGroup = conv?.type === 'group';
          void notify({
            title: isGroup
              ? (conv?.name ?? 'Gruppenanruf')
              : 'Eingehender Anruf',
            body: isGroup
              ? callerName + ' ruft die Gruppe'
              : callerName + ' ruft dich an',
          });
          break;
        }
        case 'accept':
        case 'end': {
          // No-op. Participant join/leave is tracked via LiveKit events so
          // calls stay active while at least one person remains in the room.
          break;
        }
        case 'reject': {
          // Only relevant while I'm still the outgoing-caller who hasn't
          // reached the room. Once someone joined (state = connected), a late
          // reject from another ringing peer doesn't end the call.
          if (
            cur.kind !== 'outgoing' ||
            cur.callId !== p.callId ||
            everConnectedRef.current
          ) {
            break;
          }
          // Remove the rejecter from the pending set. For group calls, keep
          // ringing as long as at least one peer hasn't answered — only the
          // last reject collapses the call to `declined`.
          pendingPeersRef.current.delete(p.byUserId);
          if (pendingPeersRef.current.size > 0) break;
          clearRingTimer();
          clearSoloTimer();
          void emitCallEvent(cur.conversationId, 'declined', cur.mediaKind, 0);
          void disconnectRoom();
          setState({ kind: 'idle' });
          break;
        }
        case 'cancel': {
          // Caller cancelled before anyone picked up — receiver dismisses the
          // incoming toast. Only hits when we're still in 'incoming' state.
          if (cur.kind === 'incoming' && cur.callId === p.callId) {
            const conv = conversationsRef.current.find((c) => c.id === cur.conversationId) ?? null;
            const callerName =
              conv?.members.find((m) => m.userId === cur.fromUserId)?.profile?.displayName ??
              conv?.peer?.displayName ??
              '…';
            void notify({
              title: 'Verpasster Anruf',
              body: callerName + ' hat aufgelegt',
            });
            setState({ kind: 'idle' });
          }
          break;
        }
      }
    }
  }, [myId, clearRingTimer, clearSoloTimer, disconnectRoom, sendSignal, emitCallEvent]);

  const setCallMode = useCallback((mode: CallMode) => {
    setCallModeState(mode);
  }, []);

  const setFocusedId = useCallback((id: string | null) => {
    setFocusedIdState(id);
  }, []);

  const setAudioInputDevice = useCallback(async (deviceId: string | null) => {
    updateAudioSettings({ inputDeviceId: deviceId });
    const r = roomRef.current;
    if (!r) return;
    try {
      // LiveKit API: `switchActiveDevice(kind, deviceId)` hot-swaps without a
      // reconnect. Pass empty string or `default` to revert to OS default.
      await r.switchActiveDevice('audioinput', deviceId ?? 'default');
    } catch (err: unknown) {
      console.error('switchActiveDevice(audioinput) failed', err);
    }
  }, []);

  const setAudioOutputDevice = useCallback(async (deviceId: string | null) => {
    updateAudioSettings({ outputDeviceId: deviceId });
    const sinkId = deviceId ?? '';
    // Apply to every <audio> element we've attached to the body. LiveKit's
    // switchActiveDevice only tracks elements it attached itself; our custom
    // appendChild path bypasses that, so we iterate and setSinkId manually.
    const els = document.querySelectorAll<HTMLAudioElement>(
      'audio[data-livekit-track]',
    );
    for (const el of Array.from(els)) {
      if (typeof el.setSinkId !== 'function') continue;
      try {
        await el.setSinkId(sinkId);
      } catch (err: unknown) {
        console.warn('setSinkId on audio element failed', err);
      }
    }
    const r = roomRef.current;
    if (r) {
      try {
        await r.switchActiveDevice('audiooutput', sinkId || 'default');
      } catch (err: unknown) {
        console.warn('switchActiveDevice(audiooutput) failed', err);
      }
    }
  }, []);

  // Reset UI call-mode state when the call leaves any active phase so the next
  // call starts fresh at grid/unfocused.
  useEffect(() => {
    if (state.kind === 'idle' || state.kind === 'error') {
      setCallModeState('grid');
      setFocusedIdState(null);
    }
  }, [state.kind]);

  // Global Esc: drop out of fullscreen cinema back to grid while in an active
  // call. Doesn't hangup and doesn't fire when other dialogs would consume it.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key !== 'Escape') return;
      if (callMode !== 'fullscreen') return;
      if (state.kind !== 'connected' && state.kind !== 'connecting') return;
      setCallModeState('grid');
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [callMode, state.kind]);

  const value = useMemo<CallContextValue>(
    () => ({
      state,
      room,
      remoteParticipants,
      isMuted,
      isE2EEActive,
      isScreenSharing,
      remoteScreenShares,
      lastCallConversationId,
      callMode,
      focusedId,
      startCall,
      joinActiveCall,
      acceptIncoming,
      rejectIncoming,
      hangup,
      toggleMute,
      toggleScreenShare,
      dismissLastCall,
      setCallMode,
      setFocusedId,
      setAudioInputDevice,
      setAudioOutputDevice,
    }),
    [
      state,
      room,
      remoteParticipants,
      isMuted,
      isE2EEActive,
      isScreenSharing,
      remoteScreenShares,
      lastCallConversationId,
      callMode,
      focusedId,
      startCall,
      joinActiveCall,
      acceptIncoming,
      rejectIncoming,
      hangup,
      toggleMute,
      toggleScreenShare,
      dismissLastCall,
      setCallMode,
      setFocusedId,
      setAudioInputDevice,
      setAudioOutputDevice,
    ],
  );

  return <CallContext.Provider value={value}>{children}</CallContext.Provider>;
}

export function useCall(): CallContextValue {
  const ctx = useContext(CallContext);
  if (!ctx) throw new Error('useCall must be used inside <CallProvider>');
  return ctx;
}

function attachTrack(
  track: RemoteTrack,
  _publication: RemoteTrackPublication,
  _participant: RemoteParticipant,
): void {
  if (track.kind === Track.Kind.Audio) {
    const audio = track.attach();
    if (audio instanceof HTMLAudioElement) {
      audio.autoplay = true;
      audio.setAttribute('playsinline', 'true');
      audio.setAttribute('data-livekit-track', track.sid ?? '');
      document.body.appendChild(audio);
      // Apply persisted sinkId so the element routes to the user's chosen
      // speaker/headphone from the start (LiveKit's own `switchActiveDevice`
      // doesn't track custom-appended elements).
      const sinkId = getAudioSettings().outputDeviceId;
      if (sinkId && typeof audio.setSinkId === 'function') {
        void audio.setSinkId(sinkId).catch((err: unknown) => {
          console.warn('setSinkId on attach failed', err);
        });
      }
    }
  }
  // Video is handled later in M2.6/M3 by a dedicated <video> element.
}

function detachTrack(
  track: RemoteTrack,
  _publication: RemoteTrackPublication,
  _participant: RemoteParticipant,
): void {
  if (track.kind === Track.Kind.Audio) {
    const els = track.detach();
    for (const el of els) {
      el.remove();
    }
  }
}

