import { getSong } from '../api/subsonicLibrary';
import { invoke } from '@tauri-apps/api/core';
import { estimateLivePosition } from '../api/orbit';
import { setDeferHotCachePrefetch } from '../utils/cache/hotCacheGate';
import {
  getPlaybackCacheServerKey,
  getPlaybackIndexKey,
  getPlaybackServerId,
} from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { songToTrack } from '../utils/playback/songToTrack';
import { useAuthStore } from './authStore';
import {
  bumpPlayGeneration,
  getIsAudioPaused,
  getPlayGeneration,
  setIsAudioPaused,
} from './engineState';
import { touchHotCacheOnPlayback } from './hotCacheTouch';
import {
  isReplayGainActive,
  loudnessGainDbForEngineBind,
} from './loudnessGainCache';
import { useOrbitStore } from './orbitStore';
import {
  playbackSourceHintForResolvedUrl,
  recordEnginePlayUrl,
} from './playbackUrlRouting';
import type { PlayerState } from './playerStoreTypes';
import { resolveQueueTrack } from '../utils/library/queueTrackView';
import { promoteCompletedStreamToHotCache } from './promoteStreamCache';
import { syncQueueToServer } from './queueSync';
import { playbackReportPlaying } from './playbackReportSession';
import { resumeRadio } from './radioPlayer';
import { clearAllPlaybackScheduleTimers } from './scheduleTimers';

type SetState = (
  partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
) => void;
type GetState = () => PlayerState;

/**
 * Resume playback from a paused state. Three mutually-exclusive
 * branches:
 *
 * 1. **Orbit guest** — catches the local player up to the host's live
 *    position. The user hit pause at some earlier point; resuming
 *    shouldn't drop them back at a stale local position while the
 *    host is already two songs ahead. Same-track → seek + un-pause;
 *    different-track → `playTrack` with a deferred seek.
 *
 * 2. **Radio** — HTML5 audio resume; no Rust engine involved.
 *
 * 3. **Regular track** — two sub-branches keyed off `getIsAudioPaused`:
 *    - **Warm**: engine still has the stream loaded but paused;
 *      `audio_resume` is enough.
 *    - **Cold**: engine has no loaded stream (app relaunch, or track
 *      ended and user hit play again). Promote any
 *      `stream_completed_cache` to hot disk, refetch the song from
 *      Navidrome for fresh ReplayGain metadata, call `audio_play`,
 *      then seek to the persisted `currentTime`. A `getSong` failure
 *      falls back to the in-memory `currentTrack`.
 */
export function runResume(set: SetState, get: GetState): void {
  clearAllPlaybackScheduleTimers();
  set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });

  // Orbit guest: resume means "catch up to the host's live stream".
  // The user hit pause at some earlier point; resuming shouldn't drop
  // them back at the stale local position while the host is already
  // two songs ahead. Covers PlayerBar, media keys, MPRIS — everything
  // that funnels through resume().
  const orbit = useOrbitStore.getState();
  const hostState = orbit.state;
  if (orbit.role === 'guest' && hostState?.isPlaying && hostState.currentTrack) {
    const trackId = hostState.currentTrack.trackId;
    const targetMs = estimateLivePosition(hostState, Date.now());
    const targetSec = Math.max(0, targetMs / 1000);
    const localTrackId = get().currentTrack?.id;
    void (async () => {
      try {
        const song = await getSong(trackId);
        if (!song) return;
        const track = songToTrack(song);
        const fraction = Math.max(0, Math.min(0.99, targetSec / Math.max(1, track.duration)));
        if (localTrackId === trackId) {
          // Same track: seek + un-pause via the Rust engine directly.
          // Bypasses this resume() branch re-entry via the early return below.
          get().seek(fraction);
          if (getIsAudioPaused()) {
            invoke('audio_resume').catch(console.error);
            setIsAudioPaused(false);
            set({ isPlaying: true });
            playbackReportPlaying(targetSec);
          } else {
            set({ isPlaying: true });
            playbackReportPlaying(targetSec);
          }
        } else {
          // Host has a different track — load it (`_orbitConfirmed=true`
          // skips the bulk gate; single-track play isn't a bulk replace
          // anyway). Seek after a short defer once the engine loads.
          get().playTrack(track, [track], false, true);
          window.setTimeout(() => {
            if (get().currentTrack?.id === trackId) get().seek(fraction);
          }, 400);
        }
      } catch { /* silent */ }
    })();
    return;
  }

  if (get().currentRadio) {
    resumeRadio().catch(console.error);
    set({ isPlaying: true });
    return;
  }
  const { currentTrack, queueItems, queueIndex, currentTime } = get();
  if (!currentTrack) return;
  // ReplayGain album-mode neighbours (resolver cache → placeholder; only their
  // RG tags matter, which a placeholder lacks → fallback dB).
  const coldPrev = queueIndex > 0 && queueItems[queueIndex - 1]
    ? resolveQueueTrack(queueItems[queueIndex - 1]) : null;
  const coldNext = queueIndex + 1 < queueItems.length && queueItems[queueIndex + 1]
    ? resolveQueueTrack(queueItems[queueIndex + 1]) : null;

  if (getIsAudioPaused()) {
    // Rust engine has audio loaded but paused — just resume it.
    invoke('audio_resume').catch(console.error);
    setIsAudioPaused(false);
    set({ isPlaying: true });
    // Mirror pause(): tell the server immediately, don't wait for `audio:playing`.
    playbackReportPlaying(currentTime);
    touchHotCacheOnPlayback(currentTrack.id, getPlaybackCacheServerKey());
  } else {
    // Engine has no loaded paused stream (app relaunch, or track ended and user
    // hits play — `isAudioPaused` is false after `audio:ended`). Flush any
    // `stream_completed_cache` from the prior play to hot disk before resolving URL.
    const gen = bumpPlayGeneration();
    const vol = get().volume;
    set({ isPlaying: true });
    playbackReportPlaying(currentTime);

    void (async () => {
      const authHot = useAuthStore.getState();
      const resumePromoteSid = getPlaybackCacheServerKey();
      if (authHot.hotCacheEnabled && resumePromoteSid) {
        await promoteCompletedStreamToHotCache(
          currentTrack,
          resumePromoteSid,
          authHot.hotCacheDownloadDir || null,
        );
      }
      if (getPlayGeneration() !== gen) return;

      // Fetch fresh track data from server to get replay gain metadata
      getSong(currentTrack.id).then(freshSong => {
        if (getPlayGeneration() !== gen) return;
        const trackToPlay = freshSong ? songToTrack(freshSong) : currentTrack;
        // Update store with fresh track data if available
        if (freshSong) set({ currentTrack: trackToPlay });
        const authStateCold = useAuthStore.getState();
        const replayGainDbCold = resolveReplayGainDb(
          trackToPlay, coldPrev, coldNext,
          isReplayGainActive(), authStateCold.replayGainMode,
        );
        const replayGainPeakCold = isReplayGainActive() ? (trackToPlay.replayGainPeak ?? null) : null;
        const coldServerId = getPlaybackIndexKey();
        setDeferHotCachePrefetch(true);
        const coldUrl = resolvePlaybackUrl(trackToPlay.id, coldServerId);
        set({ currentPlaybackSource: playbackSourceHintForResolvedUrl(trackToPlay.id, coldServerId, coldUrl) });
        recordEnginePlayUrl(trackToPlay.id, coldUrl);
        touchHotCacheOnPlayback(trackToPlay.id, coldServerId);
        invoke('audio_play', {
          url: coldUrl,
          volume: vol,
          durationHint: trackToPlay.duration,
          replayGainDb: replayGainDbCold,
          replayGainPeak: replayGainPeakCold,
          loudnessGainDb: loudnessGainDbForEngineBind(trackToPlay.id),
          preGainDb: authStateCold.replayGainPreGainDb,
          fallbackDb: authStateCold.replayGainFallbackDb,
          manual: false,
          hiResEnabled: useAuthStore.getState().enableHiRes,
          analysisTrackId: trackToPlay.id,
          serverId: coldServerId || null,
          streamFormatSuffix: trackToPlay.suffix ?? null,
          startPaused: false,
        }).then(() => {
          if (getPlayGeneration() === gen && currentTime > 1) {
            invoke('audio_seek', { seconds: currentTime }).catch(console.error);
          }
        }).catch((err: unknown) => {
          if (getPlayGeneration() !== gen) return;
          setDeferHotCachePrefetch(false);
          console.error('[psysonic] audio_play (cold resume) failed:', err);
          set({ isPlaying: false });
        });
        syncQueueToServer(queueItems, trackToPlay, currentTime);
      }).catch(() => {
        if (getPlayGeneration() !== gen) return;
        // Fallback to currentTrack if fetch fails
        const authStateCold = useAuthStore.getState();
        const replayGainDbCold = resolveReplayGainDb(
          currentTrack, coldPrev, coldNext,
          isReplayGainActive(), authStateCold.replayGainMode,
        );
        const replayGainPeakCold = isReplayGainActive() ? (currentTrack.replayGainPeak ?? null) : null;
        const coldServerId = getPlaybackIndexKey();
        setDeferHotCachePrefetch(true);
        const coldUrl = resolvePlaybackUrl(currentTrack.id, coldServerId);
        set({ currentPlaybackSource: playbackSourceHintForResolvedUrl(currentTrack.id, coldServerId, coldUrl) });
        recordEnginePlayUrl(currentTrack.id, coldUrl);
        touchHotCacheOnPlayback(currentTrack.id, coldServerId);
        invoke('audio_play', {
          url: coldUrl,
          volume: vol,
          durationHint: currentTrack.duration,
          replayGainDb: replayGainDbCold,
          replayGainPeak: replayGainPeakCold,
          loudnessGainDb: loudnessGainDbForEngineBind(currentTrack.id),
          preGainDb: authStateCold.replayGainPreGainDb,
          fallbackDb: authStateCold.replayGainFallbackDb,
          manual: false,
          hiResEnabled: useAuthStore.getState().enableHiRes,
          analysisTrackId: currentTrack.id,
          serverId: coldServerId || null,
          streamFormatSuffix: currentTrack.suffix ?? null,
          startPaused: false,
        }).catch((err: unknown) => {
          if (getPlayGeneration() !== gen) return;
          setDeferHotCachePrefetch(false);
          console.error('[psysonic] audio_play (cold resume) failed:', err);
          set({ isPlaying: false });
        });
        syncQueueToServer(queueItems, currentTrack, currentTime);
      });
    })();
  }
}
