import { useEffect, useRef, useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { toast } from 'sonner';
import { blobToWav } from '../../../Chat/useAudioRecording';
import type { UseFileBrowserAppType } from '../useFileBrowserApp';

type DictateDialogProps = {
  fileBrowserManager: UseFileBrowserAppType;
};

export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
  const { showDictate, setShowDictate, handleUpload } = fileBrowserManager;
  const [recording, setRecording] = useState(false);
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const chunksRef = useRef<Blob[]>([]);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const analyserRef = useRef<AnalyserNode | null>(null);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const animFrameRef = useRef<number>(0);
  const streamRef = useRef<MediaStream | null>(null);

  // Safe to call twice: the success path in `stopRecording` releases the hardware as early as it can, and
  // the effect cleanup then runs over the same refs. Closing an already-closed AudioContext rejects.
  const cleanup = () => {
    cancelAnimationFrame(animFrameRef.current);
    mediaRecorderRef.current?.stream?.getTracks().forEach((t) => t.stop());
    streamRef.current?.getTracks().forEach((t) => t.stop());
    if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') void audioCtxRef.current.close();
    mediaRecorderRef.current = null;
    streamRef.current = null;
    audioCtxRef.current = null;
    analyserRef.current = null;
    chunksRef.current = [];
    setRecording(false);
  };

  // The effect below closes over `cleanup` from the render that opened the dialog. Reading it through a
  // ref means the teardown always runs the current one rather than a stale capture.
  const cleanupRef = useRef(cleanup);
  cleanupRef.current = cleanup;

  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      streamRef.current = stream;

      const audioCtx = new AudioContext();
      audioCtxRef.current = audioCtx;
      const source = audioCtx.createMediaStreamSource(stream);
      const analyser = audioCtx.createAnalyser();
      analyser.fftSize = 256;
      source.connect(analyser);
      analyserRef.current = analyser;

      const recorder = new MediaRecorder(stream);
      mediaRecorderRef.current = recorder;
      chunksRef.current = [];
      recorder.ondataavailable = (ev) => {
        if (ev.data.size > 0) chunksRef.current.push(ev.data);
      };
      recorder.start(250);
      setRecording(true);

      drawSpectrum();
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Could not access microphone');
      setShowDictate(false);
    }
  };

  const drawSpectrum = () => {
    const canvas = canvasRef.current;
    const analyser = analyserRef.current;
    if (!canvas || !analyser) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const bufferLength = analyser.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);

    const draw = () => {
      animFrameRef.current = requestAnimationFrame(draw);
      analyser.getByteFrequencyData(dataArray);

      const w = canvas.width;
      const h = canvas.height;
      ctx.clearRect(0, 0, w, h);

      const barWidth = (w / bufferLength) * 2;
      let x = 0;
      for (let i = 0; i < bufferLength; i++) {
        const barHeight = (dataArray[i]! / 255) * h;
        ctx.fillStyle = `hsl(174, 60%, ${40 + (dataArray[i]! / 255) * 30}%)`;
        ctx.fillRect(x, h - barHeight, barWidth - 1, barHeight);
        x += barWidth;
      }
    };

    draw();
  };

  const stopRecording = async () => {
    const recorder = mediaRecorderRef.current;
    if (!recorder || recorder.state === 'inactive') {
      cleanup();
      return;
    }

    try {
      const blob = await new Promise<Blob>((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error('Recording stop timed out')), 5000);
        recorder.onstop = () => {
          clearTimeout(timeout);
          resolve(new Blob(chunksRef.current, { type: recorder.mimeType }));
          chunksRef.current = [];
        };
        recorder.stop();
      });

      cancelAnimationFrame(animFrameRef.current);
      recorder.stream.getTracks().forEach((t) => t.stop());
      streamRef.current?.getTracks().forEach((t) => t.stop());
      audioCtxRef.current?.close();

      if (blob.size === 0) {
        toast.error('No audio was captured');
        cleanup();
        setShowDictate(false);
        return;
      }

      const wav = await blobToWav(blob);
      const now = new Date();
      const pad = (n: number) => String(n).padStart(2, '0');
      const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
      const file = new File([wav], `dictation_${ts}.wav`, { type: 'audio/wav' });

      setShowDictate(false);
      setRecording(false);
      await handleUpload([file]);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Recording failed');
      cleanup();
      setShowDictate(false);
    }
  };

  // Teardown runs when the dialog closes and when the panel unmounts. It used to be guarded by
  // `if (!showDictate)`, which can never be true: a cleanup sees the props of the render that registered
  // it, and only the `showDictate === true` render registers one. So it never fired, and the microphone,
  // the AudioContext and the rAF loop stayed alive for the life of the tab. `QrTransfer/Receiver.tsx` is
  // the same shape done right.
  useEffect(() => {
    if (!showDictate) return;
    startRecording();
    return () => cleanupRef.current();
  }, [showDictate]);

  useEffect(() => {
    if (!showDictate || !recording) return;
    const handler = (ev: KeyboardEvent) => {
      if (ev.key === 'Enter') {
        ev.preventDefault();
        stopRecording();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [showDictate, recording]);

  return (
    <Dialog
      open={showDictate}
      onOpenChange={(open) => {
        if (!open) {
          cleanup();
          setShowDictate(false);
        }
      }}
    >
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Dictate</DialogTitle>
          <DialogDescription>Recording audio. Press Enter or click Stop to save.</DialogDescription>
        </DialogHeader>
        <div className="flex flex-col items-center gap-4">
          <canvas ref={canvasRef} width={460} height={120} className="w-full rounded-md bg-duck-dark/5" />
          <div className="flex items-center gap-2">
            {recording && <span className="h-3 w-3 rounded-full bg-red-500 animate-pulse" />}
            <span className="text-sm text-duck-dark/60">{recording ? 'Recording...' : 'Starting...'}</span>
          </div>
          <div className="flex justify-end w-full gap-2">
            <button
              type="button"
              onClick={() => {
                cleanup();
                setShowDictate(false);
              }}
              className="px-4 py-2 text-sm rounded-md text-duck-dark/70 hover:bg-duck-dark/5 cursor-pointer transition-colors"
            >
              Cancel
            </button>
            <button
              type="button"
              onClick={stopRecording}
              disabled={!recording}
              className="px-4 py-2 text-sm rounded-md bg-duck-teal text-duck-yellow hover:bg-duck-teal/90 cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
            >
              Stop & Save
            </button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
};
