import { create } from 'zustand';
import type { Track } from '../types';
import {
  clampCrossfadeMs,
  readStoredCrossfadeMs,
  readStoredLastTrack,
  readStoredPrefetchNext,
  storeCrossfadeMs,
  storeLastTrack,
  storePrefetchNext,
} from '../lib/playbackPrefs';

export type RepeatMode = 'none' | 'all' | 'one';
export type VibeAdvanceReason = 'skipped' | 'completed' | 'disliked';
export type PlaybackOwner = 'ordinary' | 'vibe';

/**
 * How many already-played tracks to keep behind the cursor. Bounds queue growth
 * from the Vibe prefetch loop while still leaving real history for prev().
 */
const MAX_HISTORY = 50;

interface PlaybackState {
  currentTrack: Track | null;
  queue: Track[];
  /** Cursor into `queue` for the current track, or -1 when the current track is not queued. */
  currentIndex: number;
  isPlaying: boolean;
  position: number;
  duration: number;
  volume: number;
  shuffle: boolean;
  repeat: RepeatMode;
  /** Warm the next track's stream into the idle audio element before this one ends. */
  prefetchNext: boolean;
  /** Overlap between tracks, in milliseconds. 0 disables the fade. */
  crossfadeMs: number;
  /** Ids already played this shuffle "lap" (repeat-all), to avoid bouncing between the same few tracks. */
  shufflePlayed: Set<string>;
  /** Installed only while a durable Vibe session owns the queue. */
  vibeAdvanceHandler: ((reason: VibeAdvanceReason) => void) | null;
  /** Vibe must opt in explicitly; ordinary browsing always owns itself. */
  queueOwner: PlaybackOwner;
  /**
   * True while another device holds the audio and this one is only showing what
   * it plays. The engine loads no stream in that state, so a phone watching the
   * desktop stops pulling megabytes of audio it will never play.
   */
  audioElsewhere: boolean;

  setQueue: (queue: Track[]) => void;
  /** Vibe-only queue replacement. Do not use for library browsing. */
  setVibeQueue: (queue: Track[]) => void;
  playTrack: (track: Track) => void;
  play: () => void;
  pause: () => void;
  next: () => void;
  nextWithReason: (reason: VibeAdvanceReason) => void;
  /** Bypass the Vibe controller after it has prepared the next committed track. */
  advance: () => void;
  setVibeAdvanceHandler: (handler: ((reason: VibeAdvanceReason) => void) | null) => void;
  prev: () => void;
  setPosition: (position: number) => void;
  setDuration: (duration: number) => void;
  setVolume: (volume: number) => void;
  setPrefetchNext: (prefetchNext: boolean) => void;
  setCrossfadeMs: (crossfadeMs: number) => void;
  setCurrentTrack: (track: Track | null) => void;
  setAudioElsewhere: (audioElsewhere: boolean) => void;
  toggleShuffle: () => void;
  cycleRepeat: () => void;
}

/**
 * Move to `index` in `queue`, trimming stale history so the queue stays bounded.
 * Returns the state patch (queue may be re-sliced, so the index is adjusted).
 */
function advanceTo(queue: Track[], index: number) {
  let nextQueue = queue;
  let nextIndex = index;
  if (index > MAX_HISTORY) {
    const drop = index - MAX_HISTORY;
    nextQueue = queue.slice(drop);
    nextIndex = index - drop;
  }
  const track = nextQueue[nextIndex];
  return {
    queue: nextQueue,
    currentIndex: nextIndex,
    currentTrack: track,
    position: 0,
    duration: track?.duration ?? 0,
    isPlaying: true,
  };
}

// A fresh tab opens on the last track it played, paused at zero — an empty
// player bar told the listener nothing about where they were.
const restoredTrack = readStoredLastTrack<Track>();

export const usePlaybackStore = create<PlaybackState>((set, get) => ({
  currentTrack: restoredTrack,
  queue: restoredTrack ? [restoredTrack] : [],
  currentIndex: restoredTrack ? 0 : -1,
  isPlaying: false,
  position: 0,
  duration: restoredTrack?.duration ?? 0,
  volume: 1,
  shuffle: false,
  repeat: 'none',
  prefetchNext: readStoredPrefetchNext(),
  crossfadeMs: readStoredCrossfadeMs(),
  shufflePlayed: new Set<string>(),
  vibeAdvanceHandler: null,
  queueOwner: 'ordinary',
  audioElsewhere: false,

  setQueue: (queue) =>
    set((state) => ({
      queue,
      // Keep the cursor pointing at whatever is playing, if it is still queued.
      currentIndex: state.currentTrack ? queue.findIndex((t) => t.id === state.currentTrack!.id) : -1,
      shufflePlayed: new Set(),
      // Every ordinary queue operation is an explicit ownership handoff. This
      // prevents a stale Vibe session from intercepting browser/UI next.
      queueOwner: 'ordinary',
      vibeAdvanceHandler: null,
    })),

  setVibeQueue: (queue) =>
    set((state) => ({
      queue,
      currentIndex: state.currentTrack ? queue.findIndex((t) => t.id === state.currentTrack!.id) : -1,
      shufflePlayed: new Set(),
      queueOwner: 'vibe',
    })),

  playTrack: (track) =>
    set((state) => ({
      currentTrack: track,
      currentIndex: state.queue.findIndex((t) => t.id === track.id),
      isPlaying: true,
      position: 0,
      duration: track.duration ?? 0,
      shufflePlayed: new Set(),
    })),

  play: () => set({ isPlaying: true }),
  pause: () => set({ isPlaying: false }),

  next: () => {
    get().nextWithReason('skipped');
  },

  nextWithReason: (reason) => {
    const { vibeAdvanceHandler: handler, queueOwner } = get();
    if (queueOwner === 'vibe' && handler) {
      handler(reason);
      return;
    }
    get().advance();
  },

  advance: () => {
    const { queue, currentTrack, shuffle, repeat, shufflePlayed } = get();
    if (queue.length === 0) {
      set({ isPlaying: false, position: 0 });
      return;
    }

    // Prefer the tracked cursor; fall back to a lookup if it has drifted.
    const tracked = get().currentIndex;
    const idx =
      tracked >= 0 && queue[tracked]?.id === currentTrack?.id
        ? tracked
        : currentTrack
          ? queue.findIndex((t) => t.id === currentTrack.id)
          : -1;

    // Repeat one: replay current track
    if (repeat === 'one' && currentTrack) {
      set({ position: 0, isPlaying: true });
      return;
    }

    if (shuffle) {
      // Shuffle: pick a random track not yet played this lap (excludes current),
      // so repeat-all doesn't bounce between the same few tracks.
      const played = new Set(shufflePlayed);
      if (currentTrack) played.add(currentTrack.id);
      const indices = queue.map((_, i) => i);
      let remaining = indices.filter((i) => !played.has(queue[i].id));
      if (remaining.length === 0) {
        if (repeat !== 'all') {
          set({ isPlaying: false, position: 0 });
          return;
        }
        // Lap complete — start a fresh one.
        played.clear();
        if (currentTrack) played.add(currentTrack.id);
        remaining = indices.filter((i) => queue[i].id !== currentTrack?.id);
        if (remaining.length === 0) {
          set({ isPlaying: false, position: 0 });
          return;
        }
      }
      const pickIdx = remaining[Math.floor(Math.random() * remaining.length)];
      played.add(queue[pickIdx].id);
      const pick = queue[pickIdx];
      set({
        currentTrack: pick,
        currentIndex: pickIdx,
        shufflePlayed: played,
        position: 0,
        duration: pick.duration ?? 0,
        isPlaying: true,
      });
      return;
    }

    // Sequential — keep the queue intact and move the cursor, so prev() has
    // history and repeat-all restarts from the true first track.
    if (idx < 0) {
      // Nothing playing (or the current track left the queue) — start at the top.
      set(advanceTo(queue, 0));
    } else if (idx + 1 < queue.length) {
      set(advanceTo(queue, idx + 1));
    } else if (repeat === 'all' && queue.length > 0) {
      set(advanceTo(queue, 0));
    } else {
      // End of queue with no looping — stop playback so the store, DOM audio
      // element, and MediaSession all agree nothing is playing. Without this
      // the browser's now-playing widget can auto-resume the finished track
      // from its near-end position, causing a tight "play last second → end →
      // auto-resume → play last second → …" loop that also degrades performance.
      set({ isPlaying: false, position: 0 });
    }
  },

  setVibeAdvanceHandler: (vibeAdvanceHandler) => set((state) => ({
    vibeAdvanceHandler,
    queueOwner: vibeAdvanceHandler ? 'vibe' : state.queueOwner,
  })),

  prev: () => {
    const { queue, currentTrack, currentIndex } = get();
    if (queue.length === 0) return;
    const idx =
      currentIndex >= 0 && queue[currentIndex]?.id === currentTrack?.id
        ? currentIndex
        : currentTrack
          ? queue.findIndex((t) => t.id === currentTrack.id)
          : -1;
    if (idx > 0) {
      const prevTrack = queue[idx - 1];
      set({
        currentTrack: prevTrack,
        currentIndex: idx - 1,
        position: 0,
        duration: prevTrack.duration ?? 0,
        isPlaying: true,
      });
    }
  },

  setPosition: (position) => set({ position }),
  setDuration: (duration) => set({ duration }),
  setVolume: (volume) => set({ volume }),
  setPrefetchNext: (prefetchNext) => {
    storePrefetchNext(prefetchNext);
    set({ prefetchNext });
  },
  setCrossfadeMs: (value) => {
    const crossfadeMs = clampCrossfadeMs(value);
    storeCrossfadeMs(crossfadeMs);
    set({ crossfadeMs });
  },
  setCurrentTrack: (currentTrack) =>
    set((state) => ({
      currentTrack,
      currentIndex: currentTrack ? state.queue.findIndex((t) => t.id === currentTrack.id) : -1,
    })),

  setAudioElsewhere: (audioElsewhere) => set({ audioElsewhere }),

  toggleShuffle: () => set((state) => ({ shuffle: !state.shuffle })),
  cycleRepeat: () =>
    set((state) => {
      const modes: RepeatMode[] = ['none', 'all', 'one'];
      const next = modes[(modes.indexOf(state.repeat) + 1) % modes.length];
      return { repeat: next };
    }),
}));

// One subscription instead of a write in playTrack, advance, prev and the
// shuffle pick — every path that changes the track goes through here.
let lastPersistedTrackId = restoredTrack?.id ?? null;
usePlaybackStore.subscribe((state) => {
  const id = state.currentTrack?.id ?? null;
  if (id === lastPersistedTrackId) return;
  lastPersistedTrackId = id;
  storeLastTrack(state.currentTrack);
});
