import type { Manga, Chapter, Category, Source } from "../lib/types";
import { DEFAULT_KEYBINDS, type Keybinds } from "../lib/keybinds";

export type PageStyle        = "single" | "double" | "longstrip";
export type FitMode          = "width" | "height" | "screen" | "original";
export type LibraryFilter    = "all" | "library" | "downloaded" | string;
export type NavPage          = "home" | "library" | "sources" | "explore" | "downloads" | "extensions" | "history" | "search" | "tracking";
export type ReadingDirection = "ltr" | "rtl";
export type ChapterSortDir   = "desc" | "asc";
export type ChapterSortMode  = "source" | "chapterNumber" | "uploadDate";

export type LibrarySortMode =
  | "az"
  | "unreadCount"
  | "totalChapters"
  | "recentlyAdded"
  | "recentlyRead"
  | "latestFetched"
  | "latestUploaded";

export type LibrarySortDir = "asc" | "desc";

export type LibraryStatusFilter =
  | "ALL"
  | "ONGOING"
  | "COMPLETED"
  | "CANCELLED"
  | "HIATUS"
  | "UNKNOWN";

export type LibraryContentFilter =
  | "unread"
  | "started"
  | "downloaded"
  | "bookmarked"
  | "marked";

export type BuiltinTheme = "dark" | "high-contrast" | "light" | "light-contrast" | "midnight" | "warm";
export type Theme        = BuiltinTheme | string;

export interface ThemeTokens {
  "bg-void":    string;
  "bg-base":    string;
  "bg-surface": string;
  "bg-raised":  string;
  "bg-overlay": string;
  "bg-subtle":  string;
  "border-dim":    string;
  "border-base":   string;
  "border-strong": string;
  "border-focus":  string;
  "text-primary":   string;
  "text-secondary": string;
  "text-muted":     string;
  "text-faint":     string;
  "text-disabled":  string;
  "accent":        string;
  "accent-dim":    string;
  "accent-muted":  string;
  "accent-fg":     string;
  "accent-bright": string;
  "color-error":    string;
  "color-error-bg": string;
  "color-success":  string;
  "color-info":     string;
  "color-info-bg":  string;
}

export interface CustomTheme {
  id:     string;
  name:   string;
  tokens: ThemeTokens;
}

export const DEFAULT_THEME_TOKENS: ThemeTokens = {
  "bg-void":    "#080808",
  "bg-base":    "#0c0c0c",
  "bg-surface": "#101010",
  "bg-raised":  "#151515",
  "bg-overlay": "#1a1a1a",
  "bg-subtle":  "#202020",
  "border-dim":    "#1c1c1c",
  "border-base":   "#242424",
  "border-strong": "#2e2e2e",
  "border-focus":  "#4a5c4a",
  "text-primary":   "#f0efec",
  "text-secondary": "#c8c6c0",
  "text-muted":     "#8a8880",
  "text-faint":     "#4e4d4a",
  "text-disabled":  "#2a2a28",
  "accent":        "#6b8f6b",
  "accent-dim":    "#2a3d2a",
  "accent-muted":  "#1a251a",
  "accent-fg":     "#a8c4a8",
  "accent-bright": "#8fb88f",
  "color-error":    "#c47a7a",
  "color-error-bg": "#1f1212",
  "color-success":  "#7aab7a",
  "color-info":     "#7a9ec4",
  "color-info-bg":  "#121a1f",
};

export interface HistoryEntry {
  mangaId:      number;
  mangaTitle:   string;
  thumbnailUrl: string;
  chapterId:    number;
  chapterName:  string;
  readAt:       number;
}

export interface BookmarkEntry {
  mangaId:      number;
  mangaTitle:   string;
  thumbnailUrl: string;
  chapterId:    number;
  chapterName:  string;
  pageNumber:   number;
  savedAt:      number;
  label?:       string;
}

export type MarkerColor = "yellow" | "red" | "blue" | "green" | "purple";

export interface MarkerEntry {
  id:           string;
  mangaId:      number;
  mangaTitle:   string;
  thumbnailUrl: string;
  chapterId:    number;
  chapterName:  string;
  pageNumber:   number;
  note:         string;
  color:        MarkerColor;
  createdAt:    number;
  updatedAt?:   number;
}

export interface ReadLogEntry {
  mangaId:   number;
  chapterId: number;
  readAt:    number;
  minutes:   number;
}

export interface ReadingStats {
  totalChaptersRead: number;
  totalMangaRead:    number;
  totalMinutesRead:  number;
  firstReadAt:       number;
  lastReadAt:        number;
  currentStreakDays: number;
  longestStreakDays: number;
  lastStreakDate:    string;
}

export interface LibraryUpdateEntry {
  mangaId:      number;
  mangaTitle:   string;
  thumbnailUrl: string;
  newChapters:  number;
  checkedAt:    number;
}

const AVG_MIN_PER_CHAPTER = 5;

export const DEFAULT_READING_STATS: ReadingStats = {
  totalChaptersRead: 0,
  totalMangaRead:    0,
  totalMinutesRead:  0,
  firstReadAt:       0,
  lastReadAt:        0,
  currentStreakDays: 0,
  longestStreakDays: 0,
  lastStreakDate:    "",
};

export interface Toast {
  id:        string;
  kind:      "success" | "error" | "info" | "download";
  title:     string;
  body?:     string;
  duration?: number;
}

export interface ActiveDownload {
  chapterId: number;
  mangaId:   number;
  progress:  number;
}

export interface MangaPrefs {
  autoDownload:       boolean;
  downloadAhead:      number;
  deleteOnRead:       boolean;
  deleteDelayHours:   number;
  maxKeepChapters:    number;
  pauseUpdates:       boolean;
  refreshInterval:    "global" | "daily" | "weekly" | "manual";
  preferredScanlator: string;
  scanlatorFilter:    string[];
}

export const DEFAULT_MANGA_PREFS: MangaPrefs = {
  autoDownload:       false,
  downloadAhead:      0,
  deleteOnRead:       false,
  deleteDelayHours:   0,
  maxKeepChapters:    0,
  pauseUpdates:       false,
  refreshInterval:    "global",
  preferredScanlator: "",
  scanlatorFilter:    [],
};

export interface Settings {
  pageStyle:               PageStyle;
  readingDirection:        ReadingDirection;
  fitMode:                 FitMode;
  readerZoom:              number;
  pageGap:                 boolean;
  optimizeContrast:        boolean;
  offsetDoubleSpreads:     boolean;
  preloadPages:            number;
  autoMarkRead:            boolean;
  autoNextChapter:         boolean;
  libraryCropCovers:       boolean;
  libraryPageSize:         number;
  showNsfw:                boolean;
  discordRpc:              boolean;
  chapterSortDir:          ChapterSortDir;
  chapterSortMode:         ChapterSortMode;
  chapterPageSize:         number;
  uiZoom:                  number;
  compactSidebar:          boolean;
  gpuAcceleration:         boolean;
  serverUrl:               string;
  serverBinary:            string;
  autoStartServer:         boolean;
  preferredExtensionLang:  string;
  keybinds:                Keybinds;
  idleTimeoutMin?:         number;
  splashCards?:            boolean;
  storageLimitGb:          number | null;
  markReadOnNext:          boolean;
  readerDebounceMs:        number;
  autoBookmark:            boolean;
  theme:                   Theme;
  libraryBranches:         boolean;
  renderLimit:             number;
  heroSlots:               (number | null)[];
  mangaLinks:              Record<number, number[]>;
  mangaPrefs:              Record<number, Partial<MangaPrefs>>;
  serverAuthUser:          string;
  serverAuthPass:          string;
  serverAuthMode:          "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN";
  socksProxyEnabled:       boolean;
  socksProxyHost:          string;
  socksProxyPort:          string;
  socksProxyVersion:       number;
  socksProxyUsername:      string;
  socksProxyPassword:      string;
  flareSolverrEnabled:     boolean;
  flareSolverrUrl:         string;
  flareSolverrTimeout:     number;
  flareSolverrSessionName: string;
  flareSolverrSessionTtl:  number;
  flareSolverrFallback:    boolean;
  appLockEnabled:          boolean;
  appLockPin:              string;
  customThemes:            CustomTheme[];
  hiddenCategoryIds:       number[];
  defaultLibraryCategoryId: number | null;
  savedIsDefaultCategory:  boolean;
  nsfwFilteredTags:        string[];
  nsfwAllowedSourceIds:    string[];
  nsfwBlockedSourceIds:    string[];
  libraryTabSort:          Record<string, { mode: LibrarySortMode; dir: LibrarySortDir }>;
  libraryTabStatus:        Record<string, LibraryStatusFilter>;
  libraryTabFilters:       Record<string, Partial<Record<LibraryContentFilter, boolean>>>;
  maxPageWidth?:           number;
  uiScale?:                number;
  extraScanDirs:           string[];
  serverDownloadsPath:     string;
  serverLocalSourcePath:   string;
}

export const DEFAULT_SETTINGS: Settings = {
  pageStyle:              "longstrip",
  readingDirection:       "ltr",
  fitMode:                "width",
  readerZoom:             1.0,
  pageGap:                true,
  optimizeContrast:       false,
  offsetDoubleSpreads:    false,
  preloadPages:           3,
  autoMarkRead:           true,
  autoNextChapter:        true,
  libraryCropCovers:      true,
  libraryPageSize:        48,
  showNsfw:               false,
  discordRpc:             false,
  chapterSortDir:         "desc",
  chapterSortMode:        "source",
  chapterPageSize:        25,
  uiZoom:                 1.0,
  compactSidebar:         false,
  gpuAcceleration:        true,
  serverUrl:              "http://localhost:4567",
  serverBinary:           "",
  autoStartServer:        true,
  preferredExtensionLang: "en",
  keybinds:               DEFAULT_KEYBINDS,
  idleTimeoutMin:         5,
  splashCards:            true,
  storageLimitGb:         null,
  markReadOnNext:         true,
  readerDebounceMs:       120,
  autoBookmark:           true,
  theme:                  "dark",
  libraryBranches:        true,
  renderLimit:            48,
  heroSlots:              [null, null, null, null],
  mangaLinks:             {},
  mangaPrefs:             {},
  serverAuthUser:         "",
  serverAuthPass:         "",
  serverAuthMode:         "NONE",
  socksProxyEnabled:      false,
  socksProxyHost:         "",
  socksProxyPort:         "1080",
  socksProxyVersion:      5,
  socksProxyUsername:     "",
  socksProxyPassword:     "",
  flareSolverrEnabled:    false,
  flareSolverrUrl:        "http://localhost:8191",
  flareSolverrTimeout:    60,
  flareSolverrSessionName: "moku",
  flareSolverrSessionTtl: 15,
  flareSolverrFallback:   false,
  appLockEnabled:         false,
  appLockPin:             "",
  customThemes:           [],
  hiddenCategoryIds:      [],
  defaultLibraryCategoryId: null,
  savedIsDefaultCategory:  false,
  nsfwFilteredTags:      ["adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence"],
  nsfwAllowedSourceIds:  [],
  nsfwBlockedSourceIds:  [],
  libraryTabSort:         {},
  libraryTabStatus:       {},
  libraryTabFilters:      {},
  extraScanDirs:          [],
  serverDownloadsPath:    "",
  serverLocalSourcePath:  "",
};

const STORE_VERSION = 3;

const RESET_ON_UPGRADE: (keyof Settings)[] = [
  "serverBinary",
  "readerZoom",
  "uiZoom",
];

function loadPersisted(): any {
  try {
    const raw = localStorage.getItem("moku-store");
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

function persist(patch: Record<string, unknown>) {
  try {
    const current = loadPersisted() ?? {};
    localStorage.setItem("moku-store", JSON.stringify({ ...current, ...patch }));
  } catch {}
}

const saved = (() => {
  const data = loadPersisted();
  if (!data) return null;
  if ((data.storeVersion ?? 1) < STORE_VERSION) {
    const resetPatch: Partial<Settings> = {};
    for (const key of RESET_ON_UPGRADE) {
      (resetPatch as any)[key] = (DEFAULT_SETTINGS as any)[key];
    }
    const migrated = {
      ...data,
      storeVersion: STORE_VERSION,
      settings: { ...data.settings, ...resetPatch },
    };
    try {
      localStorage.setItem("moku-store", JSON.stringify(migrated));
    } catch {}
    return migrated;
  }
  return data;
})();

function mergeSettings(saved: any): Settings {
  return {
    ...DEFAULT_SETTINGS,
    ...saved?.settings,
    keybinds:          { ...DEFAULT_KEYBINDS, ...saved?.settings?.keybinds },
    heroSlots:         saved?.settings?.heroSlots         ?? [null, null, null, null],
    mangaLinks:        saved?.settings?.mangaLinks        ?? {},
    mangaPrefs:        saved?.settings?.mangaPrefs        ?? {},
    customThemes:      saved?.settings?.customThemes      ?? [],
    hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [],
    nsfwFilteredTags:  saved?.settings?.nsfwFilteredTags  ?? DEFAULT_SETTINGS.nsfwFilteredTags,
    nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [],
    nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [],
    libraryTabSort:    saved?.settings?.libraryTabSort    ?? {},
    libraryTabStatus:  saved?.settings?.libraryTabStatus  ?? {},
    libraryTabFilters: saved?.settings?.libraryTabFilters ?? {},
    extraScanDirs:     saved?.settings?.extraScanDirs     ?? [],
  };
}

class Store {
  settings:        Settings        = $state(mergeSettings(saved));
  activeManga:     Manga | null    = $state(null);
  previewManga:    Manga | null    = $state(null);
  activeChapter:   Chapter | null  = $state(null);
  activeChapterList: Chapter[]     = $state([]);
  pageUrls:        string[]        = $state([]);
  pageNumber:      number          = $state(1);
  navPage:         NavPage         = $state("home");
  libraryFilter:   LibraryFilter   = $state("all");
  genreFilter:     string          = $state("");
  searchPrefill:   string          = $state("");
  toasts:          Toast[]         = $state([]);
  categories:      Category[]      = $state([]);
  activeDownloads: ActiveDownload[] = $state([]);
  activeSource:    Source | null   = $state(null);
  libraryTagFilter: string[]       = $state([]);
  settingsOpen:    boolean         = $state(false);
  history:         HistoryEntry[]  = $state(saved?.history  ?? []);
  bookmarks:       BookmarkEntry[] = $state(saved?.bookmarks ?? []);
  markers:         MarkerEntry[]   = $state(saved?.markers   ?? []);
  readLog:         ReadLogEntry[]  = $state(saved?.readLog   ?? []);
  readingStats:    ReadingStats    = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS });
  discoverCache:   Map<string, any> = $state(new Map());
  discoverLibraryIds: Set<number>  = $state(new Set());
  discoverSrcOffset: number        = $state(0);
  readerSessionId: number          = $state(0);
  libraryUpdates:  LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
  lastLibraryRefresh: number       = $state(saved?.lastLibraryRefresh ?? 0);
  acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));

  constructor() {
    $effect.root(() => {
      $effect(() => {
        persist({
          settings:              this.settings,
          history:               this.history,
          bookmarks:             this.bookmarks,
          markers:               this.markers,
          readLog:               this.readLog,
          readingStats:          this.readingStats,
          libraryUpdates:        this.libraryUpdates,
          lastLibraryRefresh:    this.lastLibraryRefresh,
          acknowledgedUpdateIds: [...this.acknowledgedUpdates],
          storeVersion:          STORE_VERSION,
        });
      });
    });
  }

  openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) {
    this.activeChapter     = chapter;
    this.activeChapterList = chapterList;
    if (manga !== undefined) this.activeManga = manga;
  }

  closeReader() {
    this.activeChapter     = null;
    this.activeChapterList = [];
    this.pageUrls          = [];
    this.pageNumber        = 1;
  }

  addHistory(entry: HistoryEntry, completed = false, minutes?: number) {
    const filtered = this.history.filter(h => h.chapterId !== entry.chapterId);
    this.history   = [entry, ...filtered].slice(0, 500);

    if (completed) {
      const existing = this.readLog.find(e => e.chapterId === entry.chapterId);
      if (!existing) {
        const mins = minutes ?? AVG_MIN_PER_CHAPTER;
        this.readLog = [...this.readLog, { mangaId: entry.mangaId, chapterId: entry.chapterId, readAt: entry.readAt, minutes: mins }];
        const uniqueChapters = new Set(this.readLog.map(e => e.chapterId));
        const uniqueManga    = new Set(this.readLog.map(e => e.mangaId));
        const totalMinutes   = this.readLog.reduce((sum, e) => sum + e.minutes, 0);
        const now            = new Date();
        const todayStr       = now.toISOString().slice(0, 10);
        const lastDate       = this.readingStats.lastStreakDate;
        const yesterday      = new Date(now); yesterday.setDate(yesterday.getDate() - 1);
        const yesterdayStr   = yesterday.toISOString().slice(0, 10);
        let streak = this.readingStats.currentStreakDays;
        if (lastDate === todayStr) {
        } else if (lastDate === yesterdayStr) {
          streak++;
        } else {
          streak = 1;
        }
        const longest = Math.max(this.readingStats.longestStreakDays, streak);
        this.readingStats = {
          totalChaptersRead: uniqueChapters.size,
          totalMangaRead:    uniqueManga.size,
          totalMinutesRead:  totalMinutes,
          firstReadAt:       this.readingStats.firstReadAt || entry.readAt,
          lastReadAt:        entry.readAt,
          currentStreakDays: streak,
          longestStreakDays: longest,
          lastStreakDate:    todayStr,
        };
      }
    }
  }

  addBookmark(entry: Omit<BookmarkEntry, "savedAt">, label?: string) {
    const filtered = this.bookmarks.filter(b => b.chapterId !== entry.chapterId);
    this.bookmarks = [{ ...entry, savedAt: Date.now(), label }, ...filtered].slice(0, 200);
  }

  removeBookmark(chapterId: number) {
    this.bookmarks = this.bookmarks.filter(b => b.chapterId !== chapterId);
  }

  clearBookmarks() { this.bookmarks = []; }

  getBookmark(chapterId: number): BookmarkEntry | undefined {
    return this.bookmarks.find(b => b.chapterId === chapterId);
  }

  addMarker(entry: Omit<MarkerEntry, "id" | "createdAt">): string {
    const id  = Math.random().toString(36).slice(2);
    this.markers = [...this.markers, { ...entry, id, createdAt: Date.now() }];
    return id;
  }

  updateMarker(id: string, patch: Partial<Pick<MarkerEntry, "note" | "color">>) {
    this.markers = this.markers.map(m => m.id === id ? { ...m, ...patch, updatedAt: Date.now() } : m);
  }

  removeMarker(id: string) {
    this.markers = this.markers.filter(m => m.id !== id);
  }

  getMarkersForPage(chapterId: number, page: number): MarkerEntry[] {
    return this.markers.filter(m => m.chapterId === chapterId && m.pageNumber === page);
  }

  getMarkersForChapter(chapterId: number): MarkerEntry[] {
    return this.markers.filter(m => m.chapterId === chapterId);
  }

  getMarkersForManga(mangaId: number): MarkerEntry[] {
    return this.markers.filter(m => m.mangaId === mangaId);
  }

  clearMarkersForManga(mangaId: number) {
    this.markers = this.markers.filter(m => m.mangaId !== mangaId);
  }

  clearHistory() { this.history = []; this.readLog = []; }

  clearHistoryForManga(mangaId: number) {
    this.history = this.history.filter(x => x.mangaId !== mangaId);
    this.readLog = this.readLog.filter(x => x.mangaId !== mangaId);
    const uniqueChapters = new Set(this.readLog.map(e => e.chapterId));
    const uniqueManga    = new Set(this.readLog.map(e => e.mangaId));
    const totalMinutes   = this.readLog.reduce((sum, e) => sum + e.minutes, 0);
    this.readingStats = {
      ...this.readingStats,
      totalChaptersRead: uniqueChapters.size,
      totalMangaRead:    uniqueManga.size,
      totalMinutesRead:  totalMinutes,
    };
  }

  wipeAllData() {
    this.history      = [];
    this.readLog      = [];
    this.markers      = [];
    this.readingStats = { ...DEFAULT_READING_STATS };
    this.settings     = { ...this.settings, heroSlots: [null, null, null, null], mangaLinks: {} };
  }

  linkManga(idA: number, idB: number) {
    if (idA === idB) return;
    const links = { ...this.settings.mangaLinks };
    links[idA]  = [...new Set([...(links[idA] ?? []), idB])];
    links[idB]  = [...new Set([...(links[idB] ?? []), idA])];
    this.settings = { ...this.settings, mangaLinks: links };
  }

  unlinkManga(idA: number, idB: number) {
    const links = { ...this.settings.mangaLinks };
    links[idA]  = (links[idA] ?? []).filter(id => id !== idB);
    links[idB]  = (links[idB] ?? []).filter(id => id !== idA);
    if (!links[idA].length) delete links[idA];
    if (!links[idB].length) delete links[idB];
    this.settings = { ...this.settings, mangaLinks: links };
  }

  getLinkedMangaIds(mangaId: number): number[] { return this.settings.mangaLinks[mangaId] ?? []; }

  setHeroSlot(index: 1 | 2 | 3, mangaId: number | null) {
    const slots = [...(this.settings.heroSlots ?? [null, null, null, null])];
    slots[index] = mangaId;
    this.settings = { ...this.settings, heroSlots: slots };
  }

  addToast(toast: Omit<Toast, "id">) {
    this.toasts = [...this.toasts, { ...toast, id: Math.random().toString(36).slice(2) }].slice(-5);
  }

  dismissToast(id: string)                    { this.toasts          = this.toasts.filter(x => x.id !== id); }
  setCategories(cats: Category[])             { this.categories       = cats; }
  setActiveDownloads(next: ActiveDownload[])  { this.activeDownloads  = next; }
  setNavPage(next: NavPage)                   { this.navPage          = next; }
  setLibraryFilter(next: LibraryFilter)       { this.libraryFilter    = next; }
  setGenreFilter(next: string)                { this.genreFilter      = next; }
  setSearchPrefill(next: string)              { this.searchPrefill    = next; }
  setActiveManga(next: Manga | null)          { this.activeManga      = next; }
  setPreviewManga(next: Manga | null)         { this.previewManga     = next; }
  setActiveSource(next: Source | null)        { this.activeSource     = next; }
  setPageUrls(next: string[])                 { this.pageUrls         = next; }
  setPageNumber(next: number)                 { this.pageNumber       = next; }
  setLibraryTagFilter(next: string[])         { this.libraryTagFilter = next; }
  setSettingsOpen(next: boolean)              { this.settingsOpen     = next; }
  updateSettings(patch: Partial<Settings>)    { this.settings = { ...this.settings, ...patch }; }
  resetKeybinds()                             { this.settings = { ...this.settings, keybinds: DEFAULT_KEYBINDS }; }

  saveCustomTheme(theme: CustomTheme) {
    const existing = this.settings.customThemes.findIndex(t => t.id === theme.id);
    const next = existing >= 0
      ? this.settings.customThemes.map((t, i) => i === existing ? theme : t)
      : [...this.settings.customThemes, theme];
    this.settings = { ...this.settings, customThemes: next };
  }

  deleteCustomTheme(id: string) {
    const next = this.settings.customThemes.filter(t => t.id !== id);
    const wasActive = this.settings.theme === id;
    this.settings = { ...this.settings, customThemes: next, theme: wasActive ? "dark" : this.settings.theme };
  }

  async checkAndMarkCompleted(
    mangaId:    number,
    chaps:      Chapter[],
    categories: Category[],
    gqlFn:      (query: string, vars: Record<string, unknown>) => Promise<unknown>,
    UPDATE_MANGA_CATEGORIES: string,
    UPDATE_MANGA?: string,
    mangaStatus?: string,
  ): Promise<void> {
    if (!chaps.length) return;
    // Never auto-complete an ongoing series — user must set Completed manually.
    if (mangaStatus === "ONGOING") return;
    const allRead   = chaps.every(c => c.isRead);
    const completed = categories.find(c => c.name === "Completed");
    if (!completed) return;
    if (allRead) {
      await gqlFn(UPDATE_MANGA_CATEGORIES, { mangaId, addTo: [completed.id], removeFrom: [] }).catch(console.error);
      if (UPDATE_MANGA) {
        await gqlFn(UPDATE_MANGA, { id: mangaId, inLibrary: true }).catch(console.error);
      }
    } else {
      await gqlFn(UPDATE_MANGA_CATEGORIES, { mangaId, addTo: [], removeFrom: [completed.id] }).catch(console.error);
    }
  }

  toggleHiddenCategory(id: number) {
    const ids = this.settings.hiddenCategoryIds ?? [];
    const next = ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id];
    this.settings = { ...this.settings, hiddenCategoryIds: next };
  }

  clearDiscoverCache() {
    this.discoverCache      = new Map();
    this.discoverLibraryIds = new Set();
    this.discoverSrcOffset++;
  }

  setLibraryUpdates(entries: LibraryUpdateEntry[]) {
    this.libraryUpdates     = entries;
    this.lastLibraryRefresh = Date.now();
  }

  clearLibraryUpdates() {
    this.libraryUpdates      = [];
    this.lastLibraryRefresh  = 0;
    this.acknowledgedUpdates = new Set();
  }

  acknowledgeUpdate(mangaId: number) {
    if (this.acknowledgedUpdates.has(mangaId)) return;
    this.acknowledgedUpdates = new Set([...this.acknowledgedUpdates, mangaId]);
  }

  bumpReaderSession() {
    this.readerSessionId++;
  }
}

export const store = new Store();

export function openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) { store.openReader(chapter, chapterList, manga); }
export function closeReader()                                              { store.closeReader(); }
export function addHistory(entry: HistoryEntry, completed?: boolean, minutes?: number) { store.addHistory(entry, completed, minutes); }
export function clearHistory()                                             { store.clearHistory(); }
export function clearHistoryForManga(mangaId: number)                      { store.clearHistoryForManga(mangaId); }
export function wipeAllData()                                              { store.wipeAllData(); }
export function linkManga(idA: number, idB: number)                        { store.linkManga(idA, idB); }
export function unlinkManga(idA: number, idB: number)                      { store.unlinkManga(idA, idB); }
export function getLinkedMangaIds(mangaId: number)                         { return store.getLinkedMangaIds(mangaId); }
export function setHeroSlot(i: 1|2|3, mangaId: number | null)              { store.setHeroSlot(i, mangaId); }
export function addToast(toast: Omit<Toast, "id">)                         { store.addToast(toast); }
export function dismissToast(id: string)                                   { store.dismissToast(id); }
export function setCategories(cats: Category[])                             { store.setCategories(cats); }
export function setActiveDownloads(next: ActiveDownload[])                 { store.setActiveDownloads(next); }
export function setNavPage(next: NavPage)                                  { store.setNavPage(next); }
export function setLibraryFilter(next: LibraryFilter)                      { store.setLibraryFilter(next); }
export function setGenreFilter(next: string)                               { store.setGenreFilter(next); }
export function setSearchPrefill(next: string)                             { store.setSearchPrefill(next); }
export function setActiveManga(next: Manga | null)                         { store.setActiveManga(next); }
export function setPreviewManga(next: Manga | null)                        { store.setPreviewManga(next); }
export function setActiveSource(next: Source | null)                       { store.setActiveSource(next); }
export function setPageUrls(next: string[])                                { store.setPageUrls(next); }
export function setPageNumber(next: number)                                { store.setPageNumber(next); }
export function setLibraryTagFilter(next: string[])                        { store.setLibraryTagFilter(next); }
export function setSettingsOpen(next: boolean)                             { store.setSettingsOpen(next); }
export function updateSettings(patch: Partial<Settings>)                   { store.updateSettings(patch); }
export function resetKeybinds()                                            { store.resetKeybinds(); }
export function clearDiscoverCache()                                         { store.clearDiscoverCache(); }
export function setLibraryUpdates(entries: LibraryUpdateEntry[])             { store.setLibraryUpdates(entries); }
export function clearLibraryUpdates()                                        { store.clearLibraryUpdates(); }
export function acknowledgeUpdate(mangaId: number)                           { store.acknowledgeUpdate(mangaId); }
export function bumpReaderSession()                                          { store.bumpReaderSession(); }
export function addBookmark(entry: Omit<BookmarkEntry, "savedAt">, label?: string) { store.addBookmark(entry, label); }
export function removeBookmark(chapterId: number)                              { store.removeBookmark(chapterId); }
export function clearBookmarks()                                               { store.clearBookmarks(); }
export function getBookmark(chapterId: number)                                 { return store.getBookmark(chapterId); }
export function addMarker(entry: Omit<MarkerEntry, "id" | "createdAt">): string { return store.addMarker(entry); }
export function updateMarker(id: string, patch: Partial<Pick<MarkerEntry, "note" | "color">>) { store.updateMarker(id, patch); }
export function removeMarker(id: string)                                       { store.removeMarker(id); }
export function getMarkersForPage(chapterId: number, page: number)             { return store.getMarkersForPage(chapterId, page); }
export function getMarkersForChapter(chapterId: number)                        { return store.getMarkersForChapter(chapterId); }
export function getMarkersForManga(mangaId: number)                            { return store.getMarkersForManga(mangaId); }
export function clearMarkersForManga(mangaId: number)                          { store.clearMarkersForManga(mangaId); }
export function toggleHiddenCategory(id: number)                             { store.toggleHiddenCategory(id); }
export function saveCustomTheme(theme: CustomTheme)                          { store.saveCustomTheme(theme); }
export function deleteCustomTheme(id: string)                                { store.deleteCustomTheme(id); }
export async function checkAndMarkCompleted(
  mangaId:    number,
  chaps:      Chapter[],
  categories: Category[],
  gqlFn:      (query: string, vars: Record<string, unknown>) => Promise<unknown>,
  UPDATE_MANGA_CATEGORIES: string,
  UPDATE_MANGA?: string,
  mangaStatus?: string,
): Promise<void> {
  return store.checkAndMarkCompleted(mangaId, chaps, categories, gqlFn, UPDATE_MANGA_CATEGORIES, UPDATE_MANGA, mangaStatus);
}
