"use client";

/**
 * LiveKit implementation of CallSession.
 *
 * Signalling, ICE, and SFU routing all live in the LiveKit server — this
 * file only maps Room events onto the same participant/stream surface the
 * overlay already consumes. Mesh (webrtc.ts) remains the fallback when the
 * API returns configured:false.
 */

import {
  ConnectionQuality,
  Room,
  RoomEvent,
  type LocalTrack,
  type RemoteParticipant,
  type RemoteTrack,
  type RemoteTrackPublication,
} from "livekit-client";
import type { CallDataEvent, CallQuality, CallSession, CallSessionOptions, PeerState } from "./webrtc";

function qualityFrom(room: Room): CallQuality {
  const local = room.localParticipant.connectionQuality;
  const label: CallQuality["label"] =
    local === ConnectionQuality.Excellent
      ? "excellent"
      : local === ConnectionQuality.Good
        ? "good"
        : local === ConnectionQuality.Lost
          ? "lost"
          : "poor";
  return { label, packetLoss: null, rttMs: null };
}

function identityToUserId(identity: string): number {
  if (identity.startsWith("sip:")) return -Math.abs(hashId(identity));
  const n = Number(identity);
  return Number.isInteger(n) && n > 0 ? n : -Math.abs(hashId(identity));
}

function hashId(value: string): number {
  let h = 0;
  for (let i = 0; i < value.length; i++) h = (h * 31 + value.charCodeAt(i)) | 0;
  return h || 1;
}

function streamOf(tracks: Array<MediaStreamTrack | undefined>): MediaStream | null {
  const live = tracks.filter((track): track is MediaStreamTrack => Boolean(track));
  return live.length > 0 ? new MediaStream(live) : null;
}

function localStream(room: Room): MediaStream | null {
  const pubs = [...room.localParticipant.trackPublications.values()];
  return streamOf(pubs.map((pub) => pub.track?.mediaStreamTrack));
}

function remoteStream(participant: RemoteParticipant): MediaStream | null {
  const pubs = [...participant.trackPublications.values()];
  return streamOf(pubs.map((pub) => pub.track?.mediaStreamTrack));
}

function remoteConnection(participant: RemoteParticipant): PeerState["connection"] {
  if (participant.connectionQuality === ConnectionQuality.Lost) return "failed";
  const has = [...participant.trackPublications.values()].some((pub) => pub.track);
  return has ? "connected" : "connecting";
}

export async function createLiveKitCallSession(options: CallSessionOptions): Promise<CallSession> {
  const { selfUserId, video, livekit, onState, onLocalStream, onError, onData } = options;
  if (!livekit) throw new Error("LiveKit session requires url and token");

  const room = new Room({
    adaptiveStream: true,
    dynacast: true,
    publishDefaults: { simulcast: true },
  });
  let closed = false;

  function emit() {
    if (closed) return;
    const peers: PeerState[] = [...room.remoteParticipants.values()]
      .map((participant) => ({
        userId: identityToUserId(participant.identity),
        stream: remoteStream(participant),
        connection: remoteConnection(participant),
        level: participant.audioLevel,
        sawRelay: true,
      }))
      .sort((a, b) => a.userId - b.userId);
    onState({
      peers,
      localLevel: room.localParticipant.audioLevel,
      quality: qualityFrom(room),
    });
    onLocalStream(localStream(room));
  }

  function publishData(event: CallDataEvent) {
    const payload = new TextEncoder().encode(JSON.stringify(event));
    void room.localParticipant.publishData(payload, { reliable: true });
  }

  room.on(RoomEvent.TrackSubscribed, (_track: RemoteTrack, _pub: RemoteTrackPublication) => emit());
  room.on(RoomEvent.TrackUnsubscribed, () => emit());
  room.on(RoomEvent.TrackMuted, () => emit());
  room.on(RoomEvent.TrackUnmuted, () => emit());
  room.on(RoomEvent.ParticipantConnected, () => emit());
  room.on(RoomEvent.ParticipantDisconnected, () => emit());
  room.on(RoomEvent.ActiveSpeakersChanged, () => emit());
  room.on(RoomEvent.ConnectionQualityChanged, () => emit());
  room.on(RoomEvent.LocalTrackPublished, () => emit());
  room.on(RoomEvent.LocalTrackUnpublished, () => emit());
  room.on(RoomEvent.Disconnected, () => {
    if (!closed) onError(new Error("Disconnected from the media server"));
  });
  room.on(RoomEvent.DataReceived, (payload, participant) => {
    if (!participant) return;
    try {
      const parsed = JSON.parse(new TextDecoder().decode(payload)) as CallDataEvent;
      onData?.({
        fromUserId: identityToUserId(participant.identity),
        type: parsed.type === "chat" ? "chat" : "reaction",
        emoji: parsed.emoji,
        text: parsed.text,
      });
    } catch {
      // Ignore malformed in-call payloads.
    }
  });

  try {
    await room.connect(livekit.url, livekit.token, { websocketTimeout: 5_000 });
    await room.localParticipant.setMicrophoneEnabled(true);
    if (video) await room.localParticipant.setCameraEnabled(true);
  } catch (err) {
    room.disconnect();
    throw err instanceof Error ? err : new Error(String(err));
  }

  emit();

  return {
    transport: "livekit",
    setPeers() {
      emit();
    },
    handleSignal() {},
    announceReady() {},
    setMicEnabled(enabled: boolean) {
      void room.localParticipant
        .setMicrophoneEnabled(enabled)
        .then(emit)
        .catch((err) => onError(err instanceof Error ? err : new Error(String(err))));
    },
    setCameraEnabled(enabled: boolean) {
      void room.localParticipant
        .setCameraEnabled(enabled)
        .then(emit)
        .catch((err) => onError(err instanceof Error ? err : new Error(String(err))));
    },
    async startScreenShare() {
      try {
        await room.localParticipant.setScreenShareEnabled(true);
        emit();
        return true;
      } catch {
        return false;
      }
    },
    async stopScreenShare() {
      await room.localParticipant.setScreenShareEnabled(false);
      emit();
    },
    sendReaction(emoji: string) {
      const event: CallDataEvent = { fromUserId: selfUserId, type: "reaction", emoji };
      publishData(event);
      onData?.(event);
    },
    sendChat(text: string) {
      const event: CallDataEvent = { fromUserId: selfUserId, type: "chat", text };
      publishData(event);
      onData?.(event);
    },
    async switchAudioDevice(deviceId: string) {
      await room.switchActiveDevice("audioinput", deviceId);
    },
    async switchVideoDevice(deviceId: string) {
      await room.switchActiveDevice("videoinput", deviceId);
    },
    resumeAudio() {
      for (const participant of room.remoteParticipants.values()) {
        for (const pub of participant.audioTrackPublications.values()) {
          const el = pub.track?.attach();
          void (el as HTMLMediaElement | undefined)?.play?.().catch(() => undefined);
        }
      }
    },
    close() {
      if (closed) return;
      closed = true;
      for (const pub of room.localParticipant.trackPublications.values()) {
        (pub.track as LocalTrack | undefined)?.stop();
      }
      room.disconnect();
      onLocalStream(null);
    },
  };
}
