﻿import { AudioSession } from '@livekit/react-native';
import { Room, RoomEvent, Track } from 'livekit-client';
import { chat, rtc } from '@chat-app/shared';
import type { CallSignal } from '@chat-app/shared/rtc';
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { useAuth } from './authContext';
import { sendCallSignal, subscribeCallSignals } from './callSignal';
import { supabase } from './supabase';

type Identity = string;

export interface RemoteParticipantSummary {
  identity: Identity;
  name: string;
  speaking: boolean;
}

export type CallState =
  | { kind: 'idle' }
  | { kind: 'outgoing'; callId: string; conversationId: string; peers: Identity[] }
  | { kind: 'incoming'; callId: string; conversationId: string; fromUserId: Identity }
  | { kind: 'connecting'; callId: string; conversationId: string }
  | {
      kind: 'connected';
      callId: string;
      conversationId: string;
      muted: boolean;
      speakerOn: boolean;
      participants: RemoteParticipantSummary[];
    }
  | { kind: 'ended'; reason: 'normal' | 'rejected' | 'cancelled' | 'error' };

interface CallContextValue {
  state: CallState;
  startCall: (conversationId: string) => Promise<void>;
  acceptIncoming: () => Promise<void>;
  rejectIncoming: () => Promise<void>;
  cancelOutgoing: () => Promise<void>;
  endCall: () => Promise<void>;
  toggleMute: () => Promise<void>;
  toggleSpeaker: () => Promise<void>;
}

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

export function useCall(): CallContextValue {
  const v = useContext(Ctx);
  if (!v) throw new Error('useCall() called outside <CallProvider>');
  return v;
}

function randomCallId(): string {
  return 'call-' + Math.random().toString(36).slice(2, 10) + '-' + Date.now().toString(36);
}

// Time the call state lingers in `ended` so the UI can render a transient
// status pill ("Anruf abgelehnt", "Anruf beendet") before snapping back
// to idle. Keep this comfortably above the user's reaction time but short
// enough that returning to a chat feels snappy.
const ENDED_STATE_LINGER_MS = 2500;

export function CallProvider({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  const myUserId = user?.id ?? null;
  const [state, setState] = useState<CallState>({ kind: 'idle' });
  const roomRef = useRef<Room | null>(null);
  const participantsRef = useRef<Map<Identity, RemoteParticipantSummary>>(new Map());

  const peerIdsFor = useCallback(
    async (conversationId: string): Promise<Identity[]> => {
      if (!myUserId) return [];
      const all = await chat.listConversations(supabase);
      const conv = all.find((c) => c.id === conversationId);
      if (!conv) return [];
      return conv.members.map((m) => m.userId).filter((id) => id !== myUserId);
    },
    [myUserId],
  );

  const teardownRoom = useCallback(async () => {
    const r = roomRef.current;
    roomRef.current = null;
    participantsRef.current.clear();
    if (r) {
      try {
        await r.disconnect();
      } catch (err) {
        console.warn('[call] room.disconnect failed', err);
      }
    }
    try {
      await AudioSession.stopAudioSession();
    } catch {
      /* already stopped */
    }
  }, []);

  const updateParticipantsState = useCallback(() => {
    setState((prev) => {
      if (prev.kind !== 'connected') return prev;
      return {
        ...prev,
        participants: Array.from(participantsRef.current.values()),
      };
    });
  }, []);

  const joinRoom = useCallback(
    async (conversationId: string, callId: string): Promise<void> => {
      const token = await rtc.fetchLivekitToken(supabase, conversationId);
      await AudioSession.startAudioSession();
      const room = new Room();
      roomRef.current = room;

      room
        .on(RoomEvent.ParticipantConnected, (p) => {
          participantsRef.current.set(p.identity, {
            identity: p.identity,
            name: p.name || p.identity,
            speaking: p.isSpeaking,
          });
          updateParticipantsState();
        })
        .on(RoomEvent.ParticipantDisconnected, (p) => {
          participantsRef.current.delete(p.identity);
          updateParticipantsState();
        })
        .on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
          for (const [id, entry] of participantsRef.current) {
            entry.speaking = speakers.some((sp) => sp.identity === id);
            participantsRef.current.set(id, entry);
          }
          updateParticipantsState();
        });

      await room.connect(token.url, token.token);
      await room.localParticipant.setMicrophoneEnabled(true);

      for (const p of room.remoteParticipants.values()) {
        participantsRef.current.set(p.identity, {
          identity: p.identity,
          name: p.name || p.identity,
          speaking: p.isSpeaking,
        });
      }

      setState({
        kind: 'connected',
        callId,
        conversationId,
        muted: false,
        speakerOn: false,
        participants: Array.from(participantsRef.current.values()),
      });
    },
    [updateParticipantsState],
  );

  // Signal subscription. Routes incoming invites / cancels / rejects to
  // state transitions. eslint-disable on deps because handleIncomingSignal
  // is defined inline below; re-subscribing on every render would churn
  // the realtime channel.
  useEffect(() => {
    if (!myUserId) return;
    const unsub = subscribeCallSignals(supabase, myUserId, (sig) => {
      handleIncomingSignal(sig);
    });
    return unsub;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [myUserId]);

  function handleIncomingSignal(sig: CallSignal) {
    setState((prev) => {
      switch (sig.type) {
        case 'invite':
          if (prev.kind === 'idle') {
            return {
              kind: 'incoming',
              callId: sig.callId,
              conversationId: sig.conversationId,
              fromUserId: sig.fromUserId,
            };
          }
          return prev;
        case 'cancel':
          if (
            (prev.kind === 'incoming' || prev.kind === 'connecting') &&
            prev.callId === sig.callId
          ) {
            return { kind: 'ended', reason: 'cancelled' };
          }
          return prev;
        case 'reject':
          if (prev.kind === 'outgoing' && prev.callId === sig.callId) {
            void teardownRoom();
            return { kind: 'ended', reason: 'rejected' };
          }
          return prev;
        case 'accept':
          return prev;
        case 'end':
          if (
            (prev.kind === 'connected' || prev.kind === 'connecting') &&
            prev.callId === sig.callId
          ) {
            void teardownRoom();
            return { kind: 'ended', reason: 'normal' };
          }
          return prev;
      }
    });
  }

  const startCall = useCallback(
    async (conversationId: string): Promise<void> => {
      if (!myUserId) throw new Error('not authenticated');
      const callId = randomCallId();
      const peers = await peerIdsFor(conversationId);
      setState({ kind: 'outgoing', callId, conversationId, peers });
      try {
        await Promise.all(
          peers.map((peerId) =>
            sendCallSignal(supabase, peerId, {
              type: 'invite',
              callId,
              conversationId,
              fromUserId: myUserId,
              kind: 'audio',
              sentAt: new Date().toISOString(),
            }),
          ),
        );
        await joinRoom(conversationId, callId);
      } catch (err) {
        console.warn('[call] startCall failed', err);
        await teardownRoom();
        setState({ kind: 'ended', reason: 'error' });
      }
    },
    [myUserId, peerIdsFor, joinRoom, teardownRoom],
  );

  const acceptIncoming = useCallback(async (): Promise<void> => {
    if (!myUserId) return;
    if (state.kind !== 'incoming') return;
    const { callId, conversationId, fromUserId } = state;
    setState({ kind: 'connecting', callId, conversationId });
    try {
      await sendCallSignal(supabase, fromUserId, {
        type: 'accept',
        callId,
        byUserId: myUserId,
      });
      await joinRoom(conversationId, callId);
    } catch (err) {
      console.warn('[call] acceptIncoming failed', err);
      await teardownRoom();
      setState({ kind: 'ended', reason: 'error' });
    }
  }, [myUserId, state, joinRoom, teardownRoom]);

  const rejectIncoming = useCallback(async (): Promise<void> => {
    if (!myUserId) return;
    if (state.kind !== 'incoming') return;
    const { callId, fromUserId } = state;
    setState({ kind: 'ended', reason: 'rejected' });
    try {
      await sendCallSignal(supabase, fromUserId, {
        type: 'reject',
        callId,
        byUserId: myUserId,
      });
    } catch (err) {
      console.warn('[call] rejectIncoming send failed', err);
    }
  }, [myUserId, state]);

  const cancelOutgoing = useCallback(async (): Promise<void> => {
    if (!myUserId) return;
    if (state.kind !== 'outgoing') return;
    const { callId, peers } = state;
    setState({ kind: 'ended', reason: 'cancelled' });
    try {
      await Promise.all(
        peers.map((peerId) =>
          sendCallSignal(supabase, peerId, {
            type: 'cancel',
            callId,
            byUserId: myUserId,
          }),
        ),
      );
    } catch (err) {
      console.warn('[call] cancelOutgoing send failed', err);
    }
    await teardownRoom();
  }, [myUserId, state, teardownRoom]);

  const endCall = useCallback(async (): Promise<void> => {
    if (!myUserId) return;
    if (state.kind !== 'connected') {
      await teardownRoom();
      setState({ kind: 'idle' });
      return;
    }
    const { callId, conversationId } = state;
    const peers = await peerIdsFor(conversationId);
    setState({ kind: 'ended', reason: 'normal' });
    try {
      await Promise.all(
        peers.map((peerId) =>
          sendCallSignal(supabase, peerId, {
            type: 'end',
            callId,
            byUserId: myUserId,
          }),
        ),
      );
    } catch (err) {
      console.warn('[call] endCall send failed', err);
    }
    await teardownRoom();
  }, [myUserId, state, peerIdsFor, teardownRoom]);

  const toggleMute = useCallback(async (): Promise<void> => {
    if (state.kind !== 'connected') return;
    const r = roomRef.current;
    if (!r) return;
    const nextMuted = !state.muted;
    try {
      await r.localParticipant.setMicrophoneEnabled(!nextMuted);
      setState((prev) =>
        prev.kind === 'connected' ? { ...prev, muted: nextMuted } : prev,
      );
    } catch (err) {
      console.warn('[call] toggleMute failed', err);
    }
  }, [state]);

  const toggleSpeaker = useCallback(async (): Promise<void> => {
    if (state.kind !== 'connected') return;
    const nextSpeaker = !state.speakerOn;
    try {
      await AudioSession.selectAudioOutput(nextSpeaker ? 'speaker' : 'earpiece');
      setState((prev) =>
        prev.kind === 'connected' ? { ...prev, speakerOn: nextSpeaker } : prev,
      );
    } catch (err) {
      console.warn('[call] toggleSpeaker failed', err);
    }
  }, [state]);

  // Drift `ended` back to `idle` after the linger window so the UI can
  // show a brief status pill before snapping back.
  useEffect(() => {
    if (state.kind !== 'ended') return;
    const t = setTimeout(() => {
      setState({ kind: 'idle' });
    }, ENDED_STATE_LINGER_MS);
    return () => clearTimeout(t);
  }, [state]);

  const value: CallContextValue = useMemo(
    () => ({
      state,
      startCall,
      acceptIncoming,
      rejectIncoming,
      cancelOutgoing,
      endCall,
      toggleMute,
      toggleSpeaker,
    }),
    [state, startCall, acceptIncoming, rejectIncoming, cancelOutgoing, endCall, toggleMute, toggleSpeaker],
  );

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

export { Track };
