import { useCallback, useEffect, useRef, useState } from 'react';

import { getAudioSettings, subscribeAudioSettings } from '../lib/audioSettings';

type Phase = 'idle' | 'live' | 'recording' | 'playback';

const RECORD_DURATION_MS = 4000;

/**
 * Discord-style "Let's Check" mic test panel. Two parts:
 * 1. Live bargraph driven by AnalyserNode on the user's selected mic so the
 *    user sees their RMS in real time and can sanity-check the
 *    voice-threshold slider above.
 * 2. Mic-Test button: records ~4s into a MediaRecorder, then plays it back
 *    so the user hears exactly what their peers will hear.
 *
 * Resources are released aggressively — every state change tears down the
 * audio graph + media stream so the camera/mic light is honest about
 * activity.
 */
export function MicTestSection() {
  const [phase, setPhase] = useState<Phase>('idle');
  const [level, setLevel] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [threshold, setThreshold] = useState<number>(
    () => getAudioSettings().voiceThreshold,
  );
  const audioContextRef = useRef<AudioContext | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const analyserRef = useRef<AnalyserNode | null>(null);
  const rafRef = useRef<number | null>(null);
  const recorderRef = useRef<MediaRecorder | null>(null);
  const chunksRef = useRef<Blob[]>([]);
  const playbackUrlRef = useRef<string | null>(null);
  const playbackElRef = useRef<HTMLAudioElement | null>(null);

  // Pull the threshold live so the bargraph's threshold marker updates
  // while the user is dragging the slider above.
  useEffect(() => {
    return subscribeAudioSettings((s) => setThreshold(s.voiceThreshold));
  }, []);

  const teardownGraph = useCallback(() => {
    if (rafRef.current !== null) {
      cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    }
    const stream = streamRef.current;
    if (stream) {
      stream.getTracks().forEach((t) => t.stop());
      streamRef.current = null;
    }
    const ctx = audioContextRef.current;
    if (ctx) {
      void ctx.close().catch(() => undefined);
      audioContextRef.current = null;
    }
    analyserRef.current = null;
  }, []);

  const teardownPlayback = useCallback(() => {
    const el = playbackElRef.current;
    if (el) {
      el.pause();
      el.src = '';
      playbackElRef.current = null;
    }
    const url = playbackUrlRef.current;
    if (url) {
      URL.revokeObjectURL(url);
      playbackUrlRef.current = null;
    }
    chunksRef.current = [];
  }, []);

  // Final cleanup on unmount.
  useEffect(() => {
    return () => {
      teardownGraph();
      teardownPlayback();
    };
  }, [teardownGraph, teardownPlayback]);

  const acquireMic = useCallback(async (): Promise<MediaStream> => {
    const settings = getAudioSettings();
    const constraints: MediaStreamConstraints = {
      audio: settings.inputDeviceId
        ? { deviceId: { exact: settings.inputDeviceId } }
        : true,
    };
    return navigator.mediaDevices.getUserMedia(constraints);
  }, []);

  const startLive = useCallback(async () => {
    setError(null);
    teardownPlayback();
    teardownGraph();
    try {
      const stream = await acquireMic();
      streamRef.current = stream;
      const AudioCtx =
        window.AudioContext ??
        (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
      if (!AudioCtx) throw new Error('AudioContext nicht verfügbar');
      const ctx = new AudioCtx();
      audioContextRef.current = ctx;
      const src = ctx.createMediaStreamSource(stream);
      const analyser = ctx.createAnalyser();
      analyser.fftSize = 1024;
      src.connect(analyser);
      analyserRef.current = analyser;

      const buffer = new Float32Array(analyser.fftSize);
      const tick = () => {
        const a = analyserRef.current;
        if (!a) return;
        a.getFloatTimeDomainData(buffer);
        let sum = 0;
        for (let i = 0; i < buffer.length; i++) {
          const s = buffer[i] ?? 0;
          sum += s * s;
        }
        const rms = Math.sqrt(sum / buffer.length);
        setLevel(rms);
        rafRef.current = requestAnimationFrame(tick);
      };
      rafRef.current = requestAnimationFrame(tick);
      setPhase('live');
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'Mikrofon nicht verfügbar');
      setPhase('idle');
    }
  }, [acquireMic, teardownGraph, teardownPlayback]);

  const stopAll = useCallback(() => {
    const rec = recorderRef.current;
    if (rec && rec.state !== 'inactive') {
      try {
        rec.stop();
      } catch {
        /* ignore */
      }
    }
    recorderRef.current = null;
    teardownGraph();
    teardownPlayback();
    setLevel(0);
    setPhase('idle');
  }, [teardownGraph, teardownPlayback]);

  const startTest = useCallback(async () => {
    setError(null);
    teardownPlayback();
    teardownGraph();
    try {
      const stream = await acquireMic();
      streamRef.current = stream;
      // Build the same analyser graph as live so the bargraph stays alive
      // while recording — gives the user feedback that they're being heard.
      const AudioCtx =
        window.AudioContext ??
        (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
      if (AudioCtx) {
        const ctx = new AudioCtx();
        audioContextRef.current = ctx;
        const src = ctx.createMediaStreamSource(stream);
        const analyser = ctx.createAnalyser();
        analyser.fftSize = 1024;
        src.connect(analyser);
        analyserRef.current = analyser;
        const buf = new Float32Array(analyser.fftSize);
        const tick = () => {
          const a = analyserRef.current;
          if (!a) return;
          a.getFloatTimeDomainData(buf);
          let sum = 0;
          for (let i = 0; i < buf.length; i++) {
            const s = buf[i] ?? 0;
            sum += s * s;
          }
          setLevel(Math.sqrt(sum / buf.length));
          rafRef.current = requestAnimationFrame(tick);
        };
        rafRef.current = requestAnimationFrame(tick);
      }
      const rec = new MediaRecorder(stream);
      recorderRef.current = rec;
      chunksRef.current = [];
      rec.ondataavailable = (e) => {
        if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
      };
      rec.onstop = () => {
        // Stop the analyser/stream first so the mic light goes off while the
        // playback-only phase is active. Keep playback on its own element.
        if (rafRef.current !== null) {
          cancelAnimationFrame(rafRef.current);
          rafRef.current = null;
        }
        const stream2 = streamRef.current;
        if (stream2) {
          stream2.getTracks().forEach((t) => t.stop());
          streamRef.current = null;
        }
        const ctx2 = audioContextRef.current;
        if (ctx2) {
          void ctx2.close().catch(() => undefined);
          audioContextRef.current = null;
        }
        analyserRef.current = null;
        setLevel(0);

        const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
        const url = URL.createObjectURL(blob);
        playbackUrlRef.current = url;
        const el = new Audio(url);
        playbackElRef.current = el;
        el.onended = () => {
          teardownPlayback();
          setPhase('idle');
        };
        setPhase('playback');
        void el.play().catch((err) => {
          setError(err instanceof Error ? err.message : 'Wiedergabe fehlgeschlagen');
          setPhase('idle');
        });
      };
      rec.start();
      setPhase('recording');
      window.setTimeout(() => {
        const cur = recorderRef.current;
        if (cur && cur.state === 'recording') {
          try {
            cur.stop();
          } catch {
            /* ignore */
          }
        }
      }, RECORD_DURATION_MS);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'Mikrofon nicht verfügbar');
      stopAll();
    }
  }, [acquireMic, stopAll, teardownGraph, teardownPlayback]);

  // Bar fill 0..1 — clamp to 1 so very loud sources still fit.
  const fill = Math.min(1, level / 0.2);
  const thresholdMark = Math.min(1, threshold / 0.2);
  const speaking = level > threshold;

  const buttonLabel: Record<Phase, string> = {
    idle: 'Live-Pegel zeigen',
    live: 'Live stoppen',
    recording: 'Aufnahme stoppen',
    playback: 'Wiedergabe',
  };

  return (
    <div>
      <div className="text-sm font-semibold text-fg">Mikrofon-Test</div>
      <p className="mt-1 text-xs text-fg-muted">
        Sieh deinen Live-Pegel (grün = du wirst als sprechend erkannt) und nimm einen 4-Sekunden-Test
        auf, um zu hören, wie deine Peers dich hören.
      </p>

      <div className="mt-2 space-y-3 rounded-lg border border-line bg-surface-3 p-3">
        <div className="relative h-3 overflow-hidden rounded-full bg-surface-2">
          <div
            className={
              'h-full transition-[width] duration-75 ease-out ' +
              (speaking ? 'bg-emerald-500' : 'bg-accent/70')
            }
            style={{ width: (fill * 100).toFixed(2) + '%' }}
          />
          <div
            aria-label="Schwelle"
            title="Schwelle"
            className="pointer-events-none absolute top-0 h-full w-0.5 bg-amber-400"
            style={{ left: 'calc(' + (thresholdMark * 100).toFixed(2) + '% - 1px)' }}
          />
        </div>
        <div className="flex items-center gap-2">
          {phase === 'idle' && (
            <>
              <button
                type="button"
                onClick={() => void startLive()}
                className="cursor-pointer rounded-lg border border-line bg-surface-2 px-3 py-1.5 text-xs font-semibold text-fg transition hover:bg-surface-3"
              >
                {buttonLabel.idle}
              </button>
              <button
                type="button"
                onClick={() => void startTest()}
                className="cursor-pointer rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg shadow-accept-btn transition hover:brightness-110"
              >
                Mic-Test (4s)
              </button>
            </>
          )}
          {phase === 'live' && (
            <button
              type="button"
              onClick={stopAll}
              className="cursor-pointer rounded-lg border border-rose-500/40 bg-transparent px-3 py-1.5 text-xs font-semibold text-rose-600 transition hover:bg-rose-500/10 dark:text-rose-400"
            >
              {buttonLabel.live}
            </button>
          )}
          {phase === 'recording' && (
            <span className="inline-flex items-center gap-2 rounded-lg bg-rose-500/15 px-3 py-1.5 text-xs font-semibold text-rose-600 dark:text-rose-400">
              <span className="h-2 w-2 rounded-full bg-rose-500 motion-safe:animate-live-dot" />
              Aufnahme läuft …
            </span>
          )}
          {phase === 'playback' && (
            <span className="inline-flex items-center gap-2 rounded-lg bg-accent/15 px-3 py-1.5 text-xs font-semibold text-accent">
              Spielt deine Aufnahme zurück …
            </span>
          )}
        </div>
        {error && (
          <p className="text-xs text-rose-600 dark:text-rose-300">{error}</p>
        )}
      </div>
    </div>
  );
}
