import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  Badge,
  Chip,
  color,
  Icon,
  IconButton,
  Icons,
  ProgressBar,
  Spinner,
  Text,
  toRem,
} from 'folds';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { Range } from 'react-range';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { IAudioInfo } from '../../../../types/matrix/common';
import {
  PlayTimeCallback,
  useMediaLoading,
  useMediaPlay,
  useMediaPlayTimeCallback,
  useMediaSeek,
  useMediaVolume,
} from '../../../hooks/media';
import { useThrottle } from '../../../hooks/useThrottle';
import { secondsToMinutesAndSeconds } from '../../../utils/common';
import {
  decryptFile,
  downloadEncryptedMedia,
  downloadMedia,
  mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';

const PLAY_TIME_THROTTLE_OPS = {
  wait: 500,
  immediate: true,
};

const WAVEFORM_DISPLAY_BARS = 48;

/** Reduce a stored MSC1767 waveform (up to ~100 samples) to a fixed display count. */
export function downsampleWaveform(waveform: number[], count: number): number[] {
  if (waveform.length <= count) return waveform;
  const out: number[] = [];
  const step = waveform.length / count;
  for (let i = 0; i < count; i += 1) {
    out.push(waveform[Math.floor(i * step)] ?? 0);
  }
  return out;
}

// Voice-message waveform that doubles as a seek control: bars fill with the accent
// as the clip plays; click/drag/keyboard scrubs. Mirrors the recorder's bar styling
// (VoiceMessageRecorder). Falls back to the plain seek Range when there's no waveform.
function WaveformSeek({
  waveform,
  currentTime,
  duration,
  onSeek,
  getCurrentTime,
}: {
  waveform: number[];
  currentTime: number;
  duration: number;
  onSeek: (time: number) => void;
  /** Reads the live media time (the `currentTime` prop is throttled ~500ms). */
  getCurrentTime: () => number;
}) {
  const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
  const containerRef = useRef<HTMLDivElement>(null);
  const bars = useMemo(() => downsampleWaveform(waveform, WAVEFORM_DISPLAY_BARS), [waveform]);
  const barMax = useMemo(() => Math.max(...bars, 1), [bars]);
  const progress = duration > 0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0;
  const accent = lotusTerminal ? 'var(--lt-accent-green)' : color.Primary.Main;
  const unplayedColor = `color-mix(in srgb, ${accent} 32%, transparent)`;

  const seekFromClientX = useCallback(
    (clientX: number) => {
      const el = containerRef.current;
      if (!el || duration <= 0) return;
      const rect = el.getBoundingClientRect();
      const ratio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
      onSeek(Math.min(duration, Math.max(0, ratio * duration)));
    },
    [duration, onSeek],
  );

  const handlePointerDown = (evt: React.PointerEvent<HTMLDivElement>) => {
    evt.currentTarget.setPointerCapture(evt.pointerId);
    seekFromClientX(evt.clientX);
  };
  const handlePointerMove = (evt: React.PointerEvent<HTMLDivElement>) => {
    if (evt.currentTarget.hasPointerCapture(evt.pointerId)) seekFromClientX(evt.clientX);
  };
  const handleKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
    if (duration <= 0) return;
    // Base off the LIVE time so rapid presses accumulate (the prop is throttled).
    const base = getCurrentTime();
    if (evt.key === 'ArrowRight' || evt.key === 'ArrowUp') {
      evt.preventDefault();
      onSeek(Math.min(duration, base + 5));
    } else if (evt.key === 'ArrowLeft' || evt.key === 'ArrowDown') {
      evt.preventDefault();
      onSeek(Math.max(0, base - 5));
    } else if (evt.key === 'Home') {
      evt.preventDefault();
      onSeek(0);
    } else if (evt.key === 'End') {
      evt.preventDefault();
      onSeek(duration);
    }
  };

  return (
    <div
      ref={containerRef}
      role="slider"
      tabIndex={0}
      aria-label="Seek"
      aria-valuemin={0}
      aria-valuemax={Math.round(duration)}
      aria-valuenow={Math.round(currentTime)}
      aria-valuetext={`${secondsToMinutesAndSeconds(currentTime)} of ${secondsToMinutesAndSeconds(
        duration,
      )}`}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onKeyDown={handleKeyDown}
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: toRem(2),
        width: '100%',
        height: toRem(24),
        cursor: 'pointer',
        touchAction: 'none',
        overflow: 'hidden',
      }}
    >
      {bars.map((v, i) => {
        const played = bars.length > 0 && (i + 1) / bars.length <= progress;
        return (
          <div
            key={i}
            style={{
              flex: 1,
              minWidth: toRem(2),
              height: toRem(2 + (v / barMax) * 16),
              borderRadius: toRem(1),
              background: played ? accent : unplayedColor,
              transition: 'background 0.1s',
            }}
          />
        );
      })}
    </div>
  );
}

type RenderMediaControlProps = {
  after: ReactNode;
  leftControl: ReactNode;
  rightControl: ReactNode;
  children: ReactNode;
};
export type AudioContentProps = {
  mimeType: string;
  url: string;
  info: IAudioInfo;
  encInfo?: EncryptedAttachmentInfo;
  /** MSC1767 voice waveform (0–1024 ints); when present, the seek bar renders it. */
  waveform?: number[];
  renderMediaControl: (props: RenderMediaControlProps) => ReactNode;
};
export function AudioContent({
  mimeType,
  url,
  info,
  encInfo,
  waveform,
  renderMediaControl,
}: AudioContentProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();

  const [srcState, loadSrc] = useAsyncCallback(
    useCallback(async () => {
      const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
      if (!mediaUrl) throw new Error('Invalid media URL');
      const fileContent = encInfo
        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
        : await downloadMedia(mediaUrl);
      return URL.createObjectURL(fileContent);
    }, [mx, url, useAuthentication, mimeType, encInfo]),
  );

  const audioRef = useRef<HTMLAudioElement | null>(null);
  // A seek requested before the media has loaded; applied once metadata arrives.
  const pendingSeekRef = useRef<number | null>(null);

  useEffect(
    () => () => {
      if (
        srcState.status === AsyncStatus.Success &&
        typeof srcState.data === 'string' &&
        srcState.data.startsWith('blob:')
      ) {
        URL.revokeObjectURL(srcState.data);
      }
    },
    [srcState],
  );

  const [currentTime, setCurrentTime] = useState(0);
  // duration in seconds. (NOTE: info.duration is in milliseconds)
  const infoDuration = info.duration ?? 0;
  const [duration, setDuration] = useState((infoDuration >= 0 ? infoDuration : 0) / 1000);

  const getAudioRef = useCallback(() => audioRef.current, []);
  const { loading } = useMediaLoading(getAudioRef);
  const { playing, setPlaying } = useMediaPlay(getAudioRef);
  const { seek } = useMediaSeek(getAudioRef);
  const { volume, mute, setMute, setVolume } = useMediaVolume(getAudioRef);
  const handlePlayTimeCallback: PlayTimeCallback = useCallback((d, ct) => {
    setDuration(d);
    setCurrentTime(ct);
  }, []);
  useMediaPlayTimeCallback(
    getAudioRef,
    useThrottle(handlePlayTimeCallback, PLAY_TIME_THROTTLE_OPS),
  );

  const [playbackSpeed, setPlaybackSpeed] = useState<0.75 | 1 | 1.5 | 2>(1);

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return undefined;
    const applyRate = () => {
      audio.playbackRate = playbackSpeed;
      // Apply a seek that was requested before the source loaded.
      if (pendingSeekRef.current != null && audio.readyState >= 1) {
        audio.currentTime = pendingSeekRef.current;
        pendingSeekRef.current = null;
      }
    };
    // Apply immediately, and re-apply whenever the media element (re)loads a new
    // source — e.g. after async decrypt swaps in the blob URL — since the browser
    // resets playbackRate to 1 on load, discarding the user's speed choice.
    applyRate();
    audio.addEventListener('loadedmetadata', applyRate);
    audio.addEventListener('play', applyRate);
    return () => {
      audio.removeEventListener('loadedmetadata', applyRate);
      audio.removeEventListener('play', applyRate);
    };
  }, [playbackSpeed]);

  const SPEED_STEPS: Array<0.75 | 1 | 1.5 | 2> = [0.75, 1, 1.5, 2];

  const handleSpeedClick = () => {
    const currentIndex = SPEED_STEPS.indexOf(playbackSpeed);
    const nextIndex = (currentIndex + 1) % SPEED_STEPS.length;
    setPlaybackSpeed(SPEED_STEPS[nextIndex]);
  };

  const handlePlay = () => {
    if (srcState.status === AsyncStatus.Success) {
      setPlaying(!playing);
    } else if (srcState.status !== AsyncStatus.Loading) {
      loadSrc();
    }
  };

  // Seeking before the media has loaded (e.g. clicking the waveform first) loads it
  // and applies the position once metadata arrives (the <audio> autoPlays).
  const handleSeek = useCallback(
    (time: number) => {
      if (srcState.status === AsyncStatus.Success) {
        seek(time);
      } else if (srcState.status !== AsyncStatus.Loading) {
        pendingSeekRef.current = time;
        loadSrc();
      }
    },
    [srcState.status, seek, loadSrc],
  );

  const hasWaveform = !!waveform && waveform.length > 0 && duration > 0;

  return renderMediaControl({
    after: hasWaveform ? (
      <WaveformSeek
        waveform={waveform ?? []}
        currentTime={currentTime}
        duration={duration}
        onSeek={handleSeek}
        getCurrentTime={() => audioRef.current?.currentTime ?? currentTime}
      />
    ) : (
      <Range
        step={1}
        min={0}
        max={duration || 1}
        values={[currentTime]}
        onChange={(values) => handleSeek(values[0])}
        renderTrack={(params) => (
          <div {...params.props}>
            {params.children}
            <ProgressBar
              as="div"
              variant="Secondary"
              size="300"
              min={0}
              max={duration}
              value={currentTime}
              radii="300"
            />
          </div>
        )}
        renderThumb={(params) => (
          <Badge
            size="300"
            variant="Secondary"
            fill="Solid"
            radii="Pill"
            outlined
            {...params.props}
            style={{
              ...params.props.style,
              zIndex: 0,
            }}
          />
        )}
      />
    ),
    leftControl: (
      <>
        <Chip
          onClick={handlePlay}
          variant="Secondary"
          radii="300"
          disabled={srcState.status === AsyncStatus.Loading}
          before={
            srcState.status === AsyncStatus.Loading || loading ? (
              <Spinner variant="Secondary" size="50" />
            ) : (
              <Icon src={playing ? Icons.Pause : Icons.Play} size="50" filled={playing} />
            )
          }
        >
          <Text size="B300">{playing ? 'Pause' : 'Play'}</Text>
        </Chip>

        <Text size="T200">{`${secondsToMinutesAndSeconds(
          currentTime,
        )} / ${secondsToMinutesAndSeconds(duration)}`}</Text>

        <Chip
          onClick={handleSpeedClick}
          variant="Secondary"
          radii="300"
          aria-label={`Playback speed: ${playbackSpeed}×`}
        >
          <Text size="B300">{`${playbackSpeed}×`}</Text>
        </Chip>
      </>
    ),
    rightControl: (
      <>
        <IconButton
          variant="SurfaceVariant"
          size="300"
          radii="Pill"
          onClick={() => setMute(!mute)}
          aria-label={mute ? 'Unmute' : 'Mute'}
          aria-pressed={mute}
        >
          <Icon src={mute ? Icons.VolumeMute : Icons.VolumeHigh} size="50" />
        </IconButton>
        <Range
          step={0.1}
          min={0}
          max={1}
          values={[volume]}
          onChange={(values) => setVolume(values[0])}
          renderTrack={(params) => (
            <div {...params.props}>
              {params.children}
              <ProgressBar
                style={{ width: toRem(48) }}
                variant="Secondary"
                size="300"
                min={0}
                max={1}
                value={volume}
                radii="300"
              />
            </div>
          )}
          renderThumb={(params) => (
            <Badge
              size="300"
              variant="Secondary"
              fill="Solid"
              radii="Pill"
              outlined
              {...params.props}
              style={{
                ...params.props.style,
                zIndex: 0,
              }}
            />
          )}
        />
      </>
    ),
    children: (
      <audio controls={false} autoPlay ref={audioRef}>
        {srcState.status === AsyncStatus.Success && <source src={srcState.data} type={mimeType} />}
      </audio>
    ),
  });
}
