// Global audio player. Single <audio> element created lazily, reused
// everywhere — submission cards, profile track lists, the home browse
// list, the persistent NowPlayingBar. State (currentId, currentTrack,
// position, duration, isPlaying, volume) is observable via useAudio()
// so any component can render against it without owning the playback.
//
// Volume persists in localStorage so the user's setting survives
// reloads. Currently-playing track metadata is held in the service
// itself (not just the ID) so the NowPlayingBar can render cover +
// title without having to look it up from a separate catalog.

const VOLUME_KEY = 'storytime.volume';

// "Counts-as-a-stream" threshold. Same definition the ingame side
// uses — a listener has to stick around for the first 20 seconds of
// a track before the bump fires. Below this we treat the play as a
// preview / accidental click. The server applies its own cooldown
// on top so a single track can't be bumped more than once a minute
// per user even if the client gets confused and fires twice.
const STREAM_THRESHOLD_S = 20;

const AudioService = (() => {
  let el = null;
  let state = {
    // Identity of the track currently loaded into the <audio>. null
    // when nothing's been played yet OR when the track finished. Set
    // even while paused — pause keeps the ID so a re-click on the
    // same play button resumes instead of reloads.
    currentId: null,

    // Full track object (title, artist, cover, duration). Stored
    // alongside the ID so consumers can render the bar without
    // having to dig the track out of a separate store. Set every
    // time `play()` is called with a track payload.
    currentTrack: null,

    // True when the <audio> is actively playing. Goes false on
    // pause AND on ended. Distinguishes "stopped" from "paused-with-
    // same-track-still-loaded" — both have currentId set, but
    // isPlaying tells them apart.
    isPlaying: false,

    pos: 0,
    duration: 0,
    volume: (() => {
      const raw = parseFloat(localStorage.getItem(VOLUME_KEY));
      return isFinite(raw) && raw >= 0 && raw <= 1 ? raw : 0.8;
    })(),
  };
  // Two subscriber sets so we don't re-render every TrackRow on the
  // <audio>'s `timeupdate` event (~4-10 fires/sec). Status changes
  // (play/pause, track-switch, duration loaded, volume) hit both sets;
  // pure-position ticks (`timeupdate`, `seek`) only hit `subs`. The
  // `useAudio()` hook subscribes to everything (needed by NowPlayingBar
  // + waveform + lyrics). The `useAudioStatus()` hook only listens to
  // status — that's what the row-level play buttons in TrackRow,
  // SubmissionCard etc. use, so they stay quiet while a track ticks
  // through its 4-minute lifetime.
  const subs = new Set();         // pos AND status — fires on every change
  const statusSubs = new Set();   // status only — never fires on timeupdate
  const emitPos = () => subs.forEach(fn => fn(state));
  const emit = () => { subs.forEach(fn => fn(state)); statusSubs.forEach(fn => fn(state)); };

  // Tracks which (trackId) we've already credited a stream for in
  // this load. Cleared whenever a new track gets loaded so each play
  // can fire once. The 20s threshold is checked locally; the server
  // still has the final say (cooldown / self-listen filter).
  let streamedFor = null;

  function ensureEl() {
    if (el) return el;
    el = document.createElement('audio');
    el.preload = 'auto';
    el.volume = state.volume;
    el.addEventListener('timeupdate', () => {
      state = { ...state, pos: el.currentTime, duration: el.duration || state.duration };
      emitPos();
      // Fire the stream bump once the listener has crossed the
      // threshold for the currently-loaded track. Idempotent within
      // the load: `streamedFor` blocks re-firing if the user
      // scrubs back and forth across the 20-s mark.
      if (
        state.currentId &&
        streamedFor !== state.currentId &&
        el.currentTime >= STREAM_THRESHOLD_S
      ) {
        streamedFor = state.currentId;
        // Fire-and-forget — failures are swallowed in Api.stream itself.
        Api.stream(state.currentId);
      }
    });
    el.addEventListener('loadedmetadata', () => {
      state = { ...state, duration: el.duration || 0 };
      emit();
    });
    el.addEventListener('play', () => {
      state = { ...state, isPlaying: true };
      emit();
    });
    el.addEventListener('pause', () => {
      state = { ...state, isPlaying: false };
      emit();
    });
    el.addEventListener('ended', () => {
      state = { ...state, isPlaying: false, pos: 0 };
      emit();
    });
    document.body.appendChild(el);
    return el;
  }

  // Play / pause toggle on the current track if the ID matches;
  // otherwise switch to a new track and start it.
  //
  // Two call signatures supported, in priority order:
  //
  //   play(track)        — full track object with { id, audioUrl,
  //                        title, artist, cover, duration }. Stored
  //                        for the NowPlayingBar to render against.
  //
  //   play(id, url)      — legacy form used by older code that just
  //                        wants to start playback by id+url and
  //                        doesn't care about the bar UI.
  function play(arg1, arg2) {
    const e = ensureEl();
    const isTrackObj = arg1 && typeof arg1 === 'object' && arg1.id;
    const id  = isTrackObj ? arg1.id      : arg1;
    const url = isTrackObj ? arg1.audioUrl : arg2;
    const track = isTrackObj ? arg1 : null;

    // Same track loaded → just toggle. Keep the track object so the
    // bar's metadata doesn't disappear on pause.
    if (state.currentId === id) {
      if (e.paused) e.play().catch(() => {});
      else e.pause();
      // `play`/`pause` event listeners update state.isPlaying for us.
      return;
    }

    if (!url) return;
    e.src = url;
    e.currentTime = 0;
    // Reset the stream-bump latch so the next track gets its own
    // chance to fire once it crosses the threshold.
    streamedFor = null;
    state = {
      ...state,
      currentId: id,
      currentTrack: track || state.currentTrack,
      pos: 0,
      duration: track?.duration || 0,
      isPlaying: false,   // flipped true by the 'play' event below
    };
    e.play().catch(() => {});
    emit();
  }

  function pause() {
    if (el && !el.paused) el.pause();
  }

  function resume() {
    if (el && el.paused && state.currentId) el.play().catch(() => {});
  }

  function stop() {
    if (el) el.pause();
    streamedFor = null;
    state = { ...state, currentId: null, currentTrack: null, isPlaying: false, pos: 0 };
    emit();
  }

  function seek(seconds) {
    if (!el) return;
    const max = el.duration || seconds;
    const safe = Math.max(0, Math.min(max, seconds));
    el.currentTime = safe;
    state = { ...state, pos: safe };
    emit();
  }

  function setVolume(v) {
    const safe = Math.max(0, Math.min(1, v));
    if (el) el.volume = safe;
    localStorage.setItem(VOLUME_KEY, String(safe));
    state = { ...state, volume: safe };
    emit();
  }

  return {
    play, pause, resume, stop, seek, setVolume,
    get: () => state,
    // Full-fat subscription — fires on every state change, including
    // every `timeupdate` tick. NowPlayingBar uses this (waveform +
    // lyrics scroll with `pos`).
    subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); },
    // Status-only subscription — fires only when track identity /
    // playback state / duration / volume change, never on pos ticks.
    // TrackRow + screen-level play buttons use this so they don't
    // re-render at 4-10 Hz during playback.
    subscribeStatus: (fn) => { statusSubs.add(fn); return () => statusSubs.delete(fn); },
  };
})();

window.AudioService = AudioService;

// React hook — returns current state + bound action helpers. Use in
// any component that wants to play/pause or visualise the current
// track. The NowPlayingBar consumes it; so does every play-button-on-
// a-cover throughout the screens.
function useAudio() {
  const [s, setS] = React.useState(AudioService.get());
  React.useEffect(() => AudioService.subscribe(setS), []);
  return {
    ...s,
    play: AudioService.play,
    pause: AudioService.pause,
    resume: AudioService.resume,
    stop: AudioService.stop,
    seek: AudioService.seek,
    setVolume: AudioService.setVolume,
  };
}
window.useAudio = useAudio;

// Lightweight alternative for components that only care about
// identity + play state (play buttons in row lists, mini-players,
// status indicators). Skips the `timeupdate` firehose so consumers
// don't re-render multiple times per second. `pos` is still on the
// returned state object but stale between status changes — that's
// fine for "is this row currently playing?" checks, which only
// depend on `currentId` + `isPlaying`.
function useAudioStatus() {
  const [s, setS] = React.useState(AudioService.get());
  React.useEffect(() => AudioService.subscribeStatus(setS), []);
  return {
    ...s,
    play: AudioService.play,
    pause: AudioService.pause,
    resume: AudioService.resume,
    stop: AudioService.stop,
    seek: AudioService.seek,
    setVolume: AudioService.setVolume,
  };
}
window.useAudioStatus = useAudioStatus;
