/* eslint-disable jsx-a11y/media-has-caption */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Icon, IconButton, Spinner, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { VoiceRecording, VoiceRecordingResult } from '../../utils/voiceRecording';
import { secondsToMinutesAndSeconds } from '../../utils/common';
import { normalizeWaveform } from '../../utils/audioWaveform';
import { PlayTimeCallback, useMediaPlay, useMediaPlayTimeCallback } from '../../hooks/media';
import { useThrottle } from '../../hooks/useThrottle';
import * as css from './VoiceRecorderForm.css';

const LIVE_BARS = 48;
const PREVIEW_BARS = 48;
const PLAY_TIME_THROTTLE_OPS = { wait: 80, immediate: true };
// Hard cap on a single recording. Telegram itself has no duration limit (it's
// only storage/file-size bounded), so a "press and forget" would record forever
// — we auto-stop into the preview state at this point so nothing is lost and the
// mic is released. Tune freely; 5 minutes is generous for a chat voice note.
const MAX_RECORDING_MS = 5 * 60 * 1000;

type Phase = 'starting' | 'recording' | 'recorded' | 'error';

// Composer recorder icons — stroke-based outline style from the Dawn design
// canon (docs/design/new-direct-messages-design/project/shared.jsx), NOT folds'
// default icons. folds wraps these in <svg viewBox="0 0 24 24" fill="none">.
export const RecIcons = {
  Close: () => (
    <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
  ),
  Trash: () => (
    <>
      <path d="M4 7h16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
      <path
        d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinejoin="round"
      />
      <path
        d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinejoin="round"
      />
    </>
  ),
  Stop: () => (
    <rect x="6" y="6" width="12" height="12" rx="2.5" stroke="currentColor" strokeWidth="1.6" />
  ),
  Play: () => (
    <path
      d="M8 5.5l11 6.5-11 6.5V5.5z"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinejoin="round"
    />
  ),
  Pause: () => (
    <path d="M9 5v14M15 5v14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
  ),
  Send: () => (
    <path
      d="M4 12l16-8-6 18-3-7-7-3z"
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinejoin="round"
    />
  ),
};

export type VoiceRecorderProps = {
  // Fired with the finished recording when the user taps Send.
  onSend: (result: VoiceRecordingResult) => void;
  // Fired when the user closes the recorder (discard + back to text input).
  onClose: () => void;
};

// Self-contained recorder that renders into the composer's `replaceEditable`
// slot (so the input morphs in place; the composer card / Slate context / drafts
// stay mounted). It owns ALL recorder state internally — high-frequency updates
// (timer, live levels, preview position) re-render ONLY this component, never the
// big RoomInput. The hidden <audio> lives here too, so on Send/Close the whole
// subtree unmounts and any preview playback stops. See docs/plans/voice_messages.md.
export function VoiceRecorder({ onSend, onClose }: VoiceRecorderProps) {
  const { t } = useTranslation();
  const recorderRef = useRef<VoiceRecording | null>(null);
  const mountedRef = useRef(true);
  const [phase, setPhase] = useState<Phase>('starting');
  const [elapsedMs, setElapsedMs] = useState(0);
  const [liveLevels, setLiveLevels] = useState<number[]>(() => new Array(LIVE_BARS).fill(0));
  const [result, setResult] = useState<VoiceRecordingResult | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);

  const audioRef = useRef<HTMLAudioElement | null>(null);
  const [currentTime, setCurrentTime] = useState(0);
  const getAudioRef = useCallback(() => audioRef.current, []);
  const { playing, setPlaying } = useMediaPlay(getAudioRef);
  const handlePlayTime: PlayTimeCallback = useCallback((_d, ct) => setCurrentTime(ct), []);
  useMediaPlayTimeCallback(getAudioRef, useThrottle(handlePlayTime, PLAY_TIME_THROTTLE_OPS));

  const begin = useCallback(async () => {
    if (recorderRef.current) return;
    const rec = new VoiceRecording();
    recorderRef.current = rec;
    setResult(null);
    setLiveLevels(new Array(LIVE_BARS).fill(0));
    setElapsedMs(0);
    setCurrentTime(0);
    setPhase('starting');
    try {
      await rec.start((level) => {
        // Fixed-width sliding window — shift one out, push the newest in, so the
        // waveform glides continuously instead of growing then freezing.
        setLiveLevels((prev) => {
          const next = prev.slice(1);
          next.push(level);
          return next;
        });
      });
      setPhase('recording');
    } catch {
      recorderRef.current = null;
      setPhase('error');
    }
  }, []);

  const handleStop = useCallback(async () => {
    const rec = recorderRef.current;
    if (!rec) return;
    recorderRef.current = null;
    try {
      const res = await rec.stop();
      // If the composer unmounted (room switch) during the await, don't create a
      // preview URL no one is left to revoke, and skip the state writes.
      if (!mountedRef.current) return;
      setResult(res);
      setPreviewUrl(URL.createObjectURL(res.blob));
      setPhase('recorded');
    } catch {
      if (mountedRef.current) setPhase('error');
    }
  }, []);

  // Auto-start on mount; cancel any in-flight recorder on unmount (mic release).
  useEffect(() => {
    mountedRef.current = true;
    begin();
    return () => {
      mountedRef.current = false;
      recorderRef.current?.cancel();
      recorderRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Recording timer + max-duration auto-stop.
  useEffect(() => {
    if (phase !== 'recording') return undefined;
    const startedAt = performance.now();
    const id = window.setInterval(() => {
      const elapsed = performance.now() - startedAt;
      setElapsedMs(elapsed);
      if (elapsed >= MAX_RECORDING_MS) handleStop();
    }, 200);
    return () => window.clearInterval(id);
  }, [phase, handleStop]);

  // Revoke the preview object URL when it changes / on unmount.
  useEffect(
    () => () => {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
    },
    [previewUrl]
  );

  // Discard the preview and record again. Only reachable in the 'recorded'
  // phase, where recorderRef is already null, so begin() starts a fresh take.
  // The [previewUrl] effect is the single revocation owner.
  // Discard the recording and close the whole recorder (back to text input) —
  // NOT re-record. Unmounting cancels the recorder + revokes the preview URL.
  const handleDelete = useCallback(() => {
    setPlaying(false);
    onClose();
  }, [onClose, setPlaying]);

  const handleSend = useCallback(() => {
    setPlaying(false);
    if (result) onSend(result);
    else onClose();
  }, [result, onSend, onClose, setPlaying]);

  const previewBars = useMemo(() => normalizeWaveform(result?.waveform, PREVIEW_BARS), [result]);
  const durationSec = (result?.durationMs ?? 0) / 1000;
  const progress = durationSec > 0 ? Math.min(1, currentTime / durationSec) : 0;
  const recording = phase === 'recording' || phase === 'starting';
  const timeLabel = recording
    ? secondsToMinutesAndSeconds(elapsedMs / 1000)
    : secondsToMinutesAndSeconds(playing || currentTime > 0 ? currentTime : durationSec);

  return (
    <div className={css.Recorder}>
      <div className={css.WaveRow}>
        {phase === 'error' ? (
          <Text className={css.ErrorText} size="T300">
            {t('Room.voice_mic_error')}
          </Text>
        ) : (
          <>
            {recording && <span className={css.RecDot} aria-hidden />}
            {phase === 'recorded' && (
              <button
                type="button"
                className={css.PlayBtn}
                onClick={() => setPlaying(!playing)}
                aria-label={playing ? t('Room.voice_pause') : t('Room.voice_play')}
              >
                <Icon src={playing ? RecIcons.Pause : RecIcons.Play} size="200" />
              </button>
            )}
            <div className={css.Waveform} aria-hidden>
              {phase === 'recorded'
                ? previewBars.map((amp, index) => {
                    const played = (index + 0.5) / previewBars.length <= progress;
                    return (
                      <span
                        // eslint-disable-next-line react/no-array-index-key
                        key={index}
                        className={`${css.WaveBar} ${played ? css.WaveBarPlayed : css.WaveBarRest}`}
                        style={{ height: `${Math.max(12, Math.round(amp * 100))}%` }}
                      />
                    );
                  })
                : liveLevels.map((level, index) => (
                    <span
                      // eslint-disable-next-line react/no-array-index-key
                      key={index}
                      className={`${css.WaveBar} ${css.WaveBarLive}`}
                      style={{ height: `${Math.max(8, Math.round(Math.min(1, level) * 100))}%` }}
                    />
                  ))}
            </div>
            <Text className={css.Time} size="T300">
              {timeLabel}
            </Text>
          </>
        )}
      </div>

      <div className={css.Controls}>
        {/* Left action: cancel while recording (X) or discard+close in preview
            (trash). Both exit the recorder entirely. */}
        {phase === 'recorded' ? (
          <IconButton
            onClick={handleDelete}
            variant="SurfaceVariant"
            fill="None"
            size="300"
            radii="300"
            aria-label={t('Room.voice_delete')}
          >
            <Icon src={RecIcons.Trash} />
          </IconButton>
        ) : (
          <IconButton
            onClick={onClose}
            variant="SurfaceVariant"
            fill="None"
            size="300"
            radii="300"
            aria-label={t('Room.voice_close')}
          >
            <Icon src={RecIcons.Close} />
          </IconButton>
        )}

        <div className={css.Grow} />

        {phase === 'starting' && <Spinner size="200" variant="Secondary" />}

        {phase === 'recording' && (
          <IconButton
            onClick={handleStop}
            variant="Primary"
            fill="Soft"
            size="300"
            radii="300"
            aria-label={t('Room.voice_stop')}
          >
            <Icon src={RecIcons.Stop} />
          </IconButton>
        )}

        {phase === 'recorded' && (
          <IconButton
            onClick={handleSend}
            variant="Primary"
            fill="Soft"
            size="300"
            radii="300"
            aria-label={t('Room.voice_send')}
          >
            <Icon src={RecIcons.Send} />
          </IconButton>
        )}
      </div>

      <audio
        controls={false}
        ref={audioRef}
        src={previewUrl ?? undefined}
        style={{ display: 'none' }}
      />
    </div>
  );
}
