/**
 * UserSettings — persisted user preferences (spec §07, storage shape §09C).
 *
 * Portable core: no browser APIs, no React, no Monaco. Defines the settings
 * record shape, the current schema version, the factory defaults, and the
 * read-time load/normalize function. The localStorage wiring that actually
 * reads and writes the JSON lives in infrastructure — this module only knows
 * the shape and how to coerce arbitrary parsed input back into a valid record.
 *
 * Settings load at startup; per spec §07 "Startup load", any missing or
 * unrecognized value silently falls back to its factory default, so older or
 * partial saved records never break the app. `loadSettings` is the gate that
 * guarantees that (the read-time migration, mirroring `migrateSnippet`).
 */

import { isChartThemeId, type ChartThemeId } from './vega-themes';

/** Current schema version for a UserSettings record (read-time migration target). */
export const CURRENT_SETTINGS_VERSION = 1;

export interface UserSettings {
  /** Record schema version, for read-time migration (spec §09C). */
  version: number;
  /** Spec-editor configuration (spec §07 → Editor). Applies to the editing surface. */
  editor: {
    /** Editor font size, 10–18 px integer (spec §07; default 12). */
    fontSize: number;
    /**
     * Editor color theme id. The `'auto'` sentinel follows the app UI theme
     * (light app → light editor, dark → dark); any other value is an explicit
     * override (spec §07; default `'auto'`).
     */
    theme: string;
    /** Whether the editor minimap (overview strip) is shown (spec §07; default false). */
    minimap: boolean;
    /** Soft-wrap long lines (spec §07; default `'on'`). */
    wordWrap: 'on' | 'off';
    /** Show the line-number gutter (spec §07; default `'on'`). */
    lineNumbers: 'on' | 'off';
    /** Indentation width in spaces, positive integer (spec §07; default 2). */
    tabSize: number;
  };
  /** Preview-performance tuning (spec §07 → Performance). */
  performance: {
    /**
     * Delay (ms) after the user stops typing before the preview re-renders,
     * 500–5000 (spec §07; default 1500). Lower feels snappier but re-renders
     * more often; higher is calmer but laggier.
     */
    renderDebounce: number;
  };
  /** App-chrome appearance (spec §07 → Appearance; §09C). */
  ui: {
    /** Overall UI theme; flips the whole app chrome (spec §07; default `'light'`). */
    theme: 'light' | 'dark';
    /**
     * Preview sizing/fit mode (set by the preview's own Fit control, not a
     * settings cluster, but persisted in this record per §09C; see _Live
     * Preview_). Default `'default'`.
     */
    previewFitMode: 'default' | 'width' | 'height' | 'full';
    /**
     * Chart theme — which config is injected when charts render (spec §04;
     * docs/chart-theming-scope.md §4.2). `'astrolabe'` (default) is the house
     * style following the UI theme; `'stock'` injects nothing; other ids are
     * vega-themes presets. Set by the preview's settings cluster.
     */
    chartTheme: ChartThemeId;
  };
  /** Date-rendering preferences (spec §07 → Formatting). */
  formatting: {
    /**
     * How dates render throughout the app (spec §07; default `'smart'`).
     * `'smart'` = relative/human-friendly; `'iso'` = full ISO 8601 timestamp;
     * `'custom'` = use `customDateFormat`.
     */
    dateFormat: 'smart' | 'iso' | 'custom';
    /**
     * Free-text format string, used only when `dateFormat = 'custom'`
     * (spec §07; default empty string).
     */
    customDateFormat: string;
  };
}

/**
 * Build a fresh UserSettings with every field at its factory default
 * (spec §07). Returns an independent deep copy each call so callers may freely
 * mutate the result without affecting the shared `DEFAULT_SETTINGS` constant or
 * each other.
 */
export function defaultSettings(): UserSettings {
  return {
    version: CURRENT_SETTINGS_VERSION,
    editor: {
      fontSize: 12,
      theme: 'auto',
      minimap: false,
      wordWrap: 'on',
      lineNumbers: 'on',
      tabSize: 2,
    },
    performance: {
      renderDebounce: 1500,
    },
    ui: {
      theme: 'light',
      previewFitMode: 'default',
      chartTheme: 'astrolabe',
    },
    formatting: {
      dateFormat: 'smart',
      customDateFormat: '',
    },
  };
}

/**
 * Deeply-frozen factory defaults, for read-only reference (e.g. comparing a
 * form against defaults). To get a mutable copy, call `defaultSettings()`.
 */
export const DEFAULT_SETTINGS: UserSettings = deepFreeze(defaultSettings());

/** Recursively freeze an object and its nested plain-object members. */
function deepFreeze<T>(value: T): T {
  if (value !== null && typeof value === 'object') {
    for (const member of Object.values(value)) deepFreeze(member);
    Object.freeze(value);
  }
  return value;
}

/** Round to an integer and clamp to `[min, max]`. */
function clampInt(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, Math.round(value)));
}

/** A finite number, or `null` for `NaN`/`Infinity`/non-numbers. */
function asNumber(value: unknown): number | null {
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
}

/** Return `value` if it is one of `allowed`, else `fallback`. */
function asEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
  return typeof value === 'string' && (allowed as readonly string[]).includes(value)
    ? (value as T)
    : fallback;
}

/** Safely read a nested settings group as a record (tolerates non-objects). */
function group(raw: Record<string, unknown>, key: string): Record<string, unknown> {
  const value = raw[key];
  return typeof value === 'object' && value !== null && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : {};
}

/**
 * Normalize an arbitrary parsed value into a fully-valid UserSettings — the
 * read-time migration (spec §07 "Startup load"; §09C). Accepts `null`, junk, a
 * partial object, or a record from an older version. Every missing or
 * wrong-typed field silently falls back to its default; numeric fields are
 * coerced and clamped to their ranges; enum fields are validated against their
 * allowed values; unknown extra keys are ignored. The output `version` is
 * always stamped to `CURRENT_SETTINGS_VERSION` (like `migrateSnippet`).
 *
 * Tolerates partial nesting: `{ editor: { fontSize: 14 } }` keeps 14 and fills
 * the rest of `editor` (and every other group) from defaults.
 */
export function loadSettings(raw: unknown): UserSettings {
  const d = defaultSettings();
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return d;

  const r = raw as Record<string, unknown>;
  const editor = group(r, 'editor');
  const performance = group(r, 'performance');
  const ui = group(r, 'ui');
  const formatting = group(r, 'formatting');

  const fontSize = asNumber(editor.fontSize);
  const tabSize = asNumber(editor.tabSize);
  const renderDebounce = asNumber(performance.renderDebounce);

  return {
    version: CURRENT_SETTINGS_VERSION,
    editor: {
      fontSize: fontSize === null ? d.editor.fontSize : clampInt(fontSize, 10, 18),
      theme: typeof editor.theme === 'string' ? editor.theme : d.editor.theme,
      minimap: typeof editor.minimap === 'boolean' ? editor.minimap : d.editor.minimap,
      wordWrap: asEnum(editor.wordWrap, ['on', 'off'] as const, d.editor.wordWrap),
      lineNumbers: asEnum(editor.lineNumbers, ['on', 'off'] as const, d.editor.lineNumbers),
      tabSize: tabSize === null ? d.editor.tabSize : clampInt(tabSize, 1, Number.MAX_SAFE_INTEGER),
    },
    performance: {
      renderDebounce:
        renderDebounce === null
          ? d.performance.renderDebounce
          : clampInt(renderDebounce, 500, 5000),
    },
    ui: {
      theme: asEnum(ui.theme, ['light', 'dark'] as const, d.ui.theme),
      previewFitMode: asEnum(
        ui.previewFitMode,
        ['default', 'width', 'height', 'full'] as const,
        d.ui.previewFitMode,
      ),
      chartTheme: isChartThemeId(ui.chartTheme) ? ui.chartTheme : d.ui.chartTheme,
    },
    formatting: {
      dateFormat: asEnum(
        formatting.dateFormat,
        ['smart', 'iso', 'custom'] as const,
        d.formatting.dateFormat,
      ),
      customDateFormat:
        typeof formatting.customDateFormat === 'string'
          ? formatting.customDateFormat
          : d.formatting.customDateFormat,
    },
  };
}
