<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { page } from "$app/stores";
  import { goto } from "$app/navigation";
  import { commands } from "$lib/api/bindings";
  import { downloadedFilePath, resolveVideoSource } from "$lib/player/localSource";
  import type { PlayQueueRequest } from "$lib/api/bindings";
  import type { MediaItem, MediaKind } from "$lib/api/types";
  import { auth } from "$lib/stores/auth";
  import { library } from "$lib/stores/library";
  import { queue, currentQueueItem, isShuffle, repeatMode, hasNext as hasNextStore, hasPrevious as hasPreviousStore } from "$lib/stores/queue";
  import { downloads, type DownloadInfo } from "$lib/stores/downloads";
  import { playbackPosition, playbackDuration, currentMedia as storeCurrentMedia } from "$lib/stores/player";
  import { get } from "svelte/store";
  import AudioPlayer from "$lib/components/player/AudioPlayer.svelte";
  import VideoPlayer from "$lib/components/player/VideoPlayer.svelte";
  import { shouldReuseActivePlayback, resolvePlayerSurface } from "$lib/components/player/playerSurface";
  import NextEpisodePopup from "$lib/components/player/NextEpisodePopup.svelte";
  import {
    reportPlaybackStart,
    reportPlaybackProgress,
    reportPlaybackStopped,
  } from "$lib/services/playbackReporting";
  import { reportSkippedEpisode, shouldSuppressStopReport } from "$lib/services/skipReporting";
  import { cleanup as cleanupNextEpisode } from "$lib/services/nextEpisodeService";
  import * as html5Adapter from "$lib/player/html5Adapter";

  const itemId = $derived($page.params.id);
  const queueParam = $derived($page.url.searchParams.get("queue"));
  const shuffleParam = $derived($page.url.searchParams.get("shuffle") === "true");
  // When advancing to a next episode we always start from the beginning,
  // even if the episode was previously started or watched.
  const restartParam = $derived($page.url.searchParams.get("restart") === "true");

  // Derive playback context from URL query params
  const playbackContext = $derived.by(() => {
    if (!queueParam) {
      return { type: "single" as const, id: null };
    }

    if (queueParam.startsWith("parent:")) {
      const parentId = queueParam.substring(7);
      return { type: "container" as const, id: parentId };
    }

    return { type: "single" as const, id: null };
  });

  // Use player store for position/duration - updated by event system
  const position = $derived($playbackPosition);
  const duration = $derived($playbackDuration);
  let isPlaying = $state(false);
  // Shuffle/repeat/next/previous are event-driven via the queue store (instant
  // update on queue_changed), not polled.
  const shuffle = $derived($isShuffle);
  const repeat = $derived($repeatMode);
  const hasNext = $derived($hasNextStore);
  const hasPrevious = $derived($hasPreviousStore);
  let currentMedia = $state<MediaItem | null>(null);
  let streamUrl = $state<string | null>(null);
  let mediaSourceId = $state<string | null>(null);
  let isVideo = $state(false);
  let isLive = $state(false); // Whether this is a live stream (Live TV channel) - no seek/resume
  let videoInitialPosition = $state(0); // Position in seconds to seek to after video loads
  let videoNeedsTranscoding = $state(false); // Whether video needs transcoding (HEVC/10-bit)
  let isOfflinePlayback = $state(false); // Whether playing from local file
  let loading = $state(true);
  let error = $state<string | null>(null);
  let showResumeDialog = $state(false);
  let savedProgress = $state<{ positionSeconds: number; progressPercent: number } | null>(null);
  let nextEpisode = $state<MediaItem | null>(null); // Next episode for video skip button

  let pollInterval: ReturnType<typeof setInterval> | null = null;
  let loadedItemId: string | null = null;

  // Which player component to render. Video without a stream URL is "pending"
  // (still resolving), never audio — see playerSurface.ts.
  const surface = $derived(resolvePlayerSurface({ isVideo, streamUrl }));

  onMount(() => {
    // Start position polling (only for audio via MPV backend)
    pollInterval = setInterval(updateStatus, 1000);

    return () => {
      if (pollInterval) clearInterval(pollInterval);
    };
  });

  onDestroy(() => {
    cleanupNextEpisode();
  });

  // Load when itemId changes (handles both initial load and navigation)
  $effect(() => {
    const id = itemId;
    const restart = restartParam;
    if (id && id !== loadedItemId) {
      console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
      // restart=true (advancing to next episode) forces start-from-beginning,
      // bypassing the resume-progress check.
      loadAndPlay(id, restart ? 0 : undefined, restart);
    }
  });

  // Update currentMedia when queue item changes (for skip/next/previous)
  // Only for audio content - video content uses direct loading and shouldn't be affected by audio queue
  $effect(() => {
    const queueItem = $currentQueueItem;
    const currentIsVideo = currentMedia?.kind === "movie" || currentMedia?.kind === "episode";
    if (queueItem && queueItem.id !== currentMedia?.id && !currentIsVideo) {
      currentMedia = queueItem;
    }
  });

  // A playable channel leaf (ChannelFolderItem) has no dedicated item type, so
  // treat it as video when it carries a video media stream.
  function isVideoChannelItem(item: MediaItem): boolean {
    return (
      item.kind === "channelItem" &&
      (item.mediaStreams?.some((s) => s.kind === "video") ?? false)
    );
  }

  async function loadAndPlay(id: string, startPosition?: number, forceRestart = false) {
    loading = true;
    error = null;
    loadedItemId = id;
    let retrievedProgressSeconds: number | null = null;

    try {
      console.log("loadAndPlay: Loading item", id);
      // Load item details
      const item = await library.loadItem(id);
      console.log("loadAndPlay: Loaded item", item.name, "kind:", item.kind);
      currentMedia = item;

      // Check if this is a non-playable collection type that should be viewed in library instead
      const collectionKinds: MediaKind[] = ["album", "artist", "series", "season", "folder", "playlist", "channel"];
      if (item.kind && collectionKinds.includes(item.kind)) {
        console.log("loadAndPlay: Redirecting collection type to library:", item.kind);
        goto(`/library/${id}`);
        return;
      }

      // Determine if this is video content (Movie, Episode, live TV channels, and
      // channel leaf items that carry a video stream).
      isLive = item.kind === "liveChannel";
      isVideo = item.kind === "movie" || item.kind === "episode" || isLive || isVideoChannelItem(item);

      // If this track is already playing in the backend, just show the UI
      // without restarting playback (e.g., when expanding from MiniPlayer).
      // Audio only, and forceRestart bypasses it so advancing to the next
      // episode always restarts from the beginning — see playerSurface.ts for
      // why video must never take this shortcut.
      const alreadyPlayingMedia = get(storeCurrentMedia);
      if (
        shouldReuseActivePlayback({
          requestedId: id,
          activeMediaId: alreadyPlayingMedia?.id,
          isVideo,
          startPosition,
          forceRestart,
        })
      ) {
        console.log("loadAndPlay: Track already playing, showing UI without restarting");
        isPlaying = true;
        loading = false;
        // hasNext/hasPrevious come from the event-driven queue store.
        return;
      }

      // When switching to video, stop audio playback and clear the queue
      // This prevents audio from continuing in the background and clears stale state
      if (isVideo) {
        try {
          await commands.playerStop();
          queue.clear();
          console.log("loadAndPlay: Stopped audio backend for video playback");
        } catch (e) {
          // Ignore - player may not have been playing
        }
      }

      // Check for saved progress if no start position specified.
      // When forceRestart is set (advancing to a next episode) we always start
      // from the beginning, skipping the resume check and resume dialog.
      const userId = auth.getUserId();
      console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);

      // Live streams have no fixed position - never resume.
      if (!startPosition && !forceRestart && userId && !isLive) {
        try {
          const progress = await commands.storageGetPlaybackProgress(userId, id);
          console.log("Resume check - retrieved progress:", progress);

          if (progress && progress.positionMs > 0 && item.durationMs) {
            const positionSeconds = progress.positionMs / 1000;
            const totalSeconds = item.durationMs / 1000;
            const progressPercent = (positionSeconds / totalSeconds) * 100;

            console.log("Resume check - positionSeconds:", positionSeconds, "totalSeconds:", totalSeconds, "progressPercent:", progressPercent);

            // Store for later use regardless of whether dialog is shown
            retrievedProgressSeconds = positionSeconds;

            // Show resume dialog if watched > 30 seconds and < 90% complete
            if (positionSeconds > 30 && progressPercent < 90) {
              console.log("Resume check - SHOWING RESUME DIALOG");
              savedProgress = { positionSeconds, progressPercent };
              showResumeDialog = true;
              loading = false;
              return; // Wait for user decision
            } else {
              console.log("Resume check - NOT showing dialog. Position > 30?", positionSeconds > 30, "Progress < 90?", progressPercent < 90);
            }
          } else {
            console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionMs, "Has runtime?", !!item.durationMs);
          }
        } catch (e) {
          console.error("Failed to check saved progress:", e);
          // Continue with normal playback
        }
      } else {
        console.log("Resume check - Skipped. Reason:", !userId ? "No userId" : "Has startPosition");
      }

      // Check if this item is downloaded locally
      const downloadsState = get(downloads);
      const localDownload = Object.values(downloadsState.downloads).find(
        (d: DownloadInfo) => d.itemId === id && d.status === "completed"
      );

      if (localDownload) {
        // Use local file for playback
        console.log("loadAndPlay: Found local download, using offline playback:", localDownload.filePath);
        isOfflinePlayback = true;

        // Get the storage path and resolve the file's location. A completed
        // row already holds an absolute path (the worker rewrites it on
        // completion), so it must not be rooted again — see downloadedFilePath.
        // TRACES: UR-071 | DR-133
        const storagePath = await commands.storageGetPath();
        const fullPath = downloadedFilePath(storagePath, localDownload.filePath);
        console.log("loadAndPlay: Full local path:", fullPath);

        // Serve the file over the loopback media server rather than the asset
        // protocol: the asset protocol answers a range-less request with the
        // entire file, so a downloaded film never finished loading. Rust mints
        // the URL (it holds the port and the per-session token).
        // TRACES: UR-071 | DR-137
        const localUrl = await commands.mediaLocalUrl(fullPath);
        console.log("loadAndPlay: Local media URL resolved");

        if (isVideo) {
          // Local video files don't need transcoding and support native seeking
          streamUrl = localUrl;
          videoNeedsTranscoding = false;
          // Use explicit startPosition, or fall back to retrieved progress from database
          const effectivePosition = startPosition ?? retrievedProgressSeconds ?? 0;
          videoInitialPosition = effectivePosition;
        } else {
          // Local audio playback via MPV backend
          console.log("loadAndPlay: Using MPV backend for offline audio");
          // Use player_play_tracks - backend fetches all metadata from single ID
          const repo = auth.getRepository();
          const repositoryHandle = repo.getHandle();

          await commands.playerPlayTracks(repositoryHandle, {
            trackIds: [item.id],
            startIndex: 0,
            shuffle: false,
            context: {
              type: "search",
              searchQuery: "",
            },
          });
          if (startPosition) {
            await commands.playerSeek(startPosition);
          }
        }
      } else {
        // Online playback - get playback info from server
        isOfflinePlayback = false;
        const repo = auth.getRepository();

        if (isLive) {
          // Live TV channels must be "opened" before streaming; the server returns
          // a ready-to-play HLS transcoding URL. No resume, no seek, no progress.
          console.log("loadAndPlay: Opening live stream for channel:", id);
          const liveInfo = await repo.openLiveStream(id);
          console.log("loadAndPlay: Live stream URL:", liveInfo.streamUrl);
          mediaSourceId = liveInfo.mediaSourceId;
          streamUrl = liveInfo.streamUrl;
          videoNeedsTranscoding = true;
          videoInitialPosition = 0;
          isPlaying = true;
          loading = false;
          return;
        }

        console.log("loadAndPlay: Getting playback info");
        const playbackInfo = await repo.getPlaybackInfo(id);
        console.log("loadAndPlay: Got playback info, mediaSourceId:", playbackInfo.mediaSourceId);

        if (isVideo) {
          // Playback API now detects HEVC/10-bit and returns transcoded URL when needed
          console.log("loadAndPlay: Using video stream, directPlay:", playbackInfo.directPlay, "needsTranscoding:", playbackInfo.needsTranscoding);
          mediaSourceId = playbackInfo.mediaSourceId;

          // Prefer a completed download over streaming. Audio has done this
          // since the queue is built; video previously always streamed, so a
          // downloaded film re-spent bandwidth already spent and would not play
          // at all offline. Rust returns null when nothing is downloaded or the
          // file has gone, so this falls back to the server on its own.
          // TRACES: UR-071 | DR-123
          // A downloaded file is served over the loopback media server, not the
          // asset protocol — see DR-137. The URL is minted up front because
          // resolveVideoSource stays pure/synchronous.
          // TRACES: UR-071 | DR-123, DR-137
          const localPath = await commands.playerLocalMediaPath(id);
          const localUrl = localPath ? await commands.mediaLocalUrl(localPath) : null;
          const source = resolveVideoSource({
            localPath,
            remoteUrl: playbackInfo.streamUrl,
            remoteNeedsTranscoding: playbackInfo.needsTranscoding,
            toAssetUrl: () => localUrl ?? "",
          });

          streamUrl = source.url;
          videoNeedsTranscoding = source.needsTranscoding;
          console.log(
            source.isLocal
              ? "loadAndPlay: Playing downloaded file from disk"
              : `loadAndPlay: Using stream URL: ${streamUrl}`
          );

          // Set initial position for video player to seek to after load
          // Use explicit startPosition, or fall back to retrieved progress from database
          // For transcoded content, we need to request a new stream with StartTimeTicks
          const effectivePosition = startPosition ?? retrievedProgressSeconds ?? 0;
          if (effectivePosition > 0) {
            if (videoNeedsTranscoding) {
              // For transcoded streams, get a new URL starting at the position
              console.log("loadAndPlay: Getting transcoded stream starting at:", effectivePosition);
              streamUrl = await repo.getVideoStreamUrl(id, mediaSourceId ?? undefined, effectivePosition);
            } else {
              // For direct streams, we'll seek after load
              videoInitialPosition = effectivePosition;
              console.log("loadAndPlay: Will seek to position after load:", videoInitialPosition);
            }
          } else {
            videoInitialPosition = 0;
          }
        } else {
          // For audio, use MPV backend
          console.log("loadAndPlay: Using MPV backend for audio");

        // Check if we have a queue parameter (e.g., queue=parent:albumId)
        const queueParamValue = queueParam;
        if (queueParamValue?.startsWith("parent:")) {
          const parentId = queueParamValue.substring(7); // Remove "parent:" prefix
          console.log("loadAndPlay: Loading queue from parent:", parentId);

          // Fetch all tracks from the parent (album/playlist)
          const result = await repo.getItems(parentId, {
            sortBy: "SortName",
            sortOrder: "Ascending",
            limit: 500,
          });
          const audioTracks = result.items.filter(t => t.kind === "track");

          if (audioTracks.length > 0) {
            // Find the index of the current item in the tracks
            const startIndex = audioTracks.findIndex(t => t.id === id);
            const actualStartIndex = startIndex >= 0 ? startIndex : 0;

            console.log("loadAndPlay: Building queue with", audioTracks.length, "tracks, startIndex:", actualStartIndex);

            // Build queue items with stream URLs
            // Add error handling and logging for each track
            const queueItems = await Promise.all(audioTracks.map(async (t, idx) => {
              try {
                console.log(`loadAndPlay: Fetching stream URL for track ${idx + 1}/${audioTracks.length}: ${t.name}`);
                const trackStreamUrl = await repo.getAudioStreamUrl(t.id);
                if (!trackStreamUrl) {
                  console.error(`loadAndPlay: Empty stream URL for track: ${t.name}`);
                  throw new Error(`Failed to get stream URL for ${t.name}`);
                }
                return {
                  id: t.id,
                  title: t.name,
                  artist: t.artists?.join(", ") || null,
                  album: t.albumName || null,
                  duration: t.durationMs ? t.durationMs / 1000 : null,
                  artworkUrl: t.imageId
                    ? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.imageId })
                    : null,
                  mediaType: "audio",
                  streamUrl: trackStreamUrl,
                  jellyfinItemId: t.id,
                };
              } catch (e) {
                console.error(`loadAndPlay: Failed to build queue item for track ${t.name}:`, e);
                throw e; // Re-throw to fail fast and show error to user
              }
            }));

            // Use player_play_queue to set up the backend queue
            await commands.playerPlayQueue({
              items: queueItems,
              startIndex: actualStartIndex,
              shuffle: shuffleParam,
            } as unknown as PlayQueueRequest);

            // Queue will auto-update from Rust backend event
            console.log("loadAndPlay: Successfully set up queue with", audioTracks.length, "tracks");
          } else {
            // Fallback to single item playback
            console.log("loadAndPlay: No audio tracks found in parent, falling back to single item");
            // Use player_play_tracks - backend fetches all metadata from single ID
            const repo = auth.getRepository();
            const repositoryHandle = repo.getHandle();

            await commands.playerPlayTracks(repositoryHandle, {
              trackIds: [item.id],
              startIndex: 0,
              shuffle: false,
              context: {
                type: "search",
                searchQuery: "",
              },
            });

            // Queue will auto-update from Rust backend event
            console.log("loadAndPlay: Set queue with single item:", item.name);
          }
        } else {
          // No queue parameter - single item playback
          // Use player_play_tracks - backend fetches all metadata from single ID
          const repo = auth.getRepository();
          const repositoryHandle = repo.getHandle();

          await commands.playerPlayTracks(repositoryHandle, {
            trackIds: [item.id],
            startIndex: 0,
            shuffle: false,
            context: {
              type: "search",
              searchQuery: "",
            },
          });

          // Queue will auto-update from Rust backend event
          console.log("loadAndPlay: Set queue with single item:", item.name);
        }

          // Seek to start position if provided
          if (startPosition) {
            await commands.playerSeek(startPosition);
          }
        }
      }

      isPlaying = true;
      loading = false;

      // Fetch next episode for video episodes (for skip button)
      console.log("[NextEpisode] Post-load check: isVideo=", isVideo, "currentMedia=", currentMedia?.kind, currentMedia?.name);
      if (isVideo && currentMedia) {
        fetchNextEpisode(currentMedia);
      } else {
        console.log("[NextEpisode] Skipped fetchNextEpisode - isVideo:", isVideo, "currentMedia:", !!currentMedia);
      }
    } catch (e) {
      console.error("loadAndPlay error:", e);
      // Show detailed error including the full error object
      if (e instanceof Error) {
        error = `${e.name}: ${e.message}`;
      } else {
        error = `Unknown error: ${JSON.stringify(e)}`;
      }
      loading = false;
    }
  }

  function handleResumeFromBeginning() {
    showResumeDialog = false;
    savedProgress = null;
    const id = itemId;
    if (id) {
      // forceRestart bypasses the resume-progress check; without it, passing a
      // start position of 0 is treated as "no position" (`!startPosition`), which
      // re-runs the resume check and re-shows this very dialog in a loop.
      loadAndPlay(id, 0, true);
    }
  }

  function handleResumeFromSaved() {
    showResumeDialog = false;
    const position = savedProgress?.positionSeconds ?? 0;
    savedProgress = null;
    const id = itemId;
    if (id) {
      loadAndPlay(id, position);
    }
  }

  async function updateStatus() {
    try {
      const status = await commands.playerGetStatus();

      if (status.state.kind === "playing" || status.state.kind === "paused") {
        isPlaying = status.state.kind === "playing";
        // Note: position/duration are now derived from player store (updated by events)
      }
      // shuffle/repeat/hasNext/hasPrevious come from the queue store (event-driven).
    } catch (e) {
      // Ignore polling errors
    }
  }

  function handleClose() {
    // Use browser history to go back to the previous page
    // This ensures users return to where they came from (album, series, search, etc.)
    if (window.history.length > 1) {
      window.history.back();
    } else {
      // Fallback to library if no history (e.g., direct URL access)
      goto("/library");
    }
  }

  /**
   * Handle video seeking by requesting a new stream URL starting at the given position.
   * Transcoded streams don't support native seeking, so we restart from a new position.
   */
  async function handleVideoSeek(positionSeconds: number, audioStreamIndex?: number): Promise<string> {
    const repo = auth.getRepository();
    const id = itemId;
    if (!id) throw new Error("No item ID");
    return repo.getVideoStreamUrl(id, mediaSourceId ?? undefined, positionSeconds, audioStreamIndex);
  }

  // Playback reporting callbacks
  //
  // These receive the reporting item id from the VideoPlayer (its own media.id),
  // NOT the live URL param (itemId). During autoplay the URL flips to the next
  // episode before the outgoing VideoPlayer's onDestroy fires its final
  // reportStop. Keying off itemId would stamp the old episode's near-end
  // position onto the new episode, making it resume at ~99% (or pop the resume
  // dialog). The VideoPlayer always knows which media it actually played.
  function handleReportStart(positionSeconds: number, reportId?: string) {
    const id = reportId ?? itemId;
    const context = playbackContext; // playbackContext is a derived value, not a function
    if (id) {
      reportPlaybackStart(id, positionSeconds, context.type, context.id);
    }
    // Mirror HTML5 <video> state into the Rust PlayerController so it is the
    // single source of truth for video playback (see html5Adapter.ts). The
    // element lives in the webview and Rust cannot observe it directly.
    html5Adapter.reportState("playing", id ?? null);
    html5Adapter.reportPosition(positionSeconds, get(playbackDuration), { force: true });
  }

  function handleReportProgress(positionSeconds: number, isPaused: boolean, reportId?: string) {
    const id = reportId ?? itemId;
    if (id) {
      reportPlaybackProgress(id, positionSeconds, isPaused);
    }
    // Feed the Rust controller the current position and play/pause state.
    html5Adapter.reportState(isPaused ? "paused" : "playing", id ?? null);
    html5Adapter.reportPosition(positionSeconds, get(playbackDuration), { force: true });
  }

  function handleReportStop(positionSeconds: number, reportId?: string) {
    const id = reportId ?? itemId;
    // A skipped episode was already recorded as fully watched. Its unmount stop
    // report arrives after the skip navigation carrying the mid-episode
    // position; letting it through would undo that and restore the partial
    // progress bar.
    if (id && !shouldSuppressStopReport(id)) {
      reportPlaybackStopped(id, positionSeconds);
    }
    // Intentionally do NOT emit a "stopped" player state here. This runs on both
    // natural end-of-video (an autoplay handoff the backend's on_video_playback_ended
    // owns) and on player close/unmount (where player_stop already drives the
    // backend state). Emitting StateChanged{stopped} on natural end flips the
    // player/mode to idle mid-handoff and suppresses next-episode auto-advance —
    // the "sleep timer pauses at the end of an episode instead of continuing" bug.
  }

  async function handleVideoEnded() {
    // Call backend to handle autoplay decision (works on both Android and Linux)
    // Pass the item ID and repository handle so the backend can look up the item
    // and check for next episodes. HTML5 video plays independently of the Rust
    // backend queue, so the backend needs these to know what just finished.
    const mediaId = currentMedia?.id ?? null;
    console.log("[AutoPlay] Video ended. currentMedia:", mediaId, currentMedia?.name, "itemId (URL):", itemId);
    try {
      const repo = auth.getRepository();
      const repoHandle = repo.getHandle();
      await commands.playerOnPlaybackEnded(mediaId, repoHandle);
    } catch (e) {
      console.error("[AutoPlay] Failed to handle playback ended:", e);
    }
  }

  async function fetchNextEpisode(media: MediaItem) {
    nextEpisode = null;
    console.log("[NextEpisode] fetchNextEpisode called:", { kind: media.kind, seriesId: media.seriesId, seasonId: media.seasonId, indexNumber: media.indexNumber, id: media.id, name: media.name });
    if (media.kind !== "episode" || !media.seasonId || media.indexNumber == null) {
      console.log("[NextEpisode] Skipping - not an episode or missing seasonId/indexNumber");
      return;
    }
    try {
      const repo = auth.getRepository();
      // Fetch all episodes in the season sorted by episode number
      const result = await repo.getItems(media.seasonId, { sortBy: "IndexNumber", sortOrder: "Ascending", limit: 500 });
      const episodes = result.items.filter(e => e.kind === "episode");
      console.log("[NextEpisode] Season has", episodes.length, "episodes, current index:", media.indexNumber);

      // Find the episode after the current one by index number
      const currentIdx = episodes.findIndex(e => e.id === media.id);
      if (currentIdx >= 0 && currentIdx < episodes.length - 1) {
        nextEpisode = episodes[currentIdx + 1];
        console.log("[NextEpisode] Set nextEpisode:", nextEpisode.name, "index:", nextEpisode.indexNumber);
      } else {
        console.log("[NextEpisode] No next episode in season (current position:", currentIdx, "of", episodes.length, ")");
      }
    } catch (e) {
      console.error("[NextEpisode] Failed to fetch next episode:", e);
    }
  }

  function handleSkipToNextEpisode() {
    if (nextEpisode) {
      // Skipping means "I'm done with this one" — record the outgoing episode as
      // fully watched rather than leaving a mid-episode resume point behind. This
      // also arms suppression of the VideoPlayer's unmount stop report, which
      // would otherwise fire after navigation and overwrite the 100% progress
      // with the partial position (see skipReporting.ts).
      const skippedId = currentMedia?.id ?? itemId ?? null;
      void reportSkippedEpisode(skippedId);

      // Use replaceState so "close/back" returns to the library, not the previous episode.
      // restart=true so advancing to the next episode always starts from the beginning,
      // even if it was previously started or watched.
      goto(`/player/${nextEpisode.id}?restart=true`, { replaceState: true });
    }
  }

  function formatTime(seconds: number): string {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);

    if (hours > 0) {
      return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
    }
    return `${minutes}:${secs.toString().padStart(2, '0')}`;
  }
</script>

{#if showResumeDialog && savedProgress}
  <div class="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
    <div class="bg-[var(--color-surface)] rounded-lg p-6 max-w-md mx-4 shadow-xl">
      <h2 class="text-xl font-semibold mb-4">Resume Playback?</h2>
      <p class="text-gray-300 mb-2">
        You've watched {savedProgress.progressPercent.toFixed(0)}% of this {isVideo ? 'video' : 'audio'}.
      </p>
      <p class="text-gray-400 text-sm mb-6">
        Resume from {formatTime(savedProgress.positionSeconds)} or start from the beginning?
      </p>
      <div class="flex gap-3">
        <button
          onclick={handleResumeFromBeginning}
          class="flex-1 px-4 py-3 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
        >
          Start from Beginning
        </button>
        <button
          onclick={handleResumeFromSaved}
          class="flex-1 px-4 py-3 bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin-hover)] rounded-lg transition-colors font-semibold"
        >
          Resume
        </button>
      </div>
    </div>
  </div>
{:else if error}
  <div class="fixed inset-0 bg-[var(--color-background)] flex items-center justify-center z-50 p-4">
    <div class="text-center max-w-lg">
      <p class="text-red-400 mb-4 text-lg font-semibold">Playback Error</p>
      <pre class="text-red-300 mb-4 text-left bg-black/30 p-4 rounded overflow-auto max-h-48 text-sm">{error}</pre>
      <button
        onclick={handleClose}
        class="px-4 py-2 bg-[var(--color-jellyfin)] rounded-lg"
      >
        Back to Library
      </button>
    </div>
  </div>
{:else if loading || surface === "pending"}
  <!-- "pending" = video whose stream URL has not resolved yet. Showing the
       spinner keeps it out of the audio player. -->
  <div class="fixed inset-0 bg-[var(--color-background)] flex items-center justify-center z-50">
    <div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
  </div>
{:else if surface === "video" && streamUrl}
  <VideoPlayer
    media={currentMedia}
    {streamUrl}
    mediaSourceId={mediaSourceId ?? undefined}
    initialPosition={videoInitialPosition}
    needsTranscoding={videoNeedsTranscoding}
    {isLive}
    onClose={handleClose}
    onSeek={handleVideoSeek}
    onReportStart={handleReportStart}
    onReportProgress={handleReportProgress}
    onReportStop={handleReportStop}
    onEnded={handleVideoEnded}
    hasNext={nextEpisode !== null}
    onNext={handleSkipToNextEpisode}
  />
  <NextEpisodePopup />
{:else}
  <AudioPlayer
    media={currentMedia}
    {isPlaying}
    {position}
    {duration}
    {shuffle}
    {repeat}
    {hasNext}
    {hasPrevious}
    onClose={handleClose}
  />
{/if}
