import type { Api } from "@jellyfin/sdk";
import type {
  BaseItemDto,
  MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtomValue } from "jotai";
import React, {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { Platform } from "react-native";
import {
  downloadTrack,
  getLocalPath,
  initAudioStorage,
  isDownloading,
  setMaxCacheSizeMB,
} from "@/providers/AudioStorage";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useNetworkStatus } from "@/providers/NetworkStatusProvider";
import { settingsAtom } from "@/utils/atoms/settings";
import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl";
import { storage } from "@/utils/mmkv";

// Conditionally import TrackPlayer only on non-TV platforms
// This prevents the native module from being loaded on TV where it doesn't exist
const TrackPlayer = Platform.isTV
  ? null
  : require("react-native-track-player").default;

const TrackPlayerModule = Platform.isTV
  ? null
  : require("react-native-track-player");

// Extract types and enums from the module (only available on non-TV)
const Capability = TrackPlayerModule?.Capability;
const TPRepeatMode = TrackPlayerModule?.RepeatMode;
type Track = NonNullable<typeof TrackPlayerModule>["Track"];
type Progress = NonNullable<typeof TrackPlayerModule>["Progress"];

// Storage keys
const STORAGE_KEYS = {
  QUEUE: "music_player_queue",
  QUEUE_INDEX: "music_player_queue_index",
  REPEAT_MODE: "music_player_repeat_mode",
  SHUFFLE_ENABLED: "music_player_shuffle_enabled",
  CURRENT_PROGRESS: "music_player_progress",
} as const;

export type RepeatMode = "off" | "all" | "one";

interface TrackMediaInfo {
  mediaSource: MediaSourceInfo | null;
  isTranscoding: boolean;
}

interface PreparedTrack {
  track: Track;
  sessionId: string | null;
  mediaSource: MediaSourceInfo | null;
  isTranscoding: boolean;
  mediaInfo: TrackMediaInfo | null;
}

interface MusicPlayerState {
  currentTrack: BaseItemDto | null;
  queue: BaseItemDto[];
  originalQueue: BaseItemDto[]; // Original order before shuffle
  queueIndex: number;
  isPlaying: boolean;
  isLoading: boolean;
  loadingTrackId: string | null; // Track ID being loaded
  progress: number;
  duration: number;
  streamUrl: string | null;
  playSessionId: string | null;
  repeatMode: RepeatMode;
  shuffleEnabled: boolean;
  mediaSource: MediaSourceInfo | null;
  isTranscoding: boolean;
  trackMediaInfoMap: Record<string, TrackMediaInfo>;
}

interface MusicPlayerContextType extends MusicPlayerState {
  // Playback control
  playTrack: (track: BaseItemDto, queue?: BaseItemDto[]) => void;
  playQueue: (queue: BaseItemDto[], startIndex?: number) => void;
  playAlbum: (albumId: string, startIndex?: number) => void;
  playPlaylist: (playlistId: string, startIndex?: number) => void;
  pause: () => void;
  resume: () => void;
  togglePlayPause: () => void;
  next: () => void;
  previous: () => void;
  seek: (position: number) => void;
  stop: () => void;

  // Queue management
  addToQueue: (tracks: BaseItemDto | BaseItemDto[]) => void;
  playNext: (tracks: BaseItemDto | BaseItemDto[]) => void;
  removeFromQueue: (index: number) => void;
  moveInQueue: (fromIndex: number, toIndex: number) => void;
  reorderQueue: (newQueue: BaseItemDto[]) => void;
  clearQueue: () => void;
  jumpToIndex: (index: number) => void;

  // Modes
  setRepeatMode: (mode: RepeatMode) => void;
  toggleShuffle: () => void;

  // Internal setters (for playback engine)
  setProgress: (progress: number) => void;
  setDuration: (duration: number) => void;
  setIsPlaying: (isPlaying: boolean) => void;
  reportProgress: () => void;
  onTrackEnd: () => void;
  syncFromTrackPlayer: () => void;

  // Audio caching
  triggerLookahead: () => void;
}

const defaultState: MusicPlayerState = {
  currentTrack: null,
  queue: [],
  originalQueue: [],
  queueIndex: 0,
  isPlaying: false,
  isLoading: false,
  loadingTrackId: null,
  progress: 0,
  duration: 0,
  streamUrl: null,
  playSessionId: null,
  repeatMode: "off",
  shuffleEnabled: false,
  mediaSource: null,
  isTranscoding: false,
  trackMediaInfoMap: {},
};

// No-op function for TV stub
const noop = () => {};

const MusicPlayerContext = createContext<MusicPlayerContextType | undefined>(
  undefined,
);

export const useMusicPlayer = () => {
  const context = useContext(MusicPlayerContext);
  if (!context) {
    throw new Error("useMusicPlayer must be used within MusicPlayerProvider");
  }
  return context;
};

interface MusicPlayerProviderProps {
  children: ReactNode;
}

// Stub provider for tvOS - music playback is not supported
const TVMusicPlayerProvider: React.FC<MusicPlayerProviderProps> = ({
  children,
}) => {
  const value: MusicPlayerContextType = {
    ...defaultState,
    playTrack: noop,
    playQueue: noop,
    playAlbum: noop,
    playPlaylist: noop,
    pause: noop,
    resume: noop,
    togglePlayPause: noop,
    next: noop,
    previous: noop,
    seek: noop,
    stop: noop,
    addToQueue: noop,
    playNext: noop,
    removeFromQueue: noop,
    moveInQueue: noop,
    reorderQueue: noop,
    clearQueue: noop,
    jumpToIndex: noop,
    setRepeatMode: noop,
    toggleShuffle: noop,
    setProgress: noop,
    setDuration: noop,
    setIsPlaying: noop,
    reportProgress: noop,
    onTrackEnd: noop,
    syncFromTrackPlayer: noop,
    triggerLookahead: noop,
  };

  return (
    <MusicPlayerContext.Provider value={value}>
      {children}
    </MusicPlayerContext.Provider>
  );
};

// Persistence helpers
const saveQueueToStorage = (queue: BaseItemDto[], queueIndex: number) => {
  try {
    storage.set(STORAGE_KEYS.QUEUE, JSON.stringify(queue));
    storage.set(STORAGE_KEYS.QUEUE_INDEX, queueIndex.toString());
  } catch {
    // Silently fail
  }
};

const loadQueueFromStorage = (): {
  queue: BaseItemDto[];
  queueIndex: number;
} | null => {
  try {
    const queueJson = storage.getString(STORAGE_KEYS.QUEUE);
    const indexStr = storage.getString(STORAGE_KEYS.QUEUE_INDEX);

    if (queueJson && indexStr) {
      const queue = JSON.parse(queueJson) as BaseItemDto[];
      const queueIndex = parseInt(indexStr, 10);
      if (queue.length > 0 && queueIndex >= 0 && queueIndex < queue.length) {
        return { queue, queueIndex };
      }
    }
  } catch {
    // Silently fail
  }
  return null;
};

const loadRepeatMode = (): RepeatMode => {
  try {
    const mode = storage.getString(STORAGE_KEYS.REPEAT_MODE);
    if (mode === "off" || mode === "all" || mode === "one") {
      return mode;
    }
  } catch {
    // Silently fail
  }
  return "off";
};

const loadShuffleEnabled = (): boolean => {
  try {
    return storage.getBoolean(STORAGE_KEYS.SHUFFLE_ENABLED) ?? false;
  } catch {
    return false;
  }
};

const saveProgress = (progress: number) => {
  try {
    storage.set(STORAGE_KEYS.CURRENT_PROGRESS, progress.toString());
  } catch {
    // Silently fail
  }
};

const loadProgress = (): number => {
  try {
    const progressStr = storage.getString(STORAGE_KEYS.CURRENT_PROGRESS);
    if (progressStr) {
      return parseFloat(progressStr);
    }
  } catch {
    // Silently fail
  }
  return 0;
};

// Shuffle array using Fisher-Yates
const shuffleArray = <T,>(array: T[], currentIndex: number): T[] => {
  const result = [...array];
  const currentItem = result[currentIndex];

  // Remove current item
  result.splice(currentIndex, 1);

  // Shuffle remaining
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }

  // Put current item at the beginning
  result.unshift(currentItem);

  return result;
};

// Filter queue to only include downloaded items (for offline playback)
const filterQueueForOffline = (
  queue: BaseItemDto[],
  startIndex: number,
): { queue: BaseItemDto[]; startIndex: number } => {
  const startItem = queue[startIndex];
  const downloadedOnly = queue.filter((item) => getLocalPath(item.Id) !== null);
  const newStartIndex = downloadedOnly.findIndex((t) => t.Id === startItem?.Id);
  return {
    queue: downloadedOnly,
    startIndex: newStartIndex >= 0 ? newStartIndex : 0,
  };
};

// Convert BaseItemDto to TrackPlayer Track
const itemToTrack = (
  item: BaseItemDto,
  url: string,
  api: Api,
  preferLocalAudio = true,
): Track => {
  const albumId = item.AlbumId || item.ParentId;
  const artworkId = albumId || item.Id;
  const artwork = artworkId
    ? `${api.basePath}/Items/${artworkId}/Images/Primary?maxHeight=512&maxWidth=512&quality=90`
    : undefined;

  // Check if track is cached locally (permanent downloads take precedence)
  // getLocalPath returns full file:// URI if cached, null otherwise
  const cachedUrl = preferLocalAudio ? getLocalPath(item.Id) : null;
  const finalUrl = cachedUrl || url;

  if (cachedUrl) {
    console.log(
      `[MusicPlayer] Using cached file for ${item.Name}: ${cachedUrl}`,
    );
  }

  return {
    id: item.Id || "",
    url: finalUrl,
    title: item.Name || "Unknown",
    artist: item.Artists?.join(", ") || item.AlbumArtist || "Unknown Artist",
    album: item.Album || undefined,
    artwork,
    duration: item.RunTimeTicks ? item.RunTimeTicks / 10000000 : undefined,
  };
};

// Full implementation for non-TV platforms
const MobileMusicPlayerProvider: React.FC<MusicPlayerProviderProps> = ({
  children,
}) => {
  const api = useAtomValue(apiAtom);
  const user = useAtomValue(userAtom);
  const settings = useAtomValue(settingsAtom);
  const { isConnected, serverConnected } = useNetworkStatus();
  const isOffline = !isConnected || serverConnected === false;
  const initializedRef = useRef(false);
  const playerSetupRef = useRef(false);

  const [state, setState] = useState<MusicPlayerState>({
    currentTrack: null,
    queue: [],
    originalQueue: [],
    queueIndex: 0,
    isPlaying: false,
    isLoading: false,
    loadingTrackId: null,
    progress: 0,
    duration: 0,
    streamUrl: null,
    playSessionId: null,
    repeatMode: loadRepeatMode(),
    shuffleEnabled: loadShuffleEnabled(),
    mediaSource: null,
    isTranscoding: false,
    trackMediaInfoMap: {},
  });

  const lastReportRef = useRef<number>(0);

  // Setup TrackPlayer and AudioStorage
  useEffect(() => {
    if (!TrackPlayer) return;

    const setupPlayer = async () => {
      if (playerSetupRef.current) return;

      try {
        // Initialize audio storage for caching
        await initAudioStorage();

        await TrackPlayer.setupPlayer({
          minBuffer: 120, // Minimum 2 minutes buffer for network resilience
          maxBuffer: 240, // Maximum 4 minutes buffer
          playBuffer: 5, // Start playback after 5 seconds buffered
          backBuffer: 30, // Keep 30 seconds behind for seeking
        });
        await TrackPlayer.updateOptions({
          capabilities: [
            Capability.Play,
            Capability.Pause,
            Capability.SkipToNext,
            Capability.SkipToPrevious,
            Capability.SeekTo,
            Capability.Stop,
          ],
          compactCapabilities: [
            Capability.Play,
            Capability.Pause,
            Capability.SkipToNext,
            Capability.SkipToPrevious,
          ],
        });
        playerSetupRef.current = true;
      } catch (_error) {
        // Player might already be set up
        playerSetupRef.current = true;
      }
    };

    setupPlayer();
  }, []);

  // Update audio cache size when settings change
  useEffect(() => {
    if (settings?.audioMaxCacheSizeMB) {
      setMaxCacheSizeMB(settings.audioMaxCacheSizeMB);
    }
  }, [settings?.audioMaxCacheSizeMB]);

  // Sync repeat mode to TrackPlayer
  useEffect(() => {
    if (!TrackPlayer) return;

    const syncRepeatMode = async () => {
      if (!playerSetupRef.current) return;

      let tpRepeatMode: typeof TPRepeatMode;
      switch (state.repeatMode) {
        case "one":
          tpRepeatMode = TPRepeatMode?.Track;
          break;
        case "all":
          tpRepeatMode = TPRepeatMode?.Queue;
          break;
        default:
          tpRepeatMode = TPRepeatMode?.Off;
      }
      await TrackPlayer.setRepeatMode(tpRepeatMode);
    };

    syncRepeatMode();
  }, [state.repeatMode]);

  // Restore queue on mount (when api is available)
  useEffect(() => {
    if (!api || !user?.Id || initializedRef.current) return;
    initializedRef.current = true;

    const saved = loadQueueFromStorage();
    if (saved && saved.queue.length > 0) {
      const currentTrack = saved.queue[saved.queueIndex];
      const savedProgress = loadProgress();

      setState((prev) => ({
        ...prev,
        queue: saved.queue,
        originalQueue: saved.queue,
        queueIndex: saved.queueIndex,
        currentTrack,
        progress: savedProgress,
        duration: currentTrack?.RunTimeTicks
          ? Math.floor(currentTrack.RunTimeTicks / 10000000)
          : 0,
        isPlaying: false, // Don't auto-play on restore
      }));
    }
  }, [api, user?.Id]);

  // Save queue whenever it changes
  useEffect(() => {
    if (state.queue.length > 0) {
      saveQueueToStorage(state.queue, state.queueIndex);
    }
  }, [state.queue, state.queueIndex]);

  // Save progress periodically
  useEffect(() => {
    if (state.progress > 0 && state.currentTrack) {
      saveProgress(state.progress);
    }
  }, [state.progress, state.currentTrack]);

  const reportPlaybackStart = useCallback(
    async (track: BaseItemDto, sessionId: string | null) => {
      if (!api || !user?.Id || !track.Id) return;

      try {
        await getPlaystateApi(api).reportPlaybackStart({
          playbackStartInfo: {
            ItemId: track.Id,
            PlaySessionId: sessionId || undefined,
            CanSeek: true,
            IsPaused: false,
            IsMuted: false,
            VolumeLevel: 100,
            PlayMethod: "DirectStream",
          },
        });
      } catch {
        // Silently fail
      }
    },
    [api, user?.Id],
  );

  const reportPlaybackProgress = useCallback(async () => {
    if (!api || !user?.Id || !state.currentTrack?.Id) return;

    const now = Date.now();
    if (now - lastReportRef.current < 10000) return;
    lastReportRef.current = now;

    try {
      await getPlaystateApi(api).reportPlaybackProgress({
        playbackProgressInfo: {
          ItemId: state.currentTrack.Id,
          PlaySessionId: state.playSessionId || undefined,
          PositionTicks: Math.floor(state.progress * 10000000),
          CanSeek: true,
          IsPaused: !state.isPlaying,
          IsMuted: false,
          VolumeLevel: 100,
          PlayMethod: "DirectStream",
        },
      });
    } catch {
      // Silently fail
    }
  }, [
    api,
    user?.Id,
    state.currentTrack?.Id,
    state.playSessionId,
    state.progress,
    state.isPlaying,
  ]);

  const reportPlaybackStopped = useCallback(
    async (
      track: BaseItemDto,
      positionTicks: number,
      sessionId: string | null,
    ) => {
      if (!api || !user?.Id || !track.Id) return;

      try {
        await getPlaystateApi(api).reportPlaybackStopped({
          playbackStopInfo: {
            ItemId: track.Id,
            PlaySessionId: sessionId || undefined,
            PositionTicks: Math.floor(positionTicks),
          },
        });
      } catch {
        // Silently fail
      }
    },
    [api, user?.Id],
  );

  // Helper to prepare a single track - checks cache first, then fetches from server
  const prepareTrack = useCallback(
    async (
      item: BaseItemDto,
      preferLocal: boolean,
    ): Promise<PreparedTrack | null> => {
      if (!api || !user?.Id || !item.Id) return null;

      // Check for local/cached version first
      const cachedUrl = preferLocal ? getLocalPath(item.Id) : null;

      if (cachedUrl) {
        // Downloaded track - instant return, no API call needed
        return {
          track: itemToTrack(item, cachedUrl, api, false), // false to avoid redundant cache check
          sessionId: null,
          mediaSource: null,
          isTranscoding: false,
          mediaInfo: null,
        };
      }

      // Not downloaded - need to fetch stream URL from server
      try {
        const result = await getAudioStreamUrl(api, user.Id, item.Id);
        if (!result) return null;

        return {
          track: itemToTrack(item, result.url, api, false),
          sessionId: result.sessionId,
          mediaSource: result.mediaSource,
          isTranscoding: result.isTranscoding,
          mediaInfo: {
            mediaSource: result.mediaSource,
            isTranscoding: result.isTranscoding,
          },
        };
      } catch (error) {
        console.warn(
          `[MusicPlayer] Failed to prepare track ${item.Id}:`,
          error,
        );
        // If server unreachable, check for cached version as fallback
        const fallbackCached = getLocalPath(item.Id);
        if (fallbackCached) {
          return {
            track: itemToTrack(item, fallbackCached, api, false),
            sessionId: null,
            mediaSource: null,
            isTranscoding: false,
            mediaInfo: null,
          };
        }
        return null;
      }
    },
    [api, user?.Id],
  );

  // Load remaining tracks in the background without blocking playback
  const loadRemainingTracksInBackground = useCallback(
    async (queue: BaseItemDto[], startIndex: number, preferLocal: boolean) => {
      if (!api || !user?.Id || !TrackPlayer) return;

      const mediaInfoMap: Record<string, TrackMediaInfo> = {};
      const failedItemIds: string[] = []; // Track items that failed to prepare

      // Process tracks BEFORE the start index (insert at position 0, pushing current track forward)
      const beforeTracks: Track[] = [];
      for (let i = 0; i < startIndex; i++) {
        const item = queue[i];
        if (!item.Id) continue;

        const prepared = await prepareTrack(item, preferLocal);
        if (prepared) {
          beforeTracks.push(prepared.track);
          if (prepared.mediaInfo) {
            mediaInfoMap[item.Id] = prepared.mediaInfo;
          }
        } else {
          failedItemIds.push(item.Id);
        }
      }

      // Insert tracks before current track (they go at index 0)
      if (beforeTracks.length > 0) {
        await TrackPlayer.add(beforeTracks, 0);
        // Update queue index since we inserted tracks before the current one
        setState((prev) => ({
          ...prev,
          queueIndex: beforeTracks.length,
          trackMediaInfoMap: { ...prev.trackMediaInfoMap, ...mediaInfoMap },
        }));
      }

      // Process tracks AFTER the start index (append to end)
      for (let i = startIndex + 1; i < queue.length; i++) {
        const item = queue[i];
        if (!item.Id) continue;

        const prepared = await prepareTrack(item, preferLocal);
        if (prepared) {
          await TrackPlayer.add(prepared.track); // Append to end
          if (prepared.mediaInfo && item.Id) {
            setState((prev) => ({
              ...prev,
              trackMediaInfoMap: {
                ...prev.trackMediaInfoMap,
                [item.Id!]: prepared.mediaInfo!,
              },
            }));
          }
        } else {
          failedItemIds.push(item.Id);
        }
      }

      // Remove failed items from queue to keep it in sync with TrackPlayer
      if (failedItemIds.length > 0) {
        console.log(
          `[MusicPlayer] Removing ${failedItemIds.length} unavailable tracks from queue`,
        );
        setState((prev) => {
          const newQueue = prev.queue.filter(
            (t) => !failedItemIds.includes(t.Id!),
          );
          const newOriginalQueue = prev.originalQueue.filter(
            (t) => !failedItemIds.includes(t.Id!),
          );
          // Recalculate queue index based on current track position in filtered queue
          const currentTrackId = prev.currentTrack?.Id;
          const newQueueIndex = currentTrackId
            ? newQueue.findIndex((t) => t.Id === currentTrackId)
            : 0;
          return {
            ...prev,
            queue: newQueue,
            originalQueue: newOriginalQueue,
            queueIndex: newQueueIndex >= 0 ? newQueueIndex : prev.queueIndex,
          };
        });
      }
    },
    [api, user?.Id, prepareTrack],
  );

  const loadAndPlayQueue = useCallback(
    async (queue: BaseItemDto[], startIndex: number) => {
      if (!api || !user?.Id || queue.length === 0 || !TrackPlayer) return;

      const preferLocal = settings?.preferLocalAudio ?? true;

      // Apply offline filtering at the start to ensure state.queue matches TrackPlayer queue
      let finalQueue = queue;
      let finalIndex = startIndex;

      if (isOffline) {
        const filtered = filterQueueForOffline(queue, startIndex);
        finalQueue = filtered.queue;
        finalIndex = filtered.startIndex;

        if (finalQueue.length === 0) {
          console.warn(
            "[MusicPlayer] No downloaded tracks available for offline playback",
          );
          return;
        }
      }

      const targetItem = finalQueue[finalIndex];
      setState((prev) => ({
        ...prev,
        isLoading: true,
        loadingTrackId: targetItem?.Id ?? null,
      }));

      try {
        // PHASE 1: Prepare and play the target track immediately
        const targetTrackResult = await prepareTrack(targetItem, preferLocal);

        if (!targetTrackResult) {
          setState((prev) => ({
            ...prev,
            isLoading: false,
            loadingTrackId: null,
          }));
          return;
        }

        // Reset and start playback immediately with just the target track
        await TrackPlayer.reset();
        await TrackPlayer.add(targetTrackResult.track);
        await TrackPlayer.play();

        // Update state for immediate playback
        setState((prev) => ({
          ...prev,
          queue: finalQueue,
          originalQueue: finalQueue,
          queueIndex: 0, // Target track is at index 0 in TrackPlayer initially
          currentTrack: targetItem,
          isLoading: false,
          loadingTrackId: null,
          isPlaying: true,
          streamUrl: targetTrackResult.track.url || null,
          playSessionId: targetTrackResult.sessionId,
          duration: targetItem?.RunTimeTicks
            ? Math.floor(targetItem.RunTimeTicks / 10000000)
            : 0,
          mediaSource: targetTrackResult.mediaSource,
          isTranscoding: targetTrackResult.isTranscoding,
          trackMediaInfoMap:
            targetTrackResult.mediaInfo && targetItem.Id
              ? { [targetItem.Id]: targetTrackResult.mediaInfo }
              : {},
        }));

        reportPlaybackStart(targetItem, targetTrackResult.sessionId);

        // PHASE 2: Load remaining tracks in background (non-blocking)
        if (finalQueue.length > 1) {
          loadRemainingTracksInBackground(finalQueue, finalIndex, preferLocal);
        }
      } catch (error) {
        console.error("[MusicPlayer] Error loading queue:", error);
        setState((prev) => ({
          ...prev,
          isLoading: false,
          loadingTrackId: null,
        }));
      }
    },
    [
      api,
      user?.Id,
      reportPlaybackStart,
      settings?.preferLocalAudio,
      prepareTrack,
      loadRemainingTracksInBackground,
      isOffline,
    ],
  );

  const playTrack = useCallback(
    (track: BaseItemDto, queue?: BaseItemDto[]) => {
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }

      const newQueue = queue || [track];
      const queueIndex = newQueue.findIndex((t) => t.Id === track.Id);
      loadAndPlayQueue(newQueue, queueIndex >= 0 ? queueIndex : 0);
    },
    [
      state.currentTrack,
      state.playSessionId,
      state.progress,
      reportPlaybackStopped,
      loadAndPlayQueue,
    ],
  );

  const playQueue = useCallback(
    (queue: BaseItemDto[], startIndex = 0) => {
      if (queue.length === 0) return;

      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }

      let finalQueue = queue;
      let finalIndex = startIndex;

      // When offline, filter to downloaded items only
      if (isOffline) {
        const filtered = filterQueueForOffline(queue, startIndex);
        finalQueue = filtered.queue;
        finalIndex = filtered.startIndex;

        if (finalQueue.length === 0) {
          console.warn(
            "[MusicPlayer] No downloaded tracks available for offline playback",
          );
          return;
        }
      }

      // Apply shuffle if enabled
      if (state.shuffleEnabled) {
        finalQueue = shuffleArray(finalQueue, finalIndex);
        finalIndex = 0;
      }

      loadAndPlayQueue(finalQueue, finalIndex);
    },
    [
      state.currentTrack,
      state.playSessionId,
      state.progress,
      state.shuffleEnabled,
      reportPlaybackStopped,
      loadAndPlayQueue,
      isOffline,
    ],
  );

  const playAlbum = useCallback(
    async (albumId: string, startIndex = 0) => {
      if (!api || !user?.Id) return;

      try {
        const { getItemsApi } = await import("@jellyfin/sdk/lib/utils/api");
        const response = await getItemsApi(api).getItems({
          userId: user.Id,
          parentId: albumId,
          sortBy: ["IndexNumber"],
          sortOrder: ["Ascending"],
        });

        const tracks = response.data.Items || [];
        if (tracks.length > 0) {
          playQueue(tracks, startIndex);
        }
      } catch {
        // Silently fail
      }
    },
    [api, user?.Id, playQueue],
  );

  const playPlaylist = useCallback(
    async (playlistId: string, startIndex = 0) => {
      if (!api || !user?.Id) return;

      try {
        const { getItemsApi } = await import("@jellyfin/sdk/lib/utils/api");
        const response = await getItemsApi(api).getItems({
          userId: user.Id,
          parentId: playlistId,
          sortBy: ["SortName"],
          sortOrder: ["Ascending"],
        });

        const tracks = response.data.Items || [];
        if (tracks.length > 0) {
          playQueue(tracks, startIndex);
        }
      } catch {
        // Silently fail
      }
    },
    [api, user?.Id, playQueue],
  );

  const pause = useCallback(async () => {
    if (!TrackPlayer) return;
    await TrackPlayer.pause();
    setState((prev) => ({ ...prev, isPlaying: false }));
  }, []);

  const resume = useCallback(async () => {
    if (!TrackPlayer) return;
    if (!state.streamUrl && state.currentTrack && api && user?.Id) {
      // Need to load the track first (e.g., after app restart)
      const result = await getAudioStreamUrl(
        api,
        user.Id,
        state.currentTrack.Id!,
      );
      if (result) {
        const preferLocal = settings?.preferLocalAudio ?? true;
        await TrackPlayer.reset();
        await TrackPlayer.add(
          itemToTrack(state.currentTrack, result.url, api, preferLocal),
        );
        await TrackPlayer.seekTo(state.progress);
        await TrackPlayer.play();
        setState((prev) => ({
          ...prev,
          streamUrl: result.url,
          playSessionId: result.sessionId,
          isPlaying: true,
        }));
      }
    } else {
      await TrackPlayer.play();
      setState((prev) => ({ ...prev, isPlaying: true }));
    }
  }, [
    api,
    user?.Id,
    state.streamUrl,
    state.currentTrack,
    state.progress,
    settings?.preferLocalAudio,
  ]);

  const togglePlayPause = useCallback(async () => {
    if (state.isPlaying) {
      await pause();
    } else {
      await resume();
    }
  }, [state.isPlaying, pause, resume]);

  const next = useCallback(async () => {
    if (!TrackPlayer) return;
    const currentIndex = await TrackPlayer.getActiveTrackIndex();
    const queueLength = (await TrackPlayer.getQueue()).length;

    if (currentIndex !== undefined && currentIndex < queueLength - 1) {
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }
      await TrackPlayer.skipToNext();
      const newIndex = currentIndex + 1;
      setState((prev) => {
        const nextTrack = prev.queue[newIndex];
        const mediaInfo = nextTrack?.Id
          ? prev.trackMediaInfoMap[nextTrack.Id]
          : null;
        return {
          ...prev,
          queueIndex: newIndex,
          currentTrack: nextTrack,
          mediaSource: mediaInfo?.mediaSource ?? null,
          isTranscoding: mediaInfo?.isTranscoding ?? false,
        };
      });
    } else if (state.repeatMode === "all" && state.queue.length > 0) {
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }
      await TrackPlayer.skip(0);
      setState((prev) => {
        const firstTrack = prev.queue[0];
        const mediaInfo = firstTrack?.Id
          ? prev.trackMediaInfoMap[firstTrack.Id]
          : null;
        return {
          ...prev,
          queueIndex: 0,
          currentTrack: firstTrack,
          mediaSource: mediaInfo?.mediaSource ?? null,
          isTranscoding: mediaInfo?.isTranscoding ?? false,
        };
      });
    }
  }, [
    state.queue,
    state.currentTrack,
    state.playSessionId,
    state.progress,
    state.repeatMode,
    reportPlaybackStopped,
  ]);

  const previous = useCallback(async () => {
    if (!TrackPlayer) return;
    const position = await TrackPlayer.getProgress().then(
      (p: Progress) => p.position,
    );

    if (position > 3) {
      await TrackPlayer.seekTo(0);
      setState((prev) => ({ ...prev, progress: 0 }));
      return;
    }

    const currentIndex = await TrackPlayer.getActiveTrackIndex();

    if (currentIndex !== undefined && currentIndex > 0) {
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }
      await TrackPlayer.skipToPrevious();
      const newIndex = currentIndex - 1;
      setState((prev) => {
        const prevTrack = prev.queue[newIndex];
        const mediaInfo = prevTrack?.Id
          ? prev.trackMediaInfoMap[prevTrack.Id]
          : null;
        return {
          ...prev,
          queueIndex: newIndex,
          currentTrack: prevTrack,
          mediaSource: mediaInfo?.mediaSource ?? null,
          isTranscoding: mediaInfo?.isTranscoding ?? false,
        };
      });
    } else if (state.repeatMode === "all" && state.queue.length > 0) {
      const lastIndex = state.queue.length - 1;
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }
      await TrackPlayer.skip(lastIndex);
      setState((prev) => {
        const lastTrack = prev.queue[lastIndex];
        const mediaInfo = lastTrack?.Id
          ? prev.trackMediaInfoMap[lastTrack.Id]
          : null;
        return {
          ...prev,
          queueIndex: lastIndex,
          currentTrack: lastTrack,
          mediaSource: mediaInfo?.mediaSource ?? null,
          isTranscoding: mediaInfo?.isTranscoding ?? false,
        };
      });
    }
  }, [
    state.queue,
    state.currentTrack,
    state.playSessionId,
    state.progress,
    state.repeatMode,
    reportPlaybackStopped,
  ]);

  const seek = useCallback(async (position: number) => {
    if (!TrackPlayer) return;
    await TrackPlayer.seekTo(position);
    setState((prev) => ({ ...prev, progress: position }));
  }, []);

  const stop = useCallback(async () => {
    if (!TrackPlayer) return;
    if (state.currentTrack && state.playSessionId) {
      reportPlaybackStopped(
        state.currentTrack,
        state.progress * 10000000,
        state.playSessionId,
      );
    }

    await TrackPlayer.reset();

    // Clear storage
    try {
      storage.remove(STORAGE_KEYS.QUEUE);
      storage.remove(STORAGE_KEYS.QUEUE_INDEX);
      storage.remove(STORAGE_KEYS.CURRENT_PROGRESS);
    } catch {
      // Silently fail
    }

    setState({
      currentTrack: null,
      queue: [],
      originalQueue: [],
      queueIndex: 0,
      isPlaying: false,
      isLoading: false,
      loadingTrackId: null,
      progress: 0,
      duration: 0,
      streamUrl: null,
      playSessionId: null,
      repeatMode: state.repeatMode,
      shuffleEnabled: state.shuffleEnabled,
      mediaSource: null,
      isTranscoding: false,
      trackMediaInfoMap: {},
    });
  }, [
    state.currentTrack,
    state.playSessionId,
    state.progress,
    state.repeatMode,
    state.shuffleEnabled,
    reportPlaybackStopped,
  ]);

  // Queue management
  const addToQueue = useCallback(
    async (tracks: BaseItemDto | BaseItemDto[]) => {
      if (!api || !user?.Id || !TrackPlayer) return;

      const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
      const preferLocal = settings?.preferLocalAudio ?? true;

      // Add to TrackPlayer queue
      for (const item of tracksArray) {
        if (!item.Id) continue;
        const cachedUrl = getLocalPath(item.Id);
        const result = await getAudioStreamUrl(api, user.Id, item.Id);
        if (result) {
          await TrackPlayer.add(
            itemToTrack(item, result.url, api, preferLocal),
          );
        } else if (cachedUrl) {
          console.log(
            `[MusicPlayer] Using cached file (offline) for ${item.Name}: ${cachedUrl}`,
          );
          await TrackPlayer.add(itemToTrack(item, cachedUrl, api, true));
        }
      }

      setState((prev) => ({
        ...prev,
        queue: [...prev.queue, ...tracksArray],
        originalQueue: [...prev.originalQueue, ...tracksArray],
      }));
    },
    [api, user?.Id, settings?.preferLocalAudio],
  );

  const playNext = useCallback(
    async (tracks: BaseItemDto | BaseItemDto[]) => {
      if (!api || !user?.Id || !TrackPlayer) return;

      const tracksArray = Array.isArray(tracks) ? tracks : [tracks];
      const currentIndex = await TrackPlayer.getActiveTrackIndex();
      const insertIndex = (currentIndex ?? -1) + 1;
      const preferLocal = settings?.preferLocalAudio ?? true;

      // Add to TrackPlayer queue after current track
      for (let i = tracksArray.length - 1; i >= 0; i--) {
        const item = tracksArray[i];
        if (!item.Id) continue;
        const cachedUrl = getLocalPath(item.Id);
        const result = await getAudioStreamUrl(api, user.Id, item.Id);
        if (result) {
          await TrackPlayer.add(
            itemToTrack(item, result.url, api, preferLocal),
            insertIndex,
          );
        } else if (cachedUrl) {
          console.log(
            `[MusicPlayer] Using cached file (offline) for ${item.Name}: ${cachedUrl}`,
          );
          await TrackPlayer.add(
            itemToTrack(item, cachedUrl, api, true),
            insertIndex,
          );
        }
      }

      setState((prev) => {
        const stateInsertIndex = prev.queueIndex + 1;
        const newQueue = [...prev.queue];
        const newOriginalQueue = [...prev.originalQueue];

        newQueue.splice(stateInsertIndex, 0, ...tracksArray);
        newOriginalQueue.splice(stateInsertIndex, 0, ...tracksArray);

        return {
          ...prev,
          queue: newQueue,
          originalQueue: newOriginalQueue,
        };
      });
    },
    [api, user?.Id, settings?.preferLocalAudio],
  );

  const removeFromQueue = useCallback(async (index: number) => {
    if (!TrackPlayer) return;
    const queueLength = (await TrackPlayer.getQueue()).length;
    const currentIndex = await TrackPlayer.getActiveTrackIndex();

    if (index < 0 || index >= queueLength) return;
    if (index === currentIndex) return; // Can't remove currently playing

    await TrackPlayer.remove(index);

    setState((prev) => {
      if (index < 0 || index >= prev.queue.length) return prev;
      if (index === prev.queueIndex) return prev;

      const newQueue = [...prev.queue];
      const removedTrack = newQueue.splice(index, 1)[0];

      const newOriginalQueue = prev.originalQueue.filter(
        (t) => t.Id !== removedTrack.Id,
      );

      const newIndex =
        index < prev.queueIndex ? prev.queueIndex - 1 : prev.queueIndex;

      return {
        ...prev,
        queue: newQueue,
        originalQueue: newOriginalQueue,
        queueIndex: newIndex,
      };
    });
  }, []);

  const moveInQueue = useCallback(
    async (fromIndex: number, toIndex: number) => {
      if (!TrackPlayer) return;
      const queue = await TrackPlayer.getQueue();
      if (
        fromIndex < 0 ||
        fromIndex >= queue.length ||
        toIndex < 0 ||
        toIndex >= queue.length ||
        fromIndex === toIndex
      ) {
        return;
      }

      await TrackPlayer.move(fromIndex, toIndex);

      setState((prev) => {
        const newQueue = [...prev.queue];
        const [movedItem] = newQueue.splice(fromIndex, 1);
        newQueue.splice(toIndex, 0, movedItem);

        let newIndex = prev.queueIndex;
        if (fromIndex === prev.queueIndex) {
          newIndex = toIndex;
        } else if (fromIndex < prev.queueIndex && toIndex >= prev.queueIndex) {
          newIndex = prev.queueIndex - 1;
        } else if (fromIndex > prev.queueIndex && toIndex <= prev.queueIndex) {
          newIndex = prev.queueIndex + 1;
        }

        return {
          ...prev,
          queue: newQueue,
          queueIndex: newIndex,
        };
      });
    },
    [],
  );

  // Reorder queue with a new array (used by drag-to-reorder UI)
  const reorderQueue = useCallback(
    async (newQueue: BaseItemDto[]) => {
      if (!TrackPlayer) return;
      // Find where the current track ended up in the new order
      const currentTrackId = state.currentTrack?.Id;
      const newIndex = currentTrackId
        ? newQueue.findIndex((t) => t.Id === currentTrackId)
        : 0;

      // Build the reordering operations for TrackPlayer
      // We need to match TrackPlayer's queue to the new order
      const tpQueue = await TrackPlayer.getQueue();

      // Create a map of trackId -> current TrackPlayer index
      const currentPositions = new Map<string, number>();
      tpQueue.forEach((track: Track, idx: number) => {
        currentPositions.set(track.id, idx);
      });

      // Move tracks one by one to match the new order
      // Work backwards to avoid index shifting issues
      for (let targetIdx = newQueue.length - 1; targetIdx >= 0; targetIdx--) {
        const trackId = newQueue[targetIdx].Id;
        if (!trackId) continue;

        const currentIdx = currentPositions.get(trackId);
        if (currentIdx !== undefined && currentIdx !== targetIdx) {
          await TrackPlayer.move(currentIdx, targetIdx);

          // Update positions map after move
          currentPositions.forEach((pos, id) => {
            if (currentIdx < targetIdx) {
              // Moving down: items between shift up
              if (pos > currentIdx && pos <= targetIdx) {
                currentPositions.set(id, pos - 1);
              }
            } else {
              // Moving up: items between shift down
              if (pos >= targetIdx && pos < currentIdx) {
                currentPositions.set(id, pos + 1);
              }
            }
          });
          currentPositions.set(trackId, targetIdx);
        }
      }

      setState((prev) => ({
        ...prev,
        queue: newQueue,
        queueIndex: newIndex >= 0 ? newIndex : 0,
        currentTrack: newIndex >= 0 ? newQueue[newIndex] : prev.currentTrack,
      }));
    },
    [state.currentTrack?.Id],
  );

  const clearQueue = useCallback(async () => {
    if (!TrackPlayer) return;
    const currentIndex = await TrackPlayer.getActiveTrackIndex();
    const queue = await TrackPlayer.getQueue();

    if (currentIndex === undefined || queue.length === 0) return;

    // Remove all tracks except current
    const indicesToRemove = queue
      .map((_: Track, i: number) => i)
      .filter((i: number) => i !== currentIndex);

    // Remove in reverse order to not mess up indices
    for (const i of indicesToRemove.reverse()) {
      await TrackPlayer.remove(i);
    }

    setState((prev) => {
      if (!prev.currentTrack) return prev;

      return {
        ...prev,
        queue: [prev.currentTrack],
        originalQueue: [prev.currentTrack],
        queueIndex: 0,
      };
    });
  }, []);

  const jumpToIndex = useCallback(
    async (index: number) => {
      if (!TrackPlayer) return;
      if (
        index < 0 ||
        index >= state.queue.length ||
        index === state.queueIndex
      )
        return;

      // Check if the track exists in TrackPlayer queue (might not be loaded yet due to background loading)
      const tpQueue = await TrackPlayer.getQueue();
      const targetItem = state.queue[index];

      if (index >= tpQueue.length) {
        // Track not loaded yet - need to load it first
        if (!targetItem) return;

        setState((prev) => ({
          ...prev,
          isLoading: true,
          loadingTrackId: targetItem?.Id ?? null,
        }));

        const preferLocal = settings?.preferLocalAudio ?? true;
        const prepared = await prepareTrack(targetItem, preferLocal);

        if (!prepared) {
          setState((prev) => ({
            ...prev,
            isLoading: false,
            loadingTrackId: null,
          }));
          return;
        }

        // Add the track at the correct position
        await TrackPlayer.add(prepared.track, index);
        setState((prev) => ({
          ...prev,
          isLoading: false,
          loadingTrackId: null,
          ...(prepared.mediaInfo && targetItem.Id
            ? {
                trackMediaInfoMap: {
                  ...prev.trackMediaInfoMap,
                  [targetItem.Id]: prepared.mediaInfo,
                },
              }
            : {}),
        }));
      }

      // Report stop for current track
      if (state.currentTrack && state.playSessionId) {
        reportPlaybackStopped(
          state.currentTrack,
          state.progress * 10000000,
          state.playSessionId,
        );
      }

      await TrackPlayer.skip(index);

      setState((prev) => {
        const mediaInfo = targetItem?.Id
          ? prev.trackMediaInfoMap[targetItem.Id]
          : null;
        return {
          ...prev,
          queueIndex: index,
          currentTrack: targetItem,
          mediaSource: mediaInfo?.mediaSource ?? null,
          isTranscoding: mediaInfo?.isTranscoding ?? false,
        };
      });
    },
    [
      state.queue,
      state.queueIndex,
      state.currentTrack,
      state.playSessionId,
      state.progress,
      reportPlaybackStopped,
      settings?.preferLocalAudio,
      prepareTrack,
    ],
  );

  // Modes
  const setRepeatMode = useCallback((mode: RepeatMode) => {
    storage.set(STORAGE_KEYS.REPEAT_MODE, mode);
    setState((prev) => ({ ...prev, repeatMode: mode }));
  }, []);

  const toggleShuffle = useCallback(() => {
    setState((prev) => {
      const newShuffleEnabled = !prev.shuffleEnabled;
      storage.set(STORAGE_KEYS.SHUFFLE_ENABLED, newShuffleEnabled);

      if (newShuffleEnabled) {
        const shuffled = shuffleArray(prev.queue, prev.queueIndex);
        return {
          ...prev,
          shuffleEnabled: true,
          queue: shuffled,
          queueIndex: 0,
        };
      } else {
        const currentTrackId = prev.currentTrack?.Id;
        const newIndex = prev.originalQueue.findIndex(
          (t) => t.Id === currentTrackId,
        );
        return {
          ...prev,
          shuffleEnabled: false,
          queue: prev.originalQueue,
          queueIndex: newIndex >= 0 ? newIndex : 0,
        };
      }
    });
  }, []);

  const setProgress = useCallback((progress: number) => {
    setState((prev) => ({ ...prev, progress }));
  }, []);

  const setDuration = useCallback((duration: number) => {
    setState((prev) => ({ ...prev, duration }));
  }, []);

  const setIsPlaying = useCallback((isPlaying: boolean) => {
    setState((prev) => ({ ...prev, isPlaying }));
  }, []);

  // Sync state from TrackPlayer (called when active track changes)
  // Uses ID-based lookup instead of index to handle queue mismatches
  const syncFromTrackPlayer = useCallback(async () => {
    if (!TrackPlayer) return;
    const activeTrack = await TrackPlayer.getActiveTrack();
    if (!activeTrack?.id) return;

    // Find track by ID, not by index - handles cases where queues have different tracks
    const trackIndex = state.queue.findIndex((t) => t.Id === activeTrack.id);
    if (trackIndex >= 0) {
      setState((prev) => ({
        ...prev,
        queueIndex: trackIndex,
        currentTrack: prev.queue[trackIndex],
      }));
    }
  }, [state.queue]);

  // Called by playback engine when track ends
  const onTrackEnd = useCallback(() => {
    if (!TrackPlayer) return;
    if (state.repeatMode === "one") {
      TrackPlayer.seekTo(0);
      TrackPlayer.play();
    }
    // For other modes, TrackPlayer handles it via repeat mode setting
  }, [state.repeatMode]);

  // Look-ahead cache: pre-cache upcoming N tracks (excludes current track to avoid bandwidth competition)
  const triggerLookahead = useCallback(async () => {
    if (!TrackPlayer) return;
    // Check if caching is enabled in settings
    if (settings?.audioLookaheadEnabled === false) return;
    if (!api || !user?.Id) return;

    try {
      const tpQueue = await TrackPlayer.getQueue();
      const currentIdx = await TrackPlayer.getActiveTrackIndex();
      if (currentIdx === undefined || currentIdx < 0) return;

      // Cache next N tracks (from settings, default 1) - excludes current to avoid bandwidth competition
      const lookaheadCount = settings?.audioLookaheadCount ?? 1;
      const tracksToCache = tpQueue.slice(
        currentIdx + 1,
        currentIdx + 1 + lookaheadCount,
      );

      for (const track of tracksToCache) {
        const itemId = track.id;
        // Skip if already stored locally or currently downloading
        if (!itemId || getLocalPath(itemId) || isDownloading(itemId)) continue;

        // Get stream URL for this track
        const result = await getAudioStreamUrl(api, user.Id, itemId);

        // Only cache direct streams (not transcoding - can't cache dynamic content)
        if (result?.url && !result.isTranscoding) {
          downloadTrack(itemId, result.url, {
            permanent: false,
            container: result.mediaSource?.Container || undefined,
          }).catch(() => {
            // Silent fail - caching is best-effort
          });
        }
      }
    } catch {
      // Silent fail - look-ahead caching is best-effort
    }
  }, [
    api,
    user?.Id,
    settings?.audioLookaheadEnabled,
    settings?.audioLookaheadCount,
  ]);

  const value = useMemo(
    () => ({
      ...state,
      playTrack,
      playQueue,
      playAlbum,
      playPlaylist,
      pause,
      resume,
      togglePlayPause,
      next,
      previous,
      seek,
      stop,
      addToQueue,
      playNext,
      removeFromQueue,
      moveInQueue,
      reorderQueue,
      clearQueue,
      jumpToIndex,
      setRepeatMode,
      toggleShuffle,
      setProgress,
      setDuration,
      setIsPlaying,
      reportProgress: reportPlaybackProgress,
      onTrackEnd,
      syncFromTrackPlayer,
      triggerLookahead,
    }),
    [
      state,
      playTrack,
      playQueue,
      playAlbum,
      playPlaylist,
      pause,
      resume,
      togglePlayPause,
      next,
      previous,
      seek,
      stop,
      addToQueue,
      playNext,
      removeFromQueue,
      moveInQueue,
      reorderQueue,
      clearQueue,
      jumpToIndex,
      setRepeatMode,
      toggleShuffle,
      setProgress,
      setDuration,
      setIsPlaying,
      reportPlaybackProgress,
      onTrackEnd,
      syncFromTrackPlayer,
      triggerLookahead,
    ],
  );

  return (
    <MusicPlayerContext.Provider value={value}>
      {children}
    </MusicPlayerContext.Provider>
  );
};

// Export the appropriate provider based on platform
export const MusicPlayerProvider: React.FC<MusicPlayerProviderProps> =
  Platform.isTV ? TVMusicPlayerProvider : MobileMusicPlayerProvider;
