// Library state store
// TRACES: UR-007, UR-008, UR-029, UR-030 | DR-007, DR-011, DR-033

import { writable, derived } from "svelte/store";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type { Library, MediaItem, SearchResult, Genre } from "$lib/api/types";
import type { SearchOptions } from "$lib/api/bindings";
import type { SearchScope } from "$lib/utils/searchScope";
import { auth } from "./auth";

/**
 * Payload of the backend `search-event` (mirrors Rust `SearchUpdateEvent`).
 * Carries the merged cache+server results for a given search request.
 */
interface SearchUpdateEvent {
  requestId: number;
  result: SearchResult;
}

export type ViewMode = "grid" | "list";

interface LibraryState {
  libraries: Library[];
  currentLibrary: Library | null;
  items: MediaItem[];
  currentItem: MediaItem | null;
  /** Counter for concurrent loading operations. isLoading = loadingCount > 0 */
  loadingCount: number;
  error: string | null;
  totalItems: number;
  searchQuery: string;
  searchResults: MediaItem[];
  viewMode: ViewMode;
  genres: Genre[];
  selectedGenres: string[];
}

function getStoredViewMode(): ViewMode {
  if (typeof localStorage === "undefined") return "grid";
  const stored = localStorage.getItem("jellytau-view-mode");
  return stored === "list" ? "list" : "grid";
}

function createLibraryStore() {
  const initialState: LibraryState = {
    libraries: [],
    currentLibrary: null,
    items: [],
    currentItem: null,
    loadingCount: 0,
    error: null,
    totalItems: 0,
    searchQuery: "",
    searchResults: [],
    viewMode: getStoredViewMode(),
    genres: [],
    selectedGenres: [],
  };

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

  // Monotonic id identifying the most recent search request. Each new search
  // bumps it; the deferred `search-event` (carrying merged cache+server
  // results) is only applied when its requestId still matches the latest one,
  // so out-of-order / superseded results never clobber fresher ones.
  let searchRequestId = 0;
  let unlistenSearch: UnlistenFn | null = null;

  // Lazily subscribe to backend search updates the first time we search.
  async function ensureSearchListener() {
    if (unlistenSearch) return;
    unlistenSearch = await listen<SearchUpdateEvent>("search-event", (event) => {
      const { requestId, result } = event.payload;
      // Ignore results from a query the user has already moved on from.
      if (requestId !== searchRequestId) return;
      update((s) => ({ ...s, searchResults: result.items }));
    });
  }

  async function loadLibraries() {
    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));

    try {
      const startTime = performance.now();
      const repo = auth.getRepository();

      console.log("📚 [LibraryStore] Loading libraries...");

      const libraries = await repo.getLibraries();

      const loadTime = Math.round(performance.now() - startTime);

      if (loadTime < 100) {
        console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${libraries.length} libraries in ${loadTime}ms (instant)`);
      } else {
        console.log(`⏳ [LibraryStore] Loaded ${libraries.length} libraries in ${loadTime}ms (from server)`);
      }

      update((s) => ({
        ...s,
        libraries,
        loadingCount: Math.max(0, s.loadingCount - 1),
      }));

      return libraries;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load libraries";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  async function loadItems(
    parentId: string,
    options: { startIndex?: number; limit?: number; genres?: string[] } = {}
  ) {
    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));

    try {
      const startTime = performance.now();
      const repo = auth.getRepository();

      console.log(`📚 [LibraryStore] Loading items for parent: ${parentId.substring(0, 8)}...`);

      const result = await repo.getItems(parentId, {
        startIndex: options.startIndex ?? 0,
        limit: options.limit ?? 10000,
        fields: ["PrimaryImageAspectRatio", "Overview", "MediaStreams"],
        sortBy: "SortName",
        sortOrder: "Ascending",
        genres: options.genres,
      });

      const loadTime = Math.round(performance.now() - startTime);

      if (loadTime < 100) {
        console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${result.items.length} items in ${loadTime}ms (instant)`);
      } else {
        console.log(`⏳ [LibraryStore] Loaded ${result.items.length} items in ${loadTime}ms (from server)`);
      }

      update((s) => ({
        ...s,
        items: result.items,
        totalItems: result.totalRecordCount,
        loadingCount: Math.max(0, s.loadingCount - 1),
      }));

      return result;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load items";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  // Load Live TV channels (broadcast / IPTV) into the items list. Live TV uses a
  // dedicated Jellyfin endpoint rather than the generic /Items browse.
  async function loadLiveTvChannels() {
    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));
    try {
      const repo = auth.getRepository();
      const channels = await repo.getLiveTvChannels();
      update((s) => ({
        ...s,
        items: channels,
        totalItems: channels.length,
        loadingCount: Math.max(0, s.loadingCount - 1),
      }));
      return channels;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load Live TV channels";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  // Load the root list of plugin "Channels" into the items list.
  async function loadChannels() {
    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));
    try {
      const repo = auth.getRepository();
      const result = await repo.getChannels();
      update((s) => ({
        ...s,
        items: result.items,
        totalItems: result.totalRecordCount,
        loadingCount: Math.max(0, s.loadingCount - 1),
      }));
      return result;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load channels";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  async function loadItem(itemId: string) {
    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));

    try {
      const repo = auth.getRepository();
      const item = await repo.getItem(itemId);

      console.log(`[LibraryStore] loadItem(${itemId}): ${item.name} (${item.kind})`);
      console.log(`[LibraryStore] - Has people? ${item.people ? `YES (${item.people.length})` : 'NO'}`);
      if (item.people && item.people.length > 0) {
        item.people.forEach((p, i) => {
          console.log(`[LibraryStore]   [${i}] ${p.name} (type: "${p.type}", id: "${p.id}")`);
        });
      }

      update((s) => ({
        ...s,
        currentItem: item,
        loadingCount: Math.max(0, s.loadingCount - 1),
      }));

      return item;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load item";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  /**
   * Search the library, optionally narrowed to a scope.
   *
   * The scope is sent **opaque**; Rust expands it into Jellyfin item types
   * (`SearchScope::item_types()`) before the cache and server paths diverge, so
   * online and offline results filter identically. `all` resolves to no filter
   * at all — not the union of the other scopes, which would drop People and
   * folders.
   *
   * TRACES: UR-049 | DR-063, DR-065
   */
  async function search(query: string, scope: SearchScope = "all") {
    // Bump the request id for every call (including clears) so any in-flight
    // backend update for a previous query is ignored when it arrives.
    const requestId = ++searchRequestId;

    if (!query.trim()) {
      update((s) => ({ ...s, searchQuery: "", searchResults: [] }));
      return;
    }

    await ensureSearchListener();

    update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null, searchQuery: query }));

    try {
      const repo = auth.getRepository();

      // Add 10-second timeout to prevent indefinite hanging
      const timeoutPromise = new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error("Search timeout - please try again")), 10000)
      );

      // Phase 1: the command resolves with instant local-cache results. The
      // merged (cache + server) union arrives later via the `search-event`
      // listener above, tagged with this same requestId.
      // Send the opaque scope; Rust expands it to item types. The frontend
      // never names a Jellyfin item type in connection with search.
      const options: SearchOptions = { limit: 10000, scope };

      const result = await Promise.race([
        repo.search(query, options, requestId),
        timeoutPromise
      ]);

      // Only apply if this is still the active query (a newer search may have
      // started while we awaited).
      if (requestId === searchRequestId) {
        update((s) => ({ ...s, searchResults: result.items }));
      }

      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1) }));

      return result;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Search failed";
      update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
      throw error;
    }
  }

  function setCurrentLibrary(library: Library | null) {
    update((s) => ({ ...s, currentLibrary: library, items: [], currentItem: null }));
  }

  function clearSearch() {
    // Invalidate any in-flight backend search update.
    searchRequestId++;
    update((s) => ({ ...s, searchQuery: "", searchResults: [] }));
  }

  function setViewMode(mode: ViewMode) {
    if (typeof localStorage !== "undefined") {
      localStorage.setItem("jellytau-view-mode", mode);
    }
    update((s) => ({ ...s, viewMode: mode }));
  }

  function toggleViewMode() {
    update((s) => {
      const newMode = s.viewMode === "grid" ? "list" : "grid";
      if (typeof localStorage !== "undefined") {
        localStorage.setItem("jellytau-view-mode", newMode);
      }
      return { ...s, viewMode: newMode };
    });
  }

  async function loadGenres(parentId?: string) {
    try {
      const repo = auth.getRepository();
      const genres = await repo.getGenres(parentId);
      update((s) => ({ ...s, genres }));
      return genres;
    } catch (error) {
      console.error("Failed to load genres:", error);
      return [];
    }
  }

  function setSelectedGenres(genres: string[]) {
    update((s) => ({ ...s, selectedGenres: genres }));
  }

  function toggleGenre(genreName: string) {
    update((s) => {
      const current = s.selectedGenres;
      const newGenres = current.includes(genreName)
        ? current.filter((g) => g !== genreName)
        : [...current, genreName];
      return { ...s, selectedGenres: newGenres };
    });
  }

  function clearGenres() {
    update((s) => ({ ...s, selectedGenres: [] }));
  }

  function reset() {
    searchRequestId++;
    if (unlistenSearch) {
      unlistenSearch();
      unlistenSearch = null;
    }
    set(initialState);
  }

  return {
    subscribe,
    loadLibraries,
    loadItems,
    loadLiveTvChannels,
    loadChannels,
    loadItem,
    search,
    setCurrentLibrary,
    clearSearch,
    setViewMode,
    toggleViewMode,
    loadGenres,
    setSelectedGenres,
    toggleGenre,
    clearGenres,
    reset,
  };
}

export const library = createLibraryStore();

// Derived stores
export const libraries = derived(library, ($lib) => $lib.libraries);
export const currentLibrary = derived(library, ($lib) => $lib.currentLibrary);
export const libraryItems = derived(library, ($lib) => $lib.items);
export const isLibraryLoading = derived(library, ($lib) => $lib.loadingCount > 0);
export const libraryError = derived(library, ($lib) => $lib.error);
export const viewMode = derived(library, ($lib) => $lib.viewMode);
export const genres = derived(library, ($lib) => $lib.genres);
export const selectedGenres = derived(library, ($lib) => $lib.selectedGenres);
