import { useCallback, useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router';
import { useClient } from 'hooks/useClient';
import { useCapabilities } from 'hooks/useCapabilities';
import { Play, Pause, SkipBack, SkipForward, X, Volume2, VolumeX, Loader2, MicVocal } from 'lucide-react';
import { SeekBar } from '../apps/FileViewer/renderers/SeekBar';
import { MusicHeart } from './MusicHeart';
import { fmtClock, musicPath, sortTracks, trackHomePath, type AlbumMeta, type NowPlaying } from './shared';
import { useMusicPlayer, type PlayerTrack } from './useMusicPlayer';
import { GaplessEngine, type EngineTrack } from './gapless-engine';
import { publishPlayerTime, registerPlayerSeek } from './player-time';
import { useLyricsOpen } from './useLyricsOpen';

// Mounted once in the persistent DashboardLayout (outside <Routes>), so it owns the single audio engine
// and the site-wide play dock — playback survives navigation between routes.
//
// Audio is Web Audio (gapless-engine), NOT an <audio> element: it schedules each track to start at the
// exact sample the previous one ends, so a continuous mix plays with zero gap on auto-advance. React
// holds the queue/index (shared via useMusicPlayer); this host reconciles it with the engine — user
// actions (new album, jump, prev/next) command the engine, and the engine's own natural advance mirrors
// back into the index without restarting playback.

const MUSIC_API = '/api/music';

export const MusicPlayerHost = () => {
  const { token, get, put, delete: del } = useClient();
  const { can } = useCapabilities();
  const canUseMusic = can('music');
  const navigate = useNavigate();
  const { pathname } = useLocation();
  const { current, index, queue, playing, toggle, next, prev, setPlaying, syncIndex, close, loadQueue } =
    useMusicPlayer();

  const engineRef = useRef<GaplessEngine | null>(null);
  const [position, setPosition] = useState(0);
  const [duration, setDuration] = useState(0);
  const [loading, setLoading] = useState(false);
  const [volume, setVolume] = useState(() => {
    const v = parseFloat(localStorage.getItem('music.volume') ?? '1');
    return Number.isFinite(v) ? v : 1;
  });
  const [muted, setMuted] = useState(false);
  const [lyricsOpen, toggleLyrics] = useLyricsOpen();

  const trackKey = current ? `${current.albumRel}/${current.file}` : '';

  // Latest position/duration in refs so persist reads fresh values without re-subscribing. seekToRef holds
  // a pending restore offset; isRestoringRef suppresses persist during the restore load so it doesn't
  // clobber the saved position with 0; restoredRef makes restore run once; engineIndexRef is the index the
  // engine is actually on, used to tell an engine-driven advance apart from a user jump.
  const positionRef = useRef(0);
  positionRef.current = position;
  const durationRef = useRef(0);
  durationRef.current = duration;
  const restoredRef = useRef(false);
  const isRestoringRef = useRef(false);
  const seekToRef = useRef<number | null>(null);
  const engineIndexRef = useRef(0);
  // The engine is created once, so its callbacks would capture first-render closures. useMusicPlayer's
  // functional setters (syncIndex/setPlaying) read the state captured at THAT render (the initial EMPTY
  // queue) — calling them from a stale closure wipes the queue. Route them through refs kept current.
  const syncIndexRef = useRef(syncIndex);
  syncIndexRef.current = syncIndex;
  const setPlayingRef = useRef(setPlaying);
  setPlayingRef.current = setPlaying;
  const closeRef = useRef(close);
  closeRef.current = close;

  const withToken = (u: string) => (token ? `${u}${u.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : u);
  const streamUrl = (t: PlayerTrack) =>
    withToken(`${MUSIC_API}/stream?path=${encodeURIComponent(`Music/${t.albumRel}/${t.file}`)}`);
  const coverUrl = (rel: string) => withToken(`${MUSIC_API}/cover?path=${encodeURIComponent(rel)}`);
  const toEngineTrack = (t: PlayerTrack): EngineTrack => ({ key: `${t.albumRel}/${t.file}`, url: streamUrl(t) });

  const persist = () => {
    if (!current) return;
    put('/music/now-playing?device=web', {
      homePath: trackHomePath(current.albumRel, current.file),
      dir: `Music/${current.albumRel}`,
      title: current.title ?? '',
      artist: current.artist ?? '',
      album: current.albumRel.split('/').pop() ?? '',
      durationSec: Math.round(durationRef.current) || 0,
      positionSec: Math.round(positionRef.current) || 0,
    }).catch(() => {});
  };

  // ── Engine lifecycle (mounted once) ──
  useEffect(() => {
    const engine = new GaplessEngine({
      onTime: (pos, dur) => {
        positionRef.current = pos;
        durationRef.current = dur;
        setPosition(pos);
        setDuration(dur);
        publishPlayerTime(pos, dur); // the lyrics pane and the /music scrubber live in another tree
        isRestoringRef.current = false; // saved position has been applied — safe to persist again
      },
      onIndex: (i) => {
        engineIndexRef.current = i; // engine advanced on its own → mirror to UI without restarting
        syncIndexRef.current(i);
      },
      // Queue finished on its own → clear it so the in-flow dock releases its space (no idle bar lingering
      // after playback). The saved snapshot is left intact, so a reload still resumes where you left off.
      onEndOfQueue: () => closeRef.current(),
      onLoadingChange: setLoading,
    });
    engineRef.current = engine;
    engine.setVolume(muted ? 0 : volume);
    // Satisfy the browser autoplay policy ONCE, on the first user gesture — after that, sticky activation
    // lets engine.play() resume the context on its own. It MUST be once-only: a persistent listener would
    // resume the context on every click, overriding a deliberate pause (pause = ctx.suspend()).
    const unlock = () => engine.unlock();
    document.addEventListener('pointerdown', unlock, { once: true });
    return () => {
      document.removeEventListener('pointerdown', unlock);
      engine.destroy();
      engineRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Restore the saved "currently playing" on first load — paused, at its position — so a reload/return
  // lands back on the track. Skipped when a queue already exists (an in-app nav kept player state).
  //
  // Also skipped without the `music` capability. This host is mounted by the shell for every account, so it
  // used to reach for `/music/now-playing` on a member's very first paint and 403.
  useEffect(() => {
    if (restoredRef.current) return;
    if (!canUseMusic) return;
    restoredRef.current = true;
    if (queue.length) return;
    (async () => {
      const snap = await get<NowPlaying | null>('/music/now-playing?device=web').catch(() => null);
      if (!snap?.homePath) return;
      const albumRel = (snap.dir || snap.homePath.split('/').slice(0, -1).join('/')).replace(/^Music\//, '');
      const file = snap.homePath.split('/').pop() ?? '';
      const meta = await get<AlbumMeta>(`/music/meta?path=${encodeURIComponent(albumRel)}`).catch(() => null);
      if (!meta) return;
      const q: PlayerTrack[] = sortTracks(meta.tracks).map((t) => ({
        albumRel,
        file: t.file,
        title: t.title,
        artist: t.artist,
      }));
      const idx = Math.max(
        0,
        q.findIndex((t) => t.file === file),
      );
      isRestoringRef.current = true;
      seekToRef.current = snap.positionSec > 0 ? snap.positionSec : null;
      loadQueue(q, idx);
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // A NEW queue (playQueue/loadQueue set a fresh array) → (re)load the engine at that index. A pending
  // restore offset starts it paused at position; otherwise autoplay follows the queue's `playing` flag.
  useEffect(() => {
    const engine = engineRef.current;
    if (!engine) return;
    engineIndexRef.current = index;
    const seekTo = seekToRef.current ?? 0;
    seekToRef.current = null;
    engine.load(queue.map(toEngineTrack), index, playing, seekTo);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [queue]);

  // Index changed on the SAME queue: a user jump / prev-next (engine advance already matches, so it no-ops).
  useEffect(() => {
    const engine = engineRef.current;
    if (!engine || !queue.length) return;
    if (index === engineIndexRef.current) return;
    engineIndexRef.current = index;
    engine.skipTo(index);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [index]);

  // Play/pause.
  useEffect(() => {
    const engine = engineRef.current;
    if (!engine || !current) return;
    if (playing) engine.play();
    else engine.pause();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playing]);

  // Volume / mute.
  useEffect(() => {
    engineRef.current?.setVolume(muted ? 0 : volume);
  }, [volume, muted]);

  // Persist on play/pause + track change (not during the restore load).
  useEffect(() => {
    if (isRestoringRef.current) return;
    persist();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [trackKey, playing]);

  // Heartbeat while playing, so the saved position keeps up.
  useEffect(() => {
    if (!playing || !current) return;
    const id = setInterval(persist, 10000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playing, trackKey]);

  const changeVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
    const v = parseFloat(e.target.value);
    setVolume(v);
    setMuted(v === 0);
    localStorage.setItem('music.volume', String(v));
  };

  // Seek is the engine's, and the engine is this component's — so the lyrics panel, which lives in the
  // /music workspace rather than under the dock, reaches it through this registration.
  const seekTo = useCallback((sec: number) => {
    setPosition(sec);
    publishPlayerTime(sec, durationRef.current);
    engineRef.current?.seek(sec);
  }, []);

  useEffect(() => registerPlayerSeek(seekTo), [seekTo]);

  // Scrubber → engine.seek (Web Audio has no <audio>.currentTime, so drive it directly).
  const barRef = useRef<HTMLDivElement>(null);
  const onSeekDown = (e: React.MouseEvent<HTMLDivElement>) => {
    const seekAt = (clientX: number) => {
      const bar = barRef.current;
      if (!bar || !duration) return;
      const rect = bar.getBoundingClientRect();
      const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
      seekTo(pct * duration);
    };
    e.preventDefault();
    seekAt(e.clientX);
    const onMove = (ev: MouseEvent) => seekAt(ev.clientX);
    const onUp = () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  };

  const subtitle = current?.artist ?? current?.albumRel.split('/').slice(-2, -1)[0] ?? '';
  const pct = duration ? (position / duration) * 100 : 0;

  // Closing the dock also clears the saved "currently playing" snapshot, so it doesn't get restored on
  // the next load. (Merely close()-ing the local queue would leave the server snapshot to bring it back.)
  const handleClose = () => {
    del('/music/now-playing?device=web').catch(() => {});
    close();
  };

  // The dock's microphone opens the lyrics panel inside the /music workspace, so it navigates there
  // rather than growing a sheet of its own — the dock keeps its height on every screen.
  const showLyrics = () => {
    if (!lyricsOpen) navigate('/music');
    toggleLyrics();
  };

  if (!current) return null;

  // On /music the library screen draws its own MusicMiniBar inside the panel, and the album view already
  // has the transport — so the dock would be a second bar taking a full row off the workspace. The host
  // stays MOUNTED (it owns the engine); only its bar is withheld.
  if (pathname.startsWith('/music')) return null;

  // In-flow bottom bar (NOT position:fixed) — it reserves its own height so the content above shrinks to
  // fit and the nav dock naturally sits above it, no overlap hacks needed.
  return (
    <div className="flex shrink-0 items-center gap-3 border-t border-border bg-card/95 px-4 py-2 backdrop-blur">
      {/* cover + info — a real link to the playing album, so it cmd-clicks like anything else */}
      <Link
        to={musicPath(current.albumRel)}
        title="Show in library"
        className="flex min-w-0 cursor-pointer items-center gap-3 rounded text-left transition-opacity hover:opacity-80"
      >
        <div className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
          <img
            src={coverUrl(current.albumRel)}
            alt=""
            className="h-full w-full object-cover"
            onError={(e) => {
              (e.currentTarget as HTMLImageElement).style.visibility = 'hidden';
            }}
          />
        </div>
        <div className="hidden w-44 shrink-0 sm:block">
          <p className="truncate text-sm font-medium text-foreground">{current.title ?? current.file}</p>
          <p className="truncate text-xs text-muted-foreground">{subtitle}</p>
        </div>
      </Link>

      {/* transport */}
      <div className="flex shrink-0 items-center gap-1">
        <button
          type="button"
          onClick={prev}
          disabled={index === 0}
          className="cursor-pointer p-1.5 text-muted-foreground hover:text-foreground disabled:cursor-default disabled:opacity-40"
        >
          <SkipBack size={18} />
        </button>
        <button
          type="button"
          onClick={toggle}
          className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground hover:opacity-90 active:scale-95"
        >
          {loading ? (
            <Loader2 size={18} className="animate-spin" />
          ) : playing ? (
            <Pause size={18} />
          ) : (
            <Play size={18} className="ml-0.5" />
          )}
        </button>
        <button
          type="button"
          onClick={next}
          disabled={index >= queue.length - 1}
          className="cursor-pointer p-1.5 text-muted-foreground hover:text-foreground disabled:cursor-default disabled:opacity-40"
        >
          <SkipForward size={18} />
        </button>
      </div>

      {/* scrubber + times */}
      <span className="hidden w-10 shrink-0 text-right font-mono text-[10px] tabular-nums text-muted-foreground md:block">
        {fmtClock(position)}
      </span>
      <div className="min-w-0 flex-1">
        <SeekBar
          barRef={barRef}
          onSeekDown={onSeekDown}
          pct={pct}
          trackClass="bg-muted"
          fillClass="bg-primary"
          thumbClass="border-background"
        />
      </div>
      <span className="hidden w-10 shrink-0 font-mono text-[10px] tabular-nums text-muted-foreground md:block">
        {fmtClock(duration)}
      </span>

      {/* volume */}
      <div className="hidden shrink-0 items-center gap-1.5 md:flex">
        <button
          type="button"
          onClick={() => setMuted((m) => !m)}
          className="cursor-pointer text-muted-foreground hover:text-foreground"
        >
          {muted || volume === 0 ? <VolumeX size={16} /> : <Volume2 size={16} />}
        </button>
        <input
          type="range"
          min={0}
          max={1}
          step={0.01}
          value={muted ? 0 : volume}
          onChange={changeVolume}
          className="h-1 w-16 cursor-pointer accent-primary"
        />
      </div>

      <button
        type="button"
        onClick={showLyrics}
        title={lyricsOpen ? 'Hide lyrics' : 'Show lyrics'}
        aria-pressed={lyricsOpen}
        className={`shrink-0 cursor-pointer p-1.5 hover:text-foreground ${lyricsOpen ? 'text-primary' : 'text-muted-foreground'}`}
      >
        <MicVocal size={18} />
      </button>

      <MusicHeart
        kind="track"
        favKey={trackHomePath(current.albumRel, current.file)}
        size={18}
        className="shrink-0 p-1.5"
      />

      <button
        type="button"
        onClick={handleClose}
        className="shrink-0 cursor-pointer p-1.5 text-muted-foreground hover:text-foreground"
      >
        <X size={16} />
      </button>
    </div>
  );
};
