import { useCallback, useState } from "react";

// Central registry of every localStorage key the app uses. One place to see them all, rename
// safely, and avoid collisions — instead of `"siftlode.x"` string literals scattered across
// files. Parametric (per-user) keys are functions. All keys are namespaced `siftlode.*`.
export const LS = {
  theme: "siftlode.theme",
  sidebarLayout: "siftlode.sidebarLayout",
  hints: "siftlode.hints",
  lang: "siftlode.lang",
  filters: "siftlode.filters",
  // Mirror of the default saved view's filters, so it can be applied synchronously on load
  // (before the async saved-views query resolves). Kept in sync by SavedViewsWidget.
  defaultViewFilters: "siftlode.defaultViewFilters",
  page: "siftlode.page",
  perfMode: "siftlode.perfMode",
  channelFilter: "siftlode.channelFilter",
  channelsView: "siftlode.channelsView",
  channelsTable: "siftlode.channelsTable",
  channelDiscoveryTable: "siftlode.channelDiscoveryTable",
  settingsTab: "siftlode.settingsTab",
  configTab: "siftlode.configTab",
  auditTable: "siftlode.auditTable",
  statsTab: "siftlode.statsTab",
  adminUsersTab: "siftlode.adminUsersTab",
  navCollapsed: "siftlode.navCollapsed",
  filterCollapsed: "siftlode.filterCollapsed",
  playlist: "siftlode.playlist",
  plSort: "siftlode.plSort",
  plexLibrary: "siftlode.plexLibrary",
  plexShow: "siftlode.plexShow",
  plexSort: "siftlode.plexSort",
  plexFilters: "siftlode.plexFilters",
  plexQ: "siftlode.plexQ",
  plexPlaylistLayout: "siftlode.plexPlaylistLayout",
  plexPlayerPrefs: "siftlode.plexPlayerPrefs",
  notifHistory: "siftlode.notifications",
  notifSettings: "siftlode.notifSettings",
  onboardingDismissed: "siftlode.onboarding.dismissed",
  seenVersion: "siftlode.seenVersion",
  chatDock: (userId: number) => `siftlode.chatDock.${userId}`,
} as const;

// --- per-account scoping -----------------------------------------------------------------
// The account a browser TAB is acting as (per-tab, in sessionStorage — see api.ts). Duplicated
// here (rather than imported from api.ts) so storage.ts stays dependency-free and usable by the
// low-level helpers below.
export const ACTIVE_ACCOUNT_KEY = "siftlode.activeAccount";

export function activeAccountId(): number | null {
  try {
    const raw = sessionStorage.getItem(ACTIVE_ACCOUNT_KEY);
    if (!raw) return null;
    const n = Number(raw);
    return Number.isFinite(n) ? n : null;
  } catch {
    return null;
  }
}

/** Suffix a base localStorage key with an account id so per-account UI state (filters, selected
 * playlist, notifications, tab positions, cached prefs…) is NOT shared across the accounts signed
 * into one browser — a bare shared key leaks one account's state into another (and, with per-tab
 * accounts, two tabs would fight over it). Defaults to the current tab's active account; returns
 * null when no account is known yet, so callers fall back to defaults / skip persistence instead
 * of touching another account's data. */
export function accountKey(base: string, id: number | null | undefined = activeAccountId()): string | null {
  return id != null ? `${base}.${id}` : null;
}

/** Per-account variants of the read/write helpers: key by the current account, no-op / return the
 * fallback when the account isn't known (avoids reading or writing a shared/other-account key). */
export function readAccount<T>(base: string, fallback: T): T {
  const k = accountKey(base);
  return k ? readJSON(k, fallback) : fallback;
}
export function readAccountMerged<T extends object>(base: string, defaults: T): T {
  const k = accountKey(base);
  return k ? readMerged(k, defaults) : { ...defaults };
}
export function writeAccount(base: string, value: unknown): void {
  const k = accountKey(base);
  if (k) writeJSON(k, value);
}
export function getAccountRaw(base: string): string | null {
  const k = accountKey(base);
  return k ? localStorage.getItem(k) : null;
}
export function setAccountRaw(base: string, value: string): void {
  const k = accountKey(base);
  if (k) {
    try {
      localStorage.setItem(k, value);
    } catch {
      /* ignore */
    }
  }
}

// --- typed read/write helpers (do the JSON + try/catch once) ----------------------------

/** Read an object-shaped value merged over `defaults` (so missing/added keys are tolerated),
 * falling back to `defaults` on missing or corrupt data. */
export function readMerged<T extends object>(key: string, defaults: T): T {
  try {
    return { ...defaults, ...JSON.parse(localStorage.getItem(key) || "{}") };
  } catch {
    return { ...defaults };
  }
}

/** Read any JSON value (arrays/scalars), falling back on missing or corrupt data. */
export function readJSON<T>(key: string, fallback: T): T {
  try {
    const raw = localStorage.getItem(key);
    return raw == null ? fallback : (JSON.parse(raw) as T);
  } catch {
    return fallback;
  }
}

/** Persist a value as JSON, swallowing quota/serialization errors. */
export function writeJSON(key: string, value: unknown): void {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch {
    /* ignore quota / serialization errors */
  }
}

// --- reactive persisted string state ----------------------------------------------------

/** A `useState` whose string value is mirrored to localStorage, scoped to the current account (see
 * accountKey), so one account's persisted tab/view position doesn't carry over to another in the
 * same browser. Survives reload (F5); validation is deferred to the caller (clamp at render) so it
 * can be used before an async-loaded option list is known, keeping hook order stable. These hooks
 * run in components that only mount once signed in, so the account is known. */
export function useAccountPersistedState(
  base: string,
  fallback = "",
): [string, (value: string) => void] {
  const [value, setValue] = useState<string>(() => getAccountRaw(base) ?? fallback);
  const set = (next: string) => {
    setValue(next);
    setAccountRaw(base, next);
  };
  return [value, set];
}

/** A per-account persisted JSON OBJECT: `patch(partial)` merges + saves, unknown/missing keys fall
 * back to `defaults` (via readAccountMerged) so adding a field later is safe. Used for bundles of
 * related per-account settings (e.g. the Plex player prefs: volume, offsets, seek steps, sub style). */
export function useAccountPersistedObject<T extends object>(
  base: string,
  defaults: T,
): [T, (patch: Partial<T>) => void] {
  const [value, setValue] = useState<T>(() => readAccountMerged(base, defaults));
  const patch = useCallback(
    (p: Partial<T>) => {
      setValue((prev) => {
        const next = { ...prev, ...p };
        writeAccount(base, next);
        return next;
      });
    },
    [base],
  );
  return [value, patch];
}
