import type { AudioTrack, QualityRung, SubtitleTrack } from './shared';
import { useEffect, useRef, useState } from 'react';
import {
  Check,
  Gauge,
  Languages,
  Maximize2,
  Minimize2,
  Pause,
  Play,
  RotateCcw,
  SkipForward,
  Subtitles,
  Volume1,
  Volume2,
  VolumeX,
} from 'lucide-react';
import { formatClock } from './shared';

// The control bar. Ours rather than the browser's for every transport, so that the progressive fallback — whose
// scrubber has to re-request the stream — is not a visibly different player from the seekable ones.
//
// THE MENUS ARE HAND-ROLLED, and that is not an oversight. A Radix/shadcn dropdown renders through a portal
// into document.body, and body is not inside the element that went fullscreen — so every one of these would be
// invisible exactly when the player is most likely to be used. These stay in the DOM subtree they belong to.

const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];

const BUTTON = 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white/90 hover:bg-white/15';
const ROW = 'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-white/10';

type MenuProps = {
  icon: React.ReactNode;
  label: string;
  active?: boolean;
  children: (close: () => void) => React.ReactNode;
};

const Menu = ({ icon, label, active, children }: MenuProps) => {
  const [open, setOpen] = useState(false);
  const wrapper = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const onPointer = (ev: PointerEvent) => {
      if (!wrapper.current?.contains(ev.target as Node)) setOpen(false);
    };
    const onKey = (ev: KeyboardEvent) => {
      if (ev.key !== 'Escape') return;
      // Escape closes the menu rather than leaving fullscreen, but only while one is open.
      ev.stopPropagation();
      ev.preventDefault();
      setOpen(false);
    };
    document.addEventListener('pointerdown', onPointer, true);
    document.addEventListener('keydown', onKey, true);
    return () => {
      document.removeEventListener('pointerdown', onPointer, true);
      document.removeEventListener('keydown', onKey, true);
    };
  }, [open]);

  return (
    <div ref={wrapper} className="relative shrink-0">
      <button
        type="button"
        aria-label={label}
        aria-expanded={open}
        onClick={() => setOpen((was) => !was)}
        className={`${BUTTON} ${active ? 'text-primary' : ''}`}
      >
        {icon}
      </button>
      {open && (
        <div className="absolute bottom-10 right-0 z-10 max-h-72 min-w-56 overflow-y-auto rounded-lg border border-white/10 bg-black/90 p-1 text-white shadow-xl backdrop-blur">
          <p className="px-2 py-1 text-[10px] uppercase tracking-wide text-white/40">{label}</p>
          {children(() => setOpen(false))}
        </div>
      )}
    </div>
  );
};

type OptionProps = { selected: boolean; onSelect: () => void; children: React.ReactNode; hint?: string | null };

const Option = ({ selected, onSelect, children, hint }: OptionProps) => (
  <button type="button" onClick={onSelect} className={ROW}>
    <Check className={`h-3.5 w-3.5 shrink-0 ${selected ? 'opacity-100 text-primary' : 'opacity-0'}`} />
    <span className="min-w-0 flex-1">
      <span className="block truncate">{children}</span>
      {hint && <span className="block truncate text-[10px] text-white/40">{hint}</span>}
    </span>
  </button>
);

export type PlayerControlsProps = {
  playing: boolean;
  position: number;
  duration: number;
  /** Seconds buffered ahead of the current position, as one number — the element's own ranges, flattened. */
  bufferedTo: number;
  /** False in progressive mode, where a scrub costs a new transcode and only commits on release. */
  liveScrub: boolean;
  volume: number;
  muted: boolean;
  rate: number;
  fullscreen: boolean;
  audio: AudioTrack[];
  audioIndex: number | null;
  subtitles: SubtitleTrack[];
  subtitleIndex: number | null;
  qualityRungs: QualityRung[];
  maxBitrate: number | null;
  /** Set while a re-negotiation is in flight, so a picker click reads as busy rather than as nothing. */
  busy: boolean;
  onTogglePlay: () => void;
  onScrub: (seconds: number) => void;
  onScrubCommit: (seconds: number) => void;
  onSkip: (seconds: number) => void;
  onVolume: (volume: number) => void;
  onToggleMute: () => void;
  onRate: (rate: number) => void;
  onAudio: (index: number) => void;
  onSubtitle: (index: number | null) => void;
  onQuality: (bitrate: number | null) => void;
  onFullscreen: () => void;
};

export const PlayerControls = (props: PlayerControlsProps) => {
  const { position, duration, bufferedTo, liveScrub } = props;
  const barRef = useRef<HTMLDivElement>(null);
  const [dragging, setDragging] = useState<number | null>(null);
  const [hover, setHover] = useState<number | null>(null);

  const shown = dragging ?? position;
  const pct = (seconds: number) => (duration > 0 ? Math.min(100, Math.max(0, (seconds / duration) * 100)) : 0);

  const secondsAt = (clientX: number): number => {
    const box = barRef.current?.getBoundingClientRect();
    if (!box || box.width === 0) return 0;
    return Math.max(0, Math.min(duration, ((clientX - box.left) / box.width) * duration));
  };

  // Pointer capture rather than window listeners: the drag keeps tracking when the cursor leaves the bar, and
  // it ends on the same element it started on even if that is over the video or outside the window.
  const onPointerDown = (ev: React.PointerEvent<HTMLDivElement>) => {
    ev.currentTarget.setPointerCapture(ev.pointerId);
    const seconds = secondsAt(ev.clientX);
    setDragging(seconds);
    if (liveScrub) props.onScrub(seconds);
  };

  const onPointerMove = (ev: React.PointerEvent<HTMLDivElement>) => {
    const seconds = secondsAt(ev.clientX);
    setHover(seconds);
    if (dragging === null) return;
    setDragging(seconds);
    // In progressive mode every intermediate value would start an ffmpeg, so only the release commits.
    if (liveScrub) props.onScrub(seconds);
  };

  const onPointerUp = (ev: React.PointerEvent<HTMLDivElement>) => {
    if (dragging === null) return;
    ev.currentTarget.releasePointerCapture(ev.pointerId);
    props.onScrubCommit(secondsAt(ev.clientX));
    setDragging(null);
  };

  const currentSubtitle = props.subtitles.find((track) => track.index === props.subtitleIndex) ?? null;
  const currentQuality = props.qualityRungs.find((rung) => rung.videoBitrate === props.maxBitrate) ?? null;

  return (
    <div className="bg-gradient-to-t from-black/90 to-transparent px-3 pb-2 pt-6">
      <div className="flex flex-col gap-1">
        <div className="flex items-center gap-2">
          <span className="w-12 shrink-0 text-right text-[11px] tabular-nums text-white/70">{formatClock(shown)}</span>

          <div
            ref={barRef}
            role="slider"
            tabIndex={0}
            aria-label="Seek"
            aria-valuemin={0}
            aria-valuemax={Math.max(1, Math.round(duration))}
            aria-valuenow={Math.round(shown)}
            aria-valuetext={formatClock(shown)}
            onPointerDown={onPointerDown}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerLeave={() => setHover(null)}
            className="group relative h-6 flex-1 cursor-pointer touch-none select-none"
          >
            <div className="absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-white/20">
              <div className="h-full rounded-full bg-white/30" style={{ width: `${pct(bufferedTo)}%` }} />
            </div>
            <div
              className="absolute left-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-primary"
              style={{ width: `${pct(shown)}%` }}
            />
            <div
              className="absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary opacity-0 transition-opacity group-hover:opacity-100"
              style={{ left: `${pct(shown)}%`, opacity: dragging !== null ? 1 : undefined }}
            />
            {hover !== null && duration > 0 && (
              <span
                className="pointer-events-none absolute -top-5 -translate-x-1/2 rounded bg-black/80 px-1 text-[10px] tabular-nums text-white"
                style={{ left: `${pct(hover)}%` }}
              >
                {formatClock(hover)}
              </span>
            )}
          </div>

          <span className="w-12 shrink-0 text-[11px] tabular-nums text-white/70">{formatClock(duration)}</span>
        </div>

        <div className="flex items-center gap-1">
          <button
            type="button"
            onClick={props.onTogglePlay}
            aria-label={props.playing ? 'Pause' : 'Play'}
            className={BUTTON}
          >
            {props.playing ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" fill="currentColor" />}
          </button>
          <button type="button" onClick={() => props.onSkip(-10)} aria-label="Back 10 seconds" className={BUTTON}>
            <RotateCcw className="h-4 w-4" />
          </button>
          <button type="button" onClick={() => props.onSkip(30)} aria-label="Forward 30 seconds" className={BUTTON}>
            <SkipForward className="h-4 w-4" />
          </button>

          {/* The slider stays out of the tab order on purpose — the volume button is the keyboard affordance,
              and a range input here would swallow the arrow keys the player binds to seeking. */}
          <div className="group/vol flex items-center">
            <button
              type="button"
              onClick={props.onToggleMute}
              aria-label={props.muted ? 'Unmute' : 'Mute'}
              className={BUTTON}
            >
              {props.muted || props.volume === 0 ? (
                <VolumeX className="h-4 w-4" />
              ) : props.volume < 0.5 ? (
                <Volume1 className="h-4 w-4" />
              ) : (
                <Volume2 className="h-4 w-4" />
              )}
            </button>
            <input
              type="range"
              min={0}
              max={1}
              step={0.02}
              tabIndex={-1}
              value={props.muted ? 0 : props.volume}
              onChange={(ev) => props.onVolume(Number(ev.target.value))}
              aria-label="Volume"
              className="h-1 w-0 cursor-pointer accent-primary opacity-0 transition-all group-hover/vol:w-20 group-hover/vol:opacity-100"
            />
          </div>

          <span className="flex-1" />

          {props.busy && <span className="mr-1 text-[10px] text-white/50">switching…</span>}

          <Menu icon={<Subtitles className="h-4 w-4" />} label="Subtitles" active={props.subtitleIndex !== null}>
            {(close) => (
              <>
                <Option
                  selected={props.subtitleIndex === null}
                  onSelect={() => {
                    props.onSubtitle(null);
                    close();
                  }}
                >
                  Off
                </Option>
                {props.subtitles.length === 0 && (
                  <p className="px-2 py-1.5 text-[11px] text-white/40">This file has no subtitle tracks.</p>
                )}
                {props.subtitles.map((track) =>
                  track.url ? (
                    <Option
                      key={track.index}
                      selected={props.subtitleIndex === track.index}
                      onSelect={() => {
                        props.onSubtitle(track.index);
                        close();
                      }}
                      hint={track.isForced ? 'forced' : null}
                    >
                      {track.label}
                    </Option>
                  ) : (
                    <p
                      key={track.index}
                      className="px-2 py-1.5 text-[11px] text-white/30"
                      title={track.unavailable ?? ''}
                    >
                      <span className="block truncate line-through">{track.label}</span>
                      <span className="block text-[10px]">unavailable — {track.unavailable}</span>
                    </p>
                  ),
                )}
              </>
            )}
          </Menu>

          <Menu icon={<Languages className="h-4 w-4" />} label="Audio track">
            {(close) => (
              <>
                {props.audio.map((track) => (
                  <Option
                    key={track.index}
                    selected={props.audioIndex === track.index || (props.audioIndex === null && track.isDefault)}
                    onSelect={() => {
                      props.onAudio(track.index);
                      close();
                    }}
                    hint={[track.codec?.toUpperCase(), track.channels ? `${track.channels}ch` : null]
                      .filter(Boolean)
                      .join(' · ')}
                  >
                    {track.label}
                  </Option>
                ))}
                {props.audio.length <= 1 && (
                  <p className="px-2 py-1.5 text-[11px] text-white/40">Only one audio track.</p>
                )}
              </>
            )}
          </Menu>

          <Menu icon={<Gauge className="h-4 w-4" />} label="Speed and quality">
            {(close) => (
              <>
                <p className="px-2 pt-1 text-[10px] uppercase tracking-wide text-white/40">Speed</p>
                {PLAYBACK_RATES.map((rate) => (
                  <Option
                    key={rate}
                    selected={props.rate === rate}
                    onSelect={() => {
                      props.onRate(rate);
                      close();
                    }}
                  >
                    {rate === 1 ? 'Normal' : `${rate}×`}
                  </Option>
                ))}
                <p className="mt-1 border-t border-white/10 px-2 pt-2 text-[10px] uppercase tracking-wide text-white/40">
                  Quality
                </p>
                <Option
                  selected={props.maxBitrate === null}
                  onSelect={() => {
                    props.onQuality(null);
                    close();
                  }}
                  hint="direct play or remux where possible"
                >
                  Auto
                </Option>
                {props.qualityRungs.map((rung) => (
                  <Option
                    key={rung.videoBitrate}
                    selected={props.maxBitrate === rung.videoBitrate}
                    onSelect={() => {
                      props.onQuality(rung.videoBitrate);
                      close();
                    }}
                    hint={`${Math.round(rung.videoBitrate / 1_000_000)} Mbit · forces a re-encode`}
                  >
                    {rung.label}
                  </Option>
                ))}
              </>
            )}
          </Menu>

          <button
            type="button"
            onClick={props.onFullscreen}
            aria-label={props.fullscreen ? 'Exit fullscreen' : 'Fullscreen'}
            className={BUTTON}
          >
            {props.fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
          </button>
        </div>

        {(currentSubtitle || currentQuality) && (
          <p className="truncate px-1 text-[10px] text-white/35">
            {[
              currentSubtitle && `subtitles: ${currentSubtitle.label}`,
              currentQuality && `quality: ${currentQuality.label}`,
            ]
              .filter(Boolean)
              .join(' · ')}
          </p>
        )}
      </div>
    </div>
  );
};
