/**
 * Playback mode store - Thin wrapper over Rust PlaybackModeManager
 *
 * Manages transitions between Local (device playback) and Remote (controlling
 * another Jellyfin session) playback modes.
 *
 * Most business logic moved to Rust (src-tauri/src/playback_mode/mod.rs)
 *
 * TRACES: UR-010 | IR-012 | DR-037
 */

import { writable, get, derived } from "svelte/store";
import { commands, events } from "$lib/api/bindings";
import { sessions, selectedSession } from "./sessions";
import { auth } from "./auth";
import { ticksToSeconds } from "$lib/utils/playbackUnits";

export type PlaybackMode = "local" | "remote" | "idle";

interface PlaybackModeState {
  mode: PlaybackMode;
  remoteSessionId: string | null;
  isTransferring: boolean;
  transferError: string | null;
}

interface RustPlaybackMode {
  type: "local" | "remote" | "idle";
  session_id?: string;
}

function createPlaybackModeStore() {
  const initialState: PlaybackModeState = {
    mode: "idle",
    remoteSessionId: null,
    isTransferring: false,
    transferError: null,
  };

  const { subscribe, update } = writable<PlaybackModeState>(initialState);

  // Track ongoing transfer promise to allow cancellation
  let currentTransferAbort: (() => void) | null = null;

  /**
   * Refresh mode from Rust backend
   */
  async function refreshMode(): Promise<void> {
    try {
      const rustMode = (await commands.playbackModeGetCurrent()) as RustPlaybackMode;
      const remoteSessionId = rustMode.type === "remote" ? rustMode.session_id || null : null;

      update((s) => ({
        ...s,
        mode: rustMode.type,
        remoteSessionId,
      }));
      // Keep the selected session aligned so the merged UI stores follow the
      // authoritative mode.
      sessions.selectSession(remoteSessionId);
    } catch (error) {
      console.error("Failed to get playback mode:", error);
    }
  }

  /**
   * Set playback mode directly (for internal use)
   */
  function setMode(mode: PlaybackMode, remoteSessionId: string | null = null): void {
    update((s) => ({ ...s, mode, remoteSessionId }));
  }

  /**
   * Transfer playback from local to remote session
   * Rust backend handles all the heavy lifting:
   * - Sends play command with StartPositionTicks
   * - Polls remote session until track loads
   * - Stops local playback
   */
  async function transferToRemote(
    sessionId: string | null | undefined,
    currentPosition?: number,
  ): Promise<void> {
    console.log("[PlaybackMode] Transferring to remote session:", sessionId);
    update((s) => ({ ...s, isTransferring: true, transferError: null }));

    let aborted = false;
    currentTransferAbort = () => {
      aborted = true;
      update((s) => ({
        ...s,
        isTransferring: false,
        transferError: "Transfer cancelled",
      }));
    };

    try {
      // Pass the caller's current local position so the remote resumes where we
      // are. The backend can't reliably read this itself: on Linux video plays in
      // the HTML5 <video> element and the MPV backend reports 0. A null override
      // falls back to the backend position (correct for Linux audio via MPV).
      const positionOverride =
        currentPosition !== undefined && currentPosition > 0 ? currentPosition : null;

      // Rust handles everything - just wait for it to complete
      // It includes its own 5-second timeout for track loading
      console.log("[PlaybackMode] About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId, "position:", positionOverride);
      await commands.playbackModeTransferToRemote(sessionId ?? "", positionOverride);
      console.log("[PlaybackMode] Invoke completed successfully");

      if (aborted) {
        console.log("[PlaybackMode] Transfer was cancelled");
        return;
      }

      // Update local state
      sessions.selectSession(sessionId ?? null);
      update((s) => ({
        ...s,
        mode: "remote",
        remoteSessionId: sessionId ?? null,
        isTransferring: false,
      }));

      console.log("[PlaybackMode] Successfully transferred to remote");
    } catch (error) {
      if (aborted) {
        console.log("[PlaybackMode] Transfer was cancelled");
        return;
      }

      const message = error instanceof Error ? error.message : "Failed to transfer playback";
      update((s) => ({
        ...s,
        isTransferring: false,
        transferError: message,
      }));
      console.error("Transfer to remote failed:", error);
      throw error;
    } finally {
      currentTransferAbort = null;
      // Snap back to whatever the Rust manager actually settled on. If any step
      // above threw mid-transfer, the optimistic update may not match reality;
      // Rust is authoritative, so reconcile to it.
      await refreshMode();
    }
  }

  /**
   * Transfer playback from remote to local
   *
   * Note: Currently hybrid - Rust stops remote, but TypeScript handles
   * loading media since repository isn't migrated yet (Phase 3).
   * Will be fully migrated to Rust after Phase 3.
   */
  async function transferToLocal(): Promise<void> {
    console.log("[PlaybackMode] Transferring to local");
    update((s) => ({ ...s, isTransferring: true, transferError: null }));

    let aborted = false;
    currentTransferAbort = () => {
      aborted = true;
      update((s) => ({
        ...s,
        isTransferring: false,
        transferError: "Transfer cancelled",
      }));
    };

    try {
      const currentMode = get({ subscribe });
      if (currentMode.mode !== "remote" || !currentMode.remoteSessionId) {
        throw new Error("Not in remote mode");
      }

      // Get current remote session state
      const session = get(selectedSession);
      if (!session || !session.nowPlayingItem) {
        // No active playback on remote, just switch to local mode
        sessions.selectSession(null);
        update((s) => ({
          ...s,
          mode: "local",
          remoteSessionId: null,
          isTransferring: false,
        }));
        return;
      }

      const nowPlaying = session.nowPlayingItem;
      const positionTicks = session.playState?.positionTicks ?? 0;
      const positionSeconds = ticksToSeconds(positionTicks);

      // Handle both camelCase and PascalCase field names (API might return either)
      const itemId = (nowPlaying as any).id || (nowPlaying as any).Id;
      const itemName = (nowPlaying as any).name || (nowPlaying as any).Name;

      console.log("[PlaybackMode] Current remote item:", itemName, "position:", positionSeconds, "id:", itemId);

      if (!itemId) {
        throw new Error("Cannot transfer: remote item has no ID");
      }

      if (aborted) return;

      // Get repository for handle (backend will fetch playback info via player_play_tracks)
      const repository = auth.getRepository();

      // Mark the whole sequence as a transfer in the *Rust* manager too. Without
      // this, player_play_tracks sees mode=Remote and casts the track back to the
      // remote session instead of playing it locally (the frontend's own
      // isTransferring flag is invisible to Rust). Cleared in `finally`.
      await commands.playbackModeSetTransferring(true);

      // Start local playback (events allowed through because isTransferring=true)
      // Use player_play_tracks - backend fetches all metadata from single ID
      const repositoryHandle = repository.getHandle();

      // Pass the resume position so the backend seeks at load time. Doing the
      // seek here (rather than a delayed playerSeek) avoids the race where the
      // media isn't loaded yet and the seek is lost, restarting from 0.
      await commands.playerPlayTracks(repositoryHandle, {
        trackIds: [itemId],
        startIndex: 0,
        shuffle: false,
        startPosition: positionSeconds,
        context: {
          type: "search",
          searchQuery: "",
        },
      });

      if (aborted) return;

      // Let Rust handle stopping remote playback
      await commands.playbackModeTransferToLocal(itemId, positionTicks);

      if (aborted) return;

      // Finalize transfer - now update mode to local
      sessions.selectSession(null);
      update((s) => ({
        ...s,
        mode: "local",
        remoteSessionId: null,
        isTransferring: false,
      }));

      console.log("[PlaybackMode] Successfully transferred to local");
    } catch (error) {
      if (aborted) {
        console.log("[PlaybackMode] Transfer was cancelled");
        return;
      }

      const message = error instanceof Error ? error.message : "Failed to transfer playback";
      update((s) => ({
        ...s,
        isTransferring: false,
        transferError: message,
      }));
      console.error("Transfer to local failed:", error);
      throw error;
    } finally {
      // Always lower the Rust transferring flag so it can't stick on if any step
      // above threw (transfer_to_local lowers it on success, but not if we never
      // reached it). Safe to call unconditionally.
      try {
        await commands.playbackModeSetTransferring(false);
      } catch (e) {
        console.warn("[PlaybackMode] Failed to clear transferring flag:", e);
      }
      currentTransferAbort = null;
      // Reconcile to the authoritative Rust mode in case a step above threw and
      // left our optimistic state inconsistent (see transferToRemote).
      await refreshMode();
    }
  }

  /**
   * Monitor remote session for disconnection with grace period.
   * Requires multiple consecutive misses before declaring disconnection
   * to tolerate transient network hiccups.
   */
  function initializeSessionMonitoring(): void {
    let consecutiveMisses = 0;
    const DISCONNECT_THRESHOLD = 3; // ~6s at 2s polling interval

    // The lockscreen Stop button (while casting) asks the native side to
    // disconnect from the remote session and resume locally. The remote->local
    // transfer must be driven here because it reloads the media item locally,
    // which only the frontend can currently do.
    events.playerStatusEvent.listen((event) => {
      if (event.payload.type === "remote_disconnect_requested") {
        const currentState = get({ subscribe });
        if (currentState.mode === "remote") {
          console.log("[PlaybackMode] Lockscreen requested disconnect; transferring to local");
          transferToLocal().catch((e) =>
            console.error("[PlaybackMode] Lockscreen-triggered transfer failed:", e),
          );
        }
        return;
      }

      // The Rust PlaybackModeManager is the single source of truth for routing.
      // Reconcile our mirror store to it whenever it changes, so the UI and the
      // event filter in playerEvents.ts can't drift and start routing controls to
      // the wrong device. We deliberately do NOT reconcile while a transfer is in
      // flight: transfers emit intermediate mode changes (and briefly hold the
      // transferring flag), and the transfer functions own the final state.
      if (event.payload.type === "playback_mode_changed") {
        const currentState = get({ subscribe });
        if (currentState.isTransferring) {
          return;
        }
        const mode = event.payload.mode as PlaybackMode;
        const remoteSessionId =
          mode === "remote" ? event.payload.session_id ?? null : null;

        // Ignore no-op re-broadcasts. The backend re-emits on every set_mode, and
        // local playback drives set_mode("local") from BOTH the frontend
        // (handleStateChanged) and Rust, so the same mode arrives repeatedly. If
        // we reconciled unconditionally we'd re-run selectSession(null) on each
        // one, deselecting the remote session mid-cast and tripping the
        // disconnect-to-idle watchdog (breaking the lockscreen card, remote
        // volume, and — via the resulting mode flap — local audio).
        if (
          currentState.mode === mode &&
          currentState.remoteSessionId === remoteSessionId
        ) {
          return;
        }

        console.log("[PlaybackMode] Backend mode changed →", mode, remoteSessionId);
        update((s) => ({ ...s, mode, remoteSessionId }));
        // Keep the selected session in step so the merged UI stores follow, but
        // only touch the selection when it actually differs — re-selecting the
        // same id (or clearing on a non-remote emit that isn't a real change)
        // would needlessly churn the session watchdog.
        const selected = get(selectedSession);
        if ((selected?.id ?? null) !== remoteSessionId) {
          sessions.selectSession(remoteSessionId);
        }
      }
    });

    selectedSession.subscribe((session) => {
      const currentState = get({ subscribe });

      // If we're in remote mode but session is gone or lost control capability
      // Don't interfere during an active transfer (we intentionally clear the session)
      if (currentState.mode === "remote" && currentState.remoteSessionId && !currentState.isTransferring) {
        if (!session || session.id !== currentState.remoteSessionId || !session.supportsMediaControl) {
          consecutiveMisses++;
          console.warn(`[PlaybackMode] Remote session miss ${consecutiveMisses}/${DISCONNECT_THRESHOLD}`);

          if (consecutiveMisses >= DISCONNECT_THRESHOLD) {
            console.warn("[PlaybackMode] Remote session lost after sustained disconnection");
            consecutiveMisses = 0;
            update((s) => ({
              ...s,
              mode: "idle",
              remoteSessionId: null,
              transferError: "Remote session disconnected",
            }));
          }
        } else {
          // Session is healthy, reset counter
          if (consecutiveMisses > 0) {
            console.log("[PlaybackMode] Remote session recovered after", consecutiveMisses, "misses");
          }
          consecutiveMisses = 0;
        }
      } else {
        consecutiveMisses = 0;
      }
    });
  }

  /**
   * Clear transfer error message
   */
  function clearError(): void {
    update((s) => ({ ...s, transferError: null }));
  }

  /**
   * Cancel ongoing transfer operation
   */
  function cancelTransfer(): void {
    if (currentTransferAbort) {
      console.log("[PlaybackMode] Cancelling transfer");
      currentTransferAbort();
    }
  }

  /**
   * Disconnect from remote session without transferring playback
   * This stops controlling the remote device and returns to idle/local state
   */
  async function disconnect(): Promise<void> {
    console.log("[PlaybackMode] Disconnecting from remote session");

    const currentState = get({ subscribe });
    if (currentState.mode !== "remote") {
      console.log("[PlaybackMode] Not in remote mode, nothing to disconnect");
      return;
    }

    try {
      // Notify Rust backend to switch to idle mode
      await commands.playbackModeSet({ type: "idle" });

      // Update local state
      sessions.selectSession(null);
      update((s) => ({
        ...s,
        mode: "idle",
        remoteSessionId: null,
        transferError: null,
      }));

      console.log("[PlaybackMode] Successfully disconnected");
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to disconnect";
      console.error("[PlaybackMode] Disconnect failed:", error);
      update((s) => ({
        ...s,
        transferError: message,
      }));
      throw error;
    }
  }

  // Note: initializeSessionMonitoring() and refreshMode() should be called
  // manually from +layout.svelte after auth initialization, not automatically
  // at module load time to avoid race conditions with other Rust commands

  return {
    subscribe,
    setMode,
    transferToRemote,
    transferToLocal,
    disconnect,
    refresh: refreshMode,
    initializeSessionMonitoring,
    clearError,
    cancelTransfer,
  };
}

export const playbackMode = createPlaybackModeStore();

// Derived stores for convenience
export const isRemoteMode = derived(playbackMode, ($mode) => $mode.mode === "remote");
export const isLocalMode = derived(playbackMode, ($mode) => $mode.mode === "local");
export const isIdleMode = derived(playbackMode, ($mode) => $mode.mode === "idle");
export const isTransferring = derived(playbackMode, ($mode) => $mode.isTransferring);
export const transferError = derived(playbackMode, ($mode) => $mode.transferError);
