// Music library landing page data store.
// Powers the focused music landing: hero + horizontal sliders.
// TRACES: UR-007, UR-034 | DR-007, DR-038, DR-039
import { writable, derived } from "svelte/store";
import type { MediaItem, Genre } from "$lib/api/types";
import { auth } from "./auth";
import { excludePodcasts } from "$lib/utils/podcastFilter";
import { selectDiverseGenres, sampleAcross } from "$lib/utils/genreDiversity";
import { buildHeroMix } from "$lib/utils/heroMix";

/** A single "by genre" row: the genre name plus the albums in it. */
export interface GenreRow {
  id: string;
  name: string;
  items: MediaItem[];
}

interface MusicState {
  // Albums grouped from recently played tracks (backend handles grouping).
  recentlyPlayed: MediaItem[];
  // Most recently added albums in the music library.
  newlyAdded: MediaItem[];
  // User playlists.
  playlists: MediaItem[];
  // Albums the user has played but hasn't returned to in a while.
  rediscover: MediaItem[];
  // One slider per genre (top genres by album count).
  genreRows: GenreRow[];
  // Mix of recently played + rediscover, used for the hero banner.
  heroItems: MediaItem[];
  isLoading: boolean;
  error: string | null;
}

const SECTION_LIMIT = 16;
// How many genre sliders to show, and how many genres to probe to find them.
const MAX_GENRE_ROWS = 8;
// Probe a wide pool so the diverse-selection step has genres from across the
// whole (alphabetical) list to choose between, not just the first handful.
const MAX_GENRES_PROBED = 40;

function createMusicStore() {
  const initialState: MusicState = {
    recentlyPlayed: [],
    newlyAdded: [],
    playlists: [],
    rediscover: [],
    genreRows: [],
    heroItems: [],
    isLoading: false,
    error: null,
  };

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

  /** Artwork check for hero candidates: needs a primary image or backdrop. */
  const hasArt = (i: MediaItem) =>
    !!i.imageId || !!(i.backdropImageTags && i.backdropImageTags.length > 0);

  async function loadSections(libraryId: string) {
    update(s => ({
      ...s,
      isLoading: s.recentlyPlayed.length === 0 && s.newlyAdded.length === 0,
      error: null,
    }));

    try {
      const repo = auth.getRepository();

      const [recentlyPlayed, newlyAdded, playlistsResult, rediscover, surprise] = await Promise.all([
        repo.getRecentlyPlayedAudio(SECTION_LIMIT),
        repo.getItems(libraryId, {
          includeItemTypes: ["MusicAlbum"],
          sortBy: "DateCreated",
          sortOrder: "Descending",
          recursive: true,
          limit: SECTION_LIMIT,
        }),
        repo.getItems(libraryId, {
          includeItemTypes: ["Playlist"],
          sortBy: "SortName",
          sortOrder: "Ascending",
          recursive: true,
          limit: SECTION_LIMIT,
        }),
        repo.getRediscoverAlbums(libraryId, SECTION_LIMIT),
        // Random pool so the hero rotation changes between visits (SortBy=Random
        // shuffles server-side online, and via SQLite RANDOM() offline).
        repo
          .getItems(libraryId, {
            includeItemTypes: ["MusicAlbum"],
            sortBy: "Random",
            recursive: true,
            limit: SECTION_LIMIT,
          })
          .then(r => r.items)
          .catch(() => [] as MediaItem[]),
      ]);

      // HACK: drop the "Podcasts" folder that lives inside the music library.
      const recentlyPlayedAlbums = excludePodcasts(recentlyPlayed);
      const newlyAddedAlbums = excludePodcasts(newlyAdded.items);
      const playlistItems = excludePodcasts(playlistsResult.items);
      const rediscoverAlbums = excludePodcasts(rediscover);
      const surpriseAlbums = excludePodcasts(surprise);

      // Mix the hero: fresh-in-your-ears first, then "remember this?", then
      // random albums from across the library.
      const heroItems = buildHeroMix([recentlyPlayedAlbums, rediscoverAlbums, surpriseAlbums], hasArt);

      update(s => ({
        ...s,
        recentlyPlayed: recentlyPlayedAlbums,
        newlyAdded: newlyAddedAlbums,
        playlists: playlistItems,
        rediscover: rediscoverAlbums,
        heroItems,
        isLoading: false,
      }));

      // Genre rows are secondary — load them after the main sections paint so
      // the page isn't blocked on N per-genre queries.
      loadGenreRows(libraryId);
    } catch (error) {
      const message = error instanceof Error ? error.message : "Failed to load music sections";
      update(s => ({ ...s, isLoading: false, error: message }));
      console.error("Failed to load music sections:", error);
    }
  }

  /** Fetch up to SECTION_LIMIT albums for one genre as a slider row. */
  async function loadGenreRow(libraryId: string, genre: Genre): Promise<GenreRow> {
    const repo = auth.getRepository();
    try {
      const result = await repo.getItems(libraryId, {
        includeItemTypes: ["MusicAlbum"],
        genres: [genre.name],
        sortBy: "SortName",
        sortOrder: "Ascending",
        recursive: true,
        limit: SECTION_LIMIT,
      });
      // HACK: drop the "Podcasts" folder that lives in the music library.
      return { id: genre.id, name: genre.name, items: excludePodcasts(result.items) };
    } catch (e) {
      console.warn(`Failed to load genre row "${genre.name}":`, e);
      return { id: genre.id, name: genre.name, items: [] };
    }
  }

  /**
   * Build one slider per genre, showing albums in each. We pick a *diverse* set
   * rather than just the most populous — otherwise a cluster of near-synonyms
   * ("Rock", "Hard Rock", "Classic Rock", ...) crowds out musically distinct
   * genres. See selectDiverseGenres. Album genres rarely carry community
   * ratings, so we order each row by name.
   *
   * When the backend reports per-genre album counts (online), we rank and pick
   * before fetching, so we only query albums for the genres we'll actually
   * show. When counts are missing (offline), we fall back to probing a wide
   * pool, dropping empties, then ranking by what came back.
   */
  async function loadGenreRows(libraryId: string) {
    try {
      const repo = auth.getRepository();
      const genres = await repo.getGenres(libraryId);
      if (genres.length === 0) return;

      // Counts are only *useful* if they actually differentiate genres. Some
      // servers return Fields=ItemCounts but populate every genre with the same
      // value (or 0), which leaves the list in its original alphabetical order
      // after "ranking" — so diversity selection seeds on the first genre and we
      // get a wall of A-genres ("avangard", "avan-gard", ...) with no Rock.
      // Treat that as "no usable counts" and fall through to the probe path,
      // which ranks by genres' real album counts instead.
      const positiveCounts = genres
        .map(g => g.albumCount)
        .filter((c): c is number => c != null && c > 0);
      const hasUsefulCounts = new Set(positiveCounts).size > 1;

      let genreRows: GenreRow[];
      if (hasUsefulCounts) {
        // Rank by reported count, pick a diverse subset, then fetch only those.
        const ranked = [...genres].sort(
          (a, b) => (b.albumCount ?? 0) - (a.albumCount ?? 0)
        );
        const chosen = selectDiverseGenres(ranked, MAX_GENRE_ROWS);
        genreRows = (await Promise.all(chosen.map(g => loadGenreRow(libraryId, g)))).filter(
          row => row.items.length > 0
        );
      } else {
        // No usable counts (offline, a server that ignores Fields=ItemCounts,
        // or one that returns uniform/zero counts — see hasUsefulCounts above).
        // The genre list is alphabetical, so probing the first N would only
        // ever surface A-genres. Sample at an even stride across the whole
        // list instead, so the probe pool spans A→Z; then drop empties, rank
        // by what came back, and pick a diverse subset.
        const probed = sampleAcross(genres, MAX_GENRES_PROBED);
        const rows = await Promise.all(probed.map(g => loadGenreRow(libraryId, g)));
        const populated = rows
          .filter(row => row.items.length > 0)
          .sort((a, b) => b.items.length - a.items.length);
        genreRows = selectDiverseGenres(populated, MAX_GENRE_ROWS);
      }

      update(s => ({ ...s, genreRows }));
    } catch (e) {
      console.warn("Failed to load music genre rows:", e);
    }
  }

  function reset() {
    set(initialState);
  }

  return {
    subscribe,
    loadSections,
    reset,
  };
}

export const music = createMusicStore();

export const musicHeroItems = derived(music, $m => $m.heroItems);
export const isMusicLoading = derived(music, $m => $m.isLoading);
