import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { MobileTouchTarget } from '../styles/mobile.css';

type RecorderState = 'idle' | 'recording' | 'paused' | 'preview';

interface VoiceRecorderProps {
  onSend: (blob: Blob, mimeType: string, durationMs: number, waveform: number[]) => void;
  onError?: (err: string) => void;
}

function formatDuration(ms: number): string {
  const totalSec = Math.floor(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  return `${m}:${s.toString().padStart(2, '0')}`;
}

function normalizeWaveform(samples: number[]): number[] {
  if (samples.length === 0) return Array(20).fill(0);
  const max = Math.max(...samples, 1);
  const count = Math.min(samples.length, 100);
  const step = samples.length / count;
  const result: number[] = [];
  for (let i = 0; i < count; i += 1) {
    const idx = Math.floor(i * step);
    result.push(Math.round((samples[idx] / max) * 1024));
  }
  return result;
}

const WAVEFORM_BARS = 40;

export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
  const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
  const [state, setState] = useState<RecorderState>('idle');
  const [durationMs, setDurationMs] = useState(0);
  const [waveformBars, setWaveformBars] = useState<number[]>(Array(WAVEFORM_BARS).fill(0));
  const [previewBlob, setPreviewBlob] = useState<Blob | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);

  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const chunksRef = useRef<Blob[]>([]);
  const analyserRef = useRef<AnalyserNode | null>(null);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const rawSamplesRef = useRef<number[]>([]);
  // Active-recording duration excluding paused time: accumulated ms from prior
  // segments + (now - segmentStart) for the current segment.
  const accumulatedMsRef = useRef<number>(0);
  const segmentStartRef = useRef<number>(0);
  const animFrameRef = useRef<number>(0);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const previewMimeRef = useRef('audio/ogg;codecs=opus');
  const previewDurationRef = useRef(0);
  const previewAudioRef = useRef<HTMLAudioElement | null>(null);
  const [previewPlaying, setPreviewPlaying] = useState(false);

  // Start the waveform (rAF) + duration (interval) meters against the live
  // analyser. Reused by startRecording and resumeRecording.
  const startMeters = useCallback(() => {
    const analyser = analyserRef.current;
    if (!analyser) return;
    // Guard against ever running two loops.
    if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
    if (timerRef.current) clearInterval(timerRef.current);
    const buf = new Uint8Array(analyser.frequencyBinCount);
    const tick = () => {
      if (!analyserRef.current) return;
      analyserRef.current.getByteFrequencyData(buf);
      const avg = buf.reduce((a, b) => a + b, 0) / buf.length;
      rawSamplesRef.current.push(avg);
      setWaveformBars((prev) => [...prev.slice(1), Math.round((avg / 255) * 100)]);
      animFrameRef.current = requestAnimationFrame(tick);
    };
    animFrameRef.current = requestAnimationFrame(tick);
    timerRef.current = setInterval(() => {
      setDurationMs(accumulatedMsRef.current + (Date.now() - segmentStartRef.current));
    }, 100);
  }, []);

  // Stop the meters (rAF + interval) without tearing down the audio graph, so a
  // paused recording can resume.
  const stopMeters = useCallback(() => {
    if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  }, []);

  // Release the microphone. The mic tracks are independent of the MediaRecorder
  // and the AudioContext — neither mr.stop() nor audioCtx.close() releases them —
  // so they must be stopped explicitly (else the OS mic indicator stays on).
  const stopStream = useCallback(() => {
    streamRef.current?.getTracks().forEach((t) => t.stop());
    streamRef.current = null;
  }, []);

  const stopAll = useCallback(() => {
    stopMeters();
    if (audioCtxRef.current) {
      audioCtxRef.current.close();
      audioCtxRef.current = null;
    }
    analyserRef.current = null;
  }, [stopMeters]);

  useEffect(
    () => () => {
      // Unmounting mid-recording/pause must release the mic — stopAll() only
      // closes the AudioContext and cancels the meters, not the mic tracks.
      const mr = mediaRecorderRef.current;
      if (mr && (mr.state === 'recording' || mr.state === 'paused')) {
        mr.ondataavailable = null;
        mr.onstop = null;
        mr.stop();
      }
      stopStream();
      stopAll();
      if (previewUrl) URL.revokeObjectURL(previewUrl);
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const startRecording = useCallback(async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mimeType = MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')
        ? 'audio/ogg;codecs=opus'
        : 'audio/webm;codecs=opus';
      previewMimeRef.current = mimeType;

      const mr = new MediaRecorder(stream, { mimeType });
      mediaRecorderRef.current = mr;
      streamRef.current = stream;
      chunksRef.current = [];
      rawSamplesRef.current = [];
      accumulatedMsRef.current = 0;
      segmentStartRef.current = Date.now();

      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;

      startMeters();

      mr.ondataavailable = (e) => {
        if (e.data.size > 0) chunksRef.current.push(e.data);
      };

      mr.onstop = () => {
        stream.getTracks().forEach((t) => t.stop());
        streamRef.current = null;
        const blob = new Blob(chunksRef.current, { type: mimeType });
        setPreviewBlob(blob);
        setPreviewUrl((prev) => {
          if (prev) URL.revokeObjectURL(prev);
          return URL.createObjectURL(blob);
        });
        setState('preview');
      };

      mr.start(250);
      setState('recording');
    } catch {
      onError?.('Microphone access denied');
    }
  }, [onError, startMeters]);

  const pauseRecording = useCallback(() => {
    const mr = mediaRecorderRef.current;
    if (mr?.state !== 'recording') return;
    accumulatedMsRef.current += Date.now() - segmentStartRef.current;
    setDurationMs(accumulatedMsRef.current);
    stopMeters();
    mr.pause();
    setState('paused');
  }, [stopMeters]);

  const resumeRecording = useCallback(() => {
    const mr = mediaRecorderRef.current;
    if (mr?.state !== 'paused') return;
    segmentStartRef.current = Date.now();
    mr.resume();
    startMeters();
    setState('recording');
  }, [startMeters]);

  const stopRecording = useCallback(() => {
    const mr = mediaRecorderRef.current;
    const activeMs = mr?.state === 'recording' ? Date.now() - segmentStartRef.current : 0;
    previewDurationRef.current = accumulatedMsRef.current + activeMs;
    stopAll();
    if (mr && (mr.state === 'recording' || mr.state === 'paused')) {
      mr.stop();
    }
  }, [stopAll]);

  const cancelRecording = useCallback(() => {
    stopAll();
    const mr = mediaRecorderRef.current;
    if (mr && (mr.state === 'recording' || mr.state === 'paused')) {
      mr.ondataavailable = null;
      // onstop (which would release the mic) is cleared, so release it here.
      mr.onstop = null;
      mr.stop();
    }
    stopStream();
    setPreviewBlob(null);
    setPreviewUrl((prev) => {
      if (prev) URL.revokeObjectURL(prev);
      return null;
    });
    rawSamplesRef.current = [];
    accumulatedMsRef.current = 0;
    segmentStartRef.current = 0;
    setWaveformBars(Array(WAVEFORM_BARS).fill(0));
    setDurationMs(0);
    setState('idle');
  }, [stopAll, stopStream]);

  const sendVoice = useCallback(() => {
    if (!previewBlob) return;
    const waveform = normalizeWaveform(rawSamplesRef.current);
    onSend(previewBlob, previewMimeRef.current, previewDurationRef.current, waveform);
    cancelRecording();
  }, [previewBlob, onSend, cancelRecording]);

  const barMax = Math.max(...waveformBars, 1);

  if (state === 'idle') {
    return (
      <IconButton
        className={MobileTouchTarget}
        onClick={startRecording}
        aria-label="Record voice message"
        variant="SurfaceVariant"
        size="300"
        radii="300"
        title="Record voice message"
      >
        <Icon src={Icons.Mic} size="100" />
      </IconButton>
    );
  }

  if (state === 'recording' || state === 'paused') {
    const paused = state === 'paused';
    return (
      <Box
        data-voice-recorder={paused ? 'paused' : 'recording'}
        alignItems="Center"
        gap="200"
        style={{
          background: color.SurfaceVariant.Container,
          borderRadius: config.radii.R300,
          padding: `${toRem(4)} ${toRem(8)}`,
          maxWidth: '100%',
          minWidth: 0,
        }}
      >
        <Box
          data-voice-rec-dot
          style={{
            width: toRem(8),
            height: toRem(8),
            borderRadius: '50%',
            background: lotusTerminal ? 'var(--lt-accent-orange)' : color.Critical.Main,
            flexShrink: 0,
            // Pulse only while actively recording; hold steady (dimmed) when paused.
            animation: paused ? 'none' : 'pttLivePulse 900ms ease-in-out infinite',
            opacity: paused ? 0.5 : 1,
          }}
        />
        <Text
          size="T200"
          role="timer"
          aria-label={`${paused ? 'Paused' : 'Recording'}, duration ${formatDuration(durationMs)}`}
          style={{
            minWidth: toRem(32),
            fontVariantNumeric: 'tabular-nums',
            ...(lotusTerminal
              ? {
                  fontFamily: 'JetBrains Mono, monospace',
                  color: 'var(--lt-accent-green)',
                  fontWeight: 700,
                }
              : {}),
          }}
        >
          {formatDuration(durationMs)}
        </Text>
        <Box
          data-voice-waveform
          alignItems="Center"
          gap="100"
          style={{ height: toRem(20), overflow: 'hidden', flexShrink: 1, minWidth: 0 }}
        >
          {waveformBars.map((h, i) => (
            <div
              key={i}
              style={{
                width: toRem(2),
                height: toRem(2 + (h / barMax) * 16),
                borderRadius: toRem(1),
                background: lotusTerminal ? 'var(--lt-accent-green)' : color.Primary.Main,
                flexShrink: 0,
              }}
            />
          ))}
        </Box>
        <IconButton
          onClick={paused ? resumeRecording : pauseRecording}
          aria-label={paused ? 'Resume recording' : 'Pause recording'}
          variant="SurfaceVariant"
          fill="Soft"
          size="300"
          radii="300"
          title={paused ? 'Resume' : 'Pause'}
          style={{ flexShrink: 0 }}
        >
          <Icon src={paused ? Icons.Play : Icons.Pause} size="100" />
        </IconButton>
        <IconButton
          onClick={stopRecording}
          aria-label="Finish recording"
          variant="Primary"
          fill="Soft"
          size="300"
          radii="300"
          title="Finish"
          style={{ flexShrink: 0 }}
        >
          <Icon src={Icons.Check} size="100" />
        </IconButton>
        <IconButton
          onClick={cancelRecording}
          aria-label="Cancel recording"
          variant="SurfaceVariant"
          size="300"
          radii="300"
          title="Cancel"
          style={{ flexShrink: 0 }}
        >
          <Icon src={Icons.Cross} size="100" />
        </IconButton>
      </Box>
    );
  }

  return (
    <Box
      alignItems="Center"
      gap="200"
      style={{
        background: color.SurfaceVariant.Container,
        borderRadius: config.radii.R300,
        padding: `${toRem(4)} ${toRem(8)}`,
      }}
    >
      {previewUrl && (
        <>
          <audio
            ref={previewAudioRef}
            src={previewUrl}
            onEnded={() => setPreviewPlaying(false)}
            aria-hidden="true"
          />
          <IconButton
            onClick={() => {
              const audio = previewAudioRef.current;
              if (!audio) return;
              if (previewPlaying) {
                audio.pause();
                setPreviewPlaying(false);
              } else {
                audio.play();
                setPreviewPlaying(true);
              }
            }}
            aria-label={previewPlaying ? 'Pause preview' : 'Play preview'}
            variant="Secondary"
            fill="Soft"
            size="300"
            radii="300"
            title={previewPlaying ? 'Pause' : 'Play'}
          >
            <Icon src={previewPlaying ? Icons.Pause : Icons.Play} size="100" />
          </IconButton>
        </>
      )}
      <Text size="T200" style={{ fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
        {formatDuration(previewDurationRef.current)}
      </Text>
      <IconButton
        onClick={sendVoice}
        aria-label="Send voice message"
        variant="Primary"
        fill="Soft"
        size="300"
        radii="300"
        title="Send voice message"
      >
        <Icon src={Icons.Send} size="100" />
      </IconButton>
      <IconButton
        onClick={cancelRecording}
        aria-label="Discard voice message"
        variant="SurfaceVariant"
        size="300"
        radii="300"
        title="Discard"
      >
        <Icon src={Icons.Delete} size="100" />
      </IconButton>
    </Box>
  );
}
