/**
 * Live quality + speaking metrics for the class room.
 *
 * Both read from data the browser already has (RTCPeerConnection stats and the
 * raw audio tracks), so neither costs a network request.
 */

export type Quality = {
  /** Round-trip time in ms, or null when no peer is connected yet. */
  rttMs: number | null;
  /** Percentage of packets lost, 0-100. */
  lossPct: number | null;
  /** Jitter in ms. */
  jitterMs: number | null;
  /** 0 offline · 1 poor · 2 fair · 3 good */
  level: 0 | 1 | 2 | 3;
  label: string;
};

export const OFFLINE_QUALITY: Quality = {
  rttMs: null, lossPct: null, jitterMs: null, level: 0, label: 'قطع',
};

/** Grade a connection from its RTT and loss. Thresholds tuned for voice. */
export function gradeQuality(rttMs: number | null, lossPct: number | null): Quality {
  if (rttMs === null) return OFFLINE_QUALITY;

  const loss = lossPct ?? 0;
  let level: 0 | 1 | 2 | 3;
  if (rttMs < 150 && loss < 2) level = 3;
  else if (rttMs < 300 && loss < 5) level = 2;
  else level = 1;

  return {
    rttMs,
    lossPct,
    jitterMs: null,
    level,
    label: level === 3 ? 'عالی' : level === 2 ? 'متوسط' : 'ضعیف',
  };
}

/**
 * Aggregate stats across every peer connection.
 * Uses the worst RTT, since the call is only as good as its weakest link.
 */
export async function sampleQuality(peers: Iterable<RTCPeerConnection>): Promise<Quality> {
  let worstRtt: number | null = null;
  let totalLost = 0;
  let totalPackets = 0;
  let jitter: number | null = null;

  for (const peer of peers) {
    if (peer.connectionState !== 'connected') continue;
    let stats: RTCStatsReport;
    try {
      stats = await peer.getStats();
    } catch {
      continue;
    }

    stats.forEach((report: Record<string, unknown> & { type: string }) => {
      if (report.type === 'candidate-pair' && report.state === 'succeeded' && report.nominated) {
        const rtt = report.currentRoundTripTime as number | undefined;
        if (typeof rtt === 'number') {
          const ms = Math.round(rtt * 1000);
          worstRtt = worstRtt === null ? ms : Math.max(worstRtt, ms);
        }
      }
      if (report.type === 'inbound-rtp' && !report.isRemote) {
        const lost = report.packetsLost as number | undefined;
        const received = report.packetsReceived as number | undefined;
        if (typeof lost === 'number') totalLost += lost;
        if (typeof received === 'number') totalPackets += received + (lost ?? 0);
        const j = report.jitter as number | undefined;
        if (typeof j === 'number') jitter = Math.round(j * 1000);
      }
    });
  }

  const lossPct = totalPackets > 0 ? Math.min(100, (totalLost / totalPackets) * 100) : null;
  const quality = gradeQuality(worstRtt, lossPct);
  return { ...quality, jitterMs: jitter };
}

/**
 * Watches an audio track and reports whether it is currently carrying speech.
 * Returns a stop() to release the AudioContext.
 */
export function watchSpeaking(
  stream: MediaStream,
  onChange: (speaking: boolean, level: number) => void,
): () => void {
  if (stream.getAudioTracks().length === 0) return () => {};

  let ctx: AudioContext;
  try {
    ctx = new AudioContext();
  } catch {
    return () => {};
  }

  const source = ctx.createMediaStreamSource(stream);
  const analyser = ctx.createAnalyser();
  analyser.fftSize = 512;
  analyser.smoothingTimeConstant = 0.65;
  source.connect(analyser);

  const buffer = new Uint8Array(analyser.frequencyBinCount);
  let raf = 0;
  let speaking = false;
  // Hold the "speaking" state briefly so natural pauses don't flicker the ring.
  let quietFrames = 0;

  const tick = () => {
    analyser.getByteFrequencyData(buffer);
    let sum = 0;
    for (let i = 0; i < buffer.length; i += 1) sum += buffer[i] * buffer[i];
    const rms = Math.sqrt(sum / buffer.length) / 255;

    const loud = rms > 0.045;
    if (loud) {
      quietFrames = 0;
      if (!speaking) { speaking = true; onChange(true, rms); }
      else onChange(true, rms);
    } else {
      quietFrames += 1;
      if (speaking && quietFrames > 18) { speaking = false; onChange(false, 0); }
    }
    raf = requestAnimationFrame(tick);
  };
  raf = requestAnimationFrame(tick);

  return () => {
    cancelAnimationFrame(raf);
    try { source.disconnect(); analyser.disconnect(); void ctx.close(); } catch { /* already closed */ }
  };
}
