/**
 * Player Event Service
 *
 * Listens for Tauri events from the player backend and updates the
 * frontend stores accordingly. This enables push-based updates instead
 * of polling.
 *
 * TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047
 */

import { type UnlistenFn } from "@tauri-apps/api/event";
import { commands, events, type PlayerStatusEvent, type SleepTimerMode } from "$lib/api/bindings";
import { player, playbackPosition, playbackDuration, currentMedia } from "$lib/stores/player";
import { queue, currentQueueItem } from "$lib/stores/queue";
import { playbackMode } from "$lib/stores/playbackMode";
import { sleepTimer, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
import { nextEpisode, nextEpisodeItem as nextEpisodeItemStore } from "$lib/stores/nextEpisode";
import { autoPlayNext } from "$lib/services/nextEpisodeService";
import { preloadUpcomingTracks } from "$lib/services/preload";
import { playerController } from "$lib/player";
import type { MediaItem } from "$lib/api/types";
import { get } from "svelte/store";

// PlayerStatusEvent and SleepTimerMode are generated by tauri-specta and
// imported from $lib/api/bindings — they are the authoritative shapes emitted
// by src-tauri/src/player/events.rs.

let unlistenFn: UnlistenFn | null = null;
let isInitialized = false;

/**
 * Initialize the player event listener.
 * Should be called once when the app starts (e.g., in +layout.svelte).
 */
export async function initPlayerEvents(): Promise<void> {
  if (isInitialized) {
    console.warn("Player events already initialized");
    return;
  }

  try {
    unlistenFn = await events.playerStatusEvent.listen((event) => {
      handlePlayerEvent(event.payload);
    });
    isInitialized = true;
    console.log("Player event listener initialized");
  } catch (e) {
    console.error("Failed to initialize player events:", e);
  }
}

/**
 * Clean up the player event listener.
 * Should be called when the app is destroyed.
 */
export function cleanupPlayerEvents(): void {
  if (unlistenFn) {
    unlistenFn();
    unlistenFn = null;
  }
  isInitialized = false;
}

/**
 * Check if the event listener is initialized.
 */
export function isPlayerEventsInitialized(): boolean {
  return isInitialized;
}

/**
 * Handle incoming player events and update stores.
 */
function handlePlayerEvent(event: PlayerStatusEvent): void {
  // Skip local player events when in remote mode to prevent conflicts
  // EXCEPT during transfer (when local playback is starting)
  const mode = get(playbackMode);
  if (mode.mode === "remote" && !mode.isTransferring) {
    return;
  }

  switch (event.type) {
    case "position_update":
      handlePositionUpdate(event.position, event.duration);
      break;

    case "state_changed":
      handleStateChanged(event.state, event.media_id);
      break;

    case "media_loaded":
      handleMediaLoaded(event.duration);
      break;

    case "playback_ended":
      handlePlaybackEnded();
      break;

    case "buffering":
      // Could show buffering indicator in UI
      console.debug(`Buffering: ${event.percent}%`);
      break;

    case "error":
      handleError(event.message, event.recoverable);
      break;

    case "volume_changed":
      player.setVolume(event.volume);
      player.setMuted(event.muted);
      break;

    case "sleep_timer_changed":
      handleSleepTimerChanged(event.mode, event.remaining_seconds);
      break;

    case "sleep_timer_expired":
      // Backend stops its own playback; this signal lets HTML5 video (which
      // plays outside the backend on Linux) pause itself too.
      // Preferred path: drive the active video adapter directly so the backend
      // has real control authority over the webview element. The legacy
      // sleepTimerExpiredSignal is kept for any remaining subscribers.
      playerController.getActiveAdapter()?.pause();
      sleepTimerExpiredSignal.update((n) => n + 1);
      break;

    case "control_command":
      // Backend-originated control targeting the active frontend player adapter
      // (lockscreen/remote/sleep). Route it to the adapter so a backend intent
      // reaches the webview <video> element.
      handleControlCommand(event.action, event.position);
      break;

    case "show_next_episode_popup":
      handleShowNextEpisodePopup(
        event.current_episode,
        event.next_episode,
        event.countdown_seconds,
        event.auto_advance
      );
      break;

    case "countdown_tick":
      handleCountdownTick(event.remaining_seconds);
      break;

    // queue_changed is handled by the queue store's own listener
    // ($lib/stores/queue), which is the single source of truth for
    // shuffle/repeat/next/previous. No action needed here.
  }
}

/**
 * Handle position update events.
 *
 * TRACES: UR-005, UR-025 | DR-028
 */
function handlePositionUpdate(position: number, duration: number): void {
  player.updatePosition(position, duration);
  // Note: Sleep timer logic is now handled entirely in the Rust backend
}

/**
 * Resolve the duration to seed playing/paused state with.
 *
 * For the same track that is already loaded, the live store duration (kept
 * fresh by media_loaded / position_update events) is authoritative and is
 * preferred — falling back to the runTimeTicks estimate only when the store
 * has no usable duration yet. This avoids clobbering a known-good duration
 * with 0 when runTimeTicks is missing (which would zero the slider's max).
 */
function resolveDuration(currentItem: MediaItem, isSameTrack: boolean): number {
  const estimate = currentItem.runTimeTicks ? currentItem.runTimeTicks / 10000000 : 0;
  if (isSameTrack) {
    const live = get(playbackDuration);
    if (live > 0) {
      return live;
    }
  }
  return estimate;
}

/**
 * Handle state change events.
 *
 * TRACES: UR-005 | DR-001
 */
async function handleStateChanged(state: string, _mediaId: string | null): Promise<void> {
  // Get current media from queue store
  const currentItem = get(currentQueueItem);

  switch (state) {
    case "playing":
    case "paused":
    case "loading":
      // When local playback starts, ensure mode is set to local
      const mode = get(playbackMode);
      if (mode.mode !== "local") {
        console.log("Setting playback mode to local");
        playbackMode.setMode("local");
      }

      if (state === "playing" && currentItem) {
        // Preserve the current position when the same track is already loaded
        // (e.g. resuming from pause, or a spurious PlaybackRestart). Only reset
        // to 0 when switching to a different track. position_update events keep
        // it fresh either way, but resetting unconditionally caused the time to
        // flash to 0:00 on pause/resume.
        const previous = get(currentMedia);
        const isSameTrack = previous?.id === currentItem.id;
        const startPosition = isSameTrack ? get(playbackPosition) : 0;
        const initialDuration = resolveDuration(currentItem, isSameTrack);
        player.setPlaying(currentItem, startPosition, initialDuration);

        // Trigger preloading of upcoming tracks in the background
        preloadUpcomingTracks().catch((e) => {
          // Preload failures are non-critical, already logged in the service
          console.debug("[playerEvents] Preload failed (non-critical):", e);
        });
      } else if (state === "paused" && currentItem) {
        // Keep current position and duration from store. The same track is
        // already loaded on pause, so its live duration (from media_loaded /
        // position_update) is authoritative — recomputing from runTimeTicks
        // would clobber it with 0 when runTimeTicks is missing, forcing the
        // slider's max to 0 and flashing the thumb to the start.
        const previous = get(currentMedia);
        const isSameTrack = previous?.id === currentItem.id;
        const currentPosition = get(playbackPosition);
        const initialDuration = resolveDuration(currentItem, isSameTrack);
        player.setPaused(currentItem, currentPosition, initialDuration);
      } else if (state === "loading" && currentItem) {
        player.setLoading(currentItem);
      }

      break;

    case "idle":
    case "stopped":
      player.setIdle();
      // When local playback stops, revert to idle mode
      const currentMode = get(playbackMode);
      if (currentMode.mode === "local") {
        console.log("Setting playback mode to idle");
        playbackMode.setMode("idle");
      }

      break;
  }
}

/**
 * Handle media loaded event.
 */
function handleMediaLoaded(duration: number): void {
  // Media is now loaded and ready
  // The state_changed event will handle setting the playing state
  console.debug(`Media loaded, duration: ${duration}s`);
}

/**
 * Handle playback ended event.
 * Calls backend to handle autoplay decisions (sleep timer, queue advance, episode popup).
 *
 * TRACES: UR-023, UR-026 | DR-047, DR-029
 */
async function handlePlaybackEnded(): Promise<void> {
  // Call backend to handle autoplay decision (queue advance, sleep timer, episode popup, etc.)
  try {
    await commands.playerOnPlaybackEnded(null, null);
  } catch (e) {
    console.error("[playerEvents] Failed to handle playback ended:", e);
    // Fallback: set idle state on error
    player.setIdle();
  }
}

/**
 * Handle error events.
 */
async function handleError(message: string, recoverable: boolean): Promise<void> {
  console.error(`Playback error (recoverable: ${recoverable}): ${message}`);
  player.setError(message);

  // Stop backend player to prevent orphaned playback
  // This also reports playback stopped to Jellyfin server
  try {
    await commands.playerStop();
    console.log("Backend player stopped after error");
  } catch (e) {
    console.error("Failed to stop player after error:", e);
    // Continue with state cleanup even if stop fails
  }

  // Always return to idle after an error
  player.setIdle();
}

/**
 * Handle sleep timer changed event.
 *
 * TRACES: UR-026 | DR-029
 */
function handleSleepTimerChanged(mode: SleepTimerMode, remainingSeconds: number): void {
  sleepTimer.set({ mode, remainingSeconds });
}

/**
 * Route a backend-originated control command to the active player adapter, so a
 * backend intent (lockscreen/remote/sleep) can drive the webview <video> element
 * that Rust cannot reach directly. No-op when no video adapter is active (audio
 * playback is already fully backend-driven).
 */
function handleControlCommand(action: string, position: number | null): void {
  const adapter = playerController.getActiveAdapter();
  if (!adapter) return;
  switch (action) {
    case "play":
      void adapter.play();
      break;
    case "pause":
      void adapter.pause();
      break;
    case "seek":
      // Backend-driven in-place seek (e.g. lockscreen scrub). The backend has
      // already decided this is a simple position change, so use the element
      // seek primitive with no transcode offset.
      if (position != null) void adapter.seekElement(position, 0);
      break;
    case "stop":
      void adapter.pause();
      break;
    default:
      console.warn("[playerEvents] Unknown control command:", action);
  }
}

/**
 * Handle show next episode popup event.
 *
 * TRACES: UR-023 | DR-047, DR-048
 */
function handleShowNextEpisodePopup(
  currentEpisodeItem: MediaItem,
  nextEpisodeItem: MediaItem,
  countdownSeconds: number,
  autoAdvance: boolean
): void {
  // Update next episode store to show popup
  nextEpisode.showPopup(currentEpisodeItem, nextEpisodeItem, countdownSeconds, autoAdvance);
}

/**
 * Handle countdown tick event.
 * When countdown reaches 0, automatically trigger playback of the next episode.
 */
function handleCountdownTick(remainingSeconds: number): void {
  // Update next episode store with new countdown value
  nextEpisode.updateCountdown(remainingSeconds);

  // Auto-play when countdown reaches 0
  if (remainingSeconds === 0) {
    const episode = get(nextEpisodeItemStore);
    if (episode) {
      autoPlayNext(episode);
    }
  }
}
