/**
 * Settings persistence (localStorage) — docs/architecture/02 §5, spec §07/§09C.
 *
 * The home for the *UserSettings* record (spec §09C) under `astrolabe:settings`.
 * M1.5/M2 pulled the **theme** and **preview fit mode** slices forward (their
 * controls shipped first); M5 adds the full record — editor, performance, and
 * formatting groups — on the same key.
 *
 * Everything reads with **load-with-fallback** (the pure `loadSettings` in
 * `@core/settings` normalizes whatever is stored back onto a valid record) and
 * writes with a **field-level write-through merge**, so the narrow per-slice
 * writers (theme, fit mode) and the managed-settings write never clobber each
 * other: each merges into the stored record and leaves the other groups intact.
 *
 * Per the architecture rule, this is one of the only modules that may touch
 * `localStorage`; everything else goes through these typed functions.
 */

import type { FitMode } from '@core/rendering';
import { loadSettings, type UserSettings } from '@core/settings';
import type { UiTheme } from '@core/theme';
import { isChartThemeSelection, type ChartThemeSelection } from '@core/vega-themes';

const KEY = 'astrolabe:settings';

/** Spec §07 Appearance default. */
const DEFAULT_THEME: UiTheme = 'light';

/** Spec §04 — the Fit control defaults to Original. */
const DEFAULT_FIT_MODE: FitMode = 'default';

/** Spec §04 — the Chart theme picker defaults to the house style. */
const DEFAULT_CHART_THEME: ChartThemeSelection = 'astrolabe';

/** Spec §04 — the data inspector is a debugging aid, collapsed until asked for. */
const DEFAULT_DATA_INSPECTOR_OPEN = false;

/** The managed slice the per-pane settings clusters own (theme + fit live in their own slices). */
export type ManagedSettings = Pick<UserSettings, 'editor' | 'performance' | 'formatting'>;

/** Loose view of the stored record for the per-slice write-through merges. */
interface StoredSettings {
  ui?: {
    theme?: unknown;
    previewFitMode?: unknown;
    chartTheme?: unknown;
    dataInspectorOpen?: unknown;
    dataInspectorHeight?: unknown;
    [k: string]: unknown;
  };
  [k: string]: unknown;
}

/** localStorage can be absent or throw (private mode, SSR, blocked storage). */
function available(): boolean {
  try {
    return typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function';
  } catch {
    return false;
  }
}

function readRaw(): StoredSettings {
  if (!available()) return {};
  try {
    const raw = localStorage.getItem(KEY);
    if (!raw) return {};
    const parsed: unknown = JSON.parse(raw);
    return parsed && typeof parsed === 'object' ? (parsed as StoredSettings) : {};
  } catch (err) {
    console.warn('[settings] failed to read, using defaults', err);
    return {};
  }
}

function writeRaw(next: StoredSettings): void {
  if (!available()) return;
  try {
    localStorage.setItem(KEY, JSON.stringify(next));
  } catch (err) {
    console.warn('[settings] failed to write', err);
  }
}

function isUiTheme(v: unknown): v is UiTheme {
  return v === 'light' || v === 'dark';
}

function isFitMode(v: unknown): v is FitMode {
  return v === 'default' || v === 'width' || v === 'height' || v === 'full';
}

/**
 * The full, normalized UserSettings record — the load-with-fallback gate
 * (spec §07 "Startup load"). Used to hydrate the Settings store at startup.
 */
export function loadUserSettings(): UserSettings {
  return loadSettings(readRaw());
}

/**
 * Persist the managed groups (editor / performance / formatting),
 * merging into the stored record so the `ui` slice (theme, fit mode) is untouched.
 */
export function saveManagedSettings(managed: ManagedSettings): void {
  const current = readRaw();
  writeRaw({
    ...current,
    editor: managed.editor,
    performance: managed.performance,
    formatting: managed.formatting,
  });
}

/** The persisted UI theme, or the default — unknown/legacy values fall back. */
export function loadUiTheme(): UiTheme {
  const stored = readRaw().ui?.theme;
  // An unrecognized value (a future theme, or the retired 'experimental') falls
  // back rather than breaking — the load-with-fallback contract (doc §5).
  return isUiTheme(stored) ? stored : DEFAULT_THEME;
}

/** Persist the UI theme, preserving every other key already in the record. */
export function saveUiTheme(theme: UiTheme): void {
  const current = readRaw();
  writeRaw({ ...current, ui: { ...current.ui, theme } });
}

/** The persisted preview fit mode, or the default — unknown values fall back. */
export function loadPreviewFitMode(): FitMode {
  const stored = readRaw().ui?.previewFitMode;
  return isFitMode(stored) ? stored : DEFAULT_FIT_MODE;
}

/**
 * The persisted chart theme, or the default — unknown ids fall back. A
 * `custom:<id>` passes on shape; whether the record still exists is resolved at
 * render time (missing custom themes render as the house style).
 */
export function loadChartTheme(): ChartThemeSelection {
  const stored = readRaw().ui?.chartTheme;
  return isChartThemeSelection(stored) ? stored : DEFAULT_CHART_THEME;
}

/** Persist the chart theme, preserving every other key already in the record. */
export function saveChartTheme(chartTheme: ChartThemeSelection): void {
  const current = readRaw();
  writeRaw({ ...current, ui: { ...current.ui, chartTheme } });
}

/** Persist the preview fit mode, preserving every other key already in the record. */
export function savePreviewFitMode(fitMode: FitMode): void {
  const current = readRaw();
  writeRaw({ ...current, ui: { ...current.ui, previewFitMode: fitMode } });
}

/** Whether the live-preview data inspector is expanded, or the default (collapsed). */
export function loadDataInspectorOpen(): boolean {
  const stored = readRaw().ui?.dataInspectorOpen;
  return typeof stored === 'boolean' ? stored : DEFAULT_DATA_INSPECTOR_OPEN;
}

/** Persist the data inspector's open state, preserving every other key in the record. */
export function saveDataInspectorOpen(open: boolean): void {
  const current = readRaw();
  writeRaw({ ...current, ui: { ...current.ui, dataInspectorOpen: open } });
}

/**
 * The persisted data-inspector height (px), or null when absent/invalid. Null —
 * not a default — so the caller keeps the store's own default; the px default
 * lives only in `AppStore` (DATA_INSPECTOR_DEFAULT_HEIGHT). Re-clamped to the live
 * layout on drag, so a stale stored value can't wedge the pane.
 */
export function loadDataInspectorHeight(): number | null {
  const stored = readRaw().ui?.dataInspectorHeight;
  return typeof stored === 'number' && Number.isFinite(stored) && stored > 0 ? stored : null;
}

/** Persist the data inspector's height, preserving every other key in the record. */
export function saveDataInspectorHeight(height: number): void {
  const current = readRaw();
  writeRaw({ ...current, ui: { ...current.ui, dataInspectorHeight: height } });
}
