// Per-participant call-stats polling via WebRTC's getStats() API.
// Used by the Discord-style debug overlay (Ctrl+Shift+S). Computes bitrate
// deltas across two consecutive samples so the displayed kbps tracks the
// live stream rather than the cumulative byte count.

import type { Participant, RemoteParticipant, Room } from 'livekit-client';

interface TrackWithStats {
  getRTCStatsReport?: () => Promise<RTCStatsReport | undefined>;
}

export interface ParticipantTrackStats {
  audioInKbps?: number;
  audioOutKbps?: number;
  videoInKbps?: number;
  videoOutKbps?: number;
  packetLossPct?: number;
  jitterMs?: number;
  rttMs?: number;
}

export interface ParticipantStats {
  identity: string;
  isLocal: boolean;
  audio: ParticipantTrackStats;
  video: ParticipantTrackStats;
}

interface ByteSample {
  bytes: number;
  timeMs: number;
}

interface SampleCache {
  // key = `${identity}/${kind}/${dir}` — kind in {audio,video}, dir in {in,out}
  bytes: Map<string, ByteSample>;
  // packets cumulative for loss-pct delta calc.
  pkts: Map<string, { recv: number; lost: number }>;
}

export function makeSampleCache(): SampleCache {
  return { bytes: new Map(), pkts: new Map() };
}

/** Pull a single stats sample from every participant in the room. Returns
 *  one ParticipantStats entry per participant (local + remote). bitrates are
 *  computed against the previous sample stored in `cache`, so the first
 *  call returns 0 kbps everywhere — call again 1s later for real numbers. */
export async function sampleStats(
  room: Room,
  cache: SampleCache,
): Promise<ParticipantStats[]> {
  const out: ParticipantStats[] = [];

  const localStats = await collectForParticipant(
    room.localParticipant,
    true,
    cache,
  );
  out.push(localStats);

  for (const rp of room.remoteParticipants.values()) {
    const s = await collectForParticipant(rp, false, cache);
    out.push(s);
  }

  return out;
}

async function collectForParticipant(
  participant: Participant,
  isLocal: boolean,
  cache: SampleCache,
): Promise<ParticipantStats> {
  const audio: ParticipantTrackStats = {};
  const video: ParticipantTrackStats = {};

  // Local participant publishes — pull outbound-rtp from audio + video sender.
  if (isLocal) {
    for (const pub of participant.audioTrackPublications.values()) {
      const track = pub.track;
      if (!track) continue;
      const stats = await safeGetStats(track);
      if (stats) {
        const k = participant.identity + '/audio/out';
        const r = readOutbound(stats);
        audio.audioOutKbps = bitrateKbps(cache.bytes, k, r.bytes);
      }
    }
    for (const pub of participant.videoTrackPublications.values()) {
      const track = pub.track;
      if (!track) continue;
      const stats = await safeGetStats(track);
      if (stats) {
        const k = participant.identity + '/video/out';
        const r = readOutbound(stats);
        video.videoOutKbps = bitrateKbps(cache.bytes, k, r.bytes);
      }
    }
  } else {
    // Remote participant — pull inbound-rtp from each subscribed track.
    const rp = participant as RemoteParticipant;
    for (const pub of rp.audioTrackPublications.values()) {
      const track = pub.track;
      if (!track) continue;
      const stats = await safeGetStats(track);
      if (stats) {
        const k = participant.identity + '/audio/in';
        const r = readInbound(stats);
        audio.audioInKbps = bitrateKbps(cache.bytes, k, r.bytes);
        const lossPct = computeLossPct(cache.pkts, k, r.recv, r.lost);
        if (lossPct !== undefined) audio.packetLossPct = lossPct;
        if (r.jitter !== undefined) audio.jitterMs = Math.round(r.jitter * 1000);
        if (r.rttMs !== undefined) audio.rttMs = r.rttMs;
      }
    }
    for (const pub of rp.videoTrackPublications.values()) {
      const track = pub.track;
      if (!track) continue;
      const stats = await safeGetStats(track);
      if (stats) {
        const k = participant.identity + '/video/in';
        const r = readInbound(stats);
        video.videoInKbps = bitrateKbps(cache.bytes, k, r.bytes);
        const lossPct = computeLossPct(cache.pkts, k, r.recv, r.lost);
        if (lossPct !== undefined) video.packetLossPct = lossPct;
        if (r.jitter !== undefined) video.jitterMs = Math.round(r.jitter * 1000);
        if (r.rttMs !== undefined) video.rttMs = r.rttMs;
      }
    }
  }

  return { identity: participant.identity, isLocal, audio, video };
}

async function safeGetStats(track: unknown): Promise<RTCStatsReport | null> {
  const t = track as TrackWithStats;
  if (typeof t.getRTCStatsReport !== 'function') return null;
  try {
    const stats = await t.getRTCStatsReport();
    return stats ?? null;
  } catch {
    return null;
  }
}

interface InboundRead {
  bytes: number;
  recv: number;
  lost: number;
  jitter?: number;
  rttMs?: number;
}

function readInbound(stats: RTCStatsReport): InboundRead {
  let bytes = 0;
  let recv = 0;
  let lost = 0;
  let jitter: number | undefined;
  let rttMs: number | undefined;
  stats.forEach((report: { type?: string; [key: string]: unknown }) => {
    if (report.type === 'inbound-rtp') {
      const r = report as unknown as {
        bytesReceived?: number;
        packetsReceived?: number;
        packetsLost?: number;
        jitter?: number;
      };
      bytes += r.bytesReceived ?? 0;
      recv += r.packetsReceived ?? 0;
      lost += r.packetsLost ?? 0;
      if (r.jitter !== undefined) jitter = r.jitter;
    }
    if (report.type === 'remote-inbound-rtp') {
      const r = report as unknown as { roundTripTime?: number };
      if (r.roundTripTime !== undefined) {
        rttMs = Math.round(r.roundTripTime * 1000);
      }
    }
    if (report.type === 'candidate-pair') {
      const r = report as unknown as {
        nominated?: boolean;
        currentRoundTripTime?: number;
      };
      if (r.nominated && r.currentRoundTripTime !== undefined && rttMs === undefined) {
        rttMs = Math.round(r.currentRoundTripTime * 1000);
      }
    }
  });
  const result: InboundRead = { bytes, recv, lost };
  if (jitter !== undefined) result.jitter = jitter;
  if (rttMs !== undefined) result.rttMs = rttMs;
  return result;
}

function readOutbound(stats: RTCStatsReport): { bytes: number } {
  let bytes = 0;
  stats.forEach((report: { type?: string; [key: string]: unknown }) => {
    if (report.type === 'outbound-rtp') {
      const r = report as unknown as { bytesSent?: number };
      bytes += r.bytesSent ?? 0;
    }
  });
  return { bytes };
}

function bitrateKbps(
  cache: Map<string, ByteSample>,
  key: string,
  bytes: number,
): number {
  const now = performance.now();
  const prev = cache.get(key);
  cache.set(key, { bytes, timeMs: now });
  if (!prev) return 0;
  const dtSec = (now - prev.timeMs) / 1000;
  if (dtSec <= 0) return 0;
  const dBytes = Math.max(0, bytes - prev.bytes);
  // bytes -> bits -> kbps.
  return Math.round((dBytes * 8) / 1000 / dtSec);
}

function computeLossPct(
  cache: Map<string, { recv: number; lost: number }>,
  key: string,
  recv: number,
  lost: number,
): number | undefined {
  const prev = cache.get(key);
  cache.set(key, { recv, lost });
  if (!prev) return undefined;
  const dRecv = Math.max(0, recv - prev.recv);
  const dLost = Math.max(0, lost - prev.lost);
  const total = dRecv + dLost;
  if (total <= 0) return 0;
  return Math.round((dLost / total) * 100 * 10) / 10;
}
