/**
 * Forward-compatible, versioned settings migration.
 *
 * Historically, `localStorage["magnotia_settings"]` (now migrated to
 * `lumotia_settings` via Phase 7 of the rebrand cascade) was a bare JSON object
 * (whatever `SettingsState` looked like at write time). That shape
 * tolerates new fields cleanly — spread over `defaults` — but does not
 * survive:
 *   - renaming a field, even with a trivial default mapping
 *   - a type change on an existing field
 *   - a settings reset the user explicitly did not ask for (Handy #602)
 *
 * Versioned migrations fix all three. Every schema change bumps the
 * version constant and lands a `(from, to)` step that takes the stored
 * raw blob (or its previous-version transform) and returns the
 * next-version shape. `loadSettingsWithMigration` walks the chain
 * until it reaches `CURRENT_SETTINGS_VERSION`.
 *
 * Covers docs/whisper-ecosystem/brief.md item #11.
 */

import { parseStoredJson } from "$lib/utils/storage";

export const CURRENT_SETTINGS_VERSION = 1;

/** Envelope written to localStorage. The raw blob is always kept
 *  separately so a downgrade (user jumps to an older Lumotia build) can
 *  still read v1 data even after we've bumped to v2. */
export interface VersionedSettings<T> {
  version: number;
  data: T;
}

export type MigrationStep = (prev: unknown) => unknown;

/**
 * The migration chain. Add an entry per schema change.
 *
 * Each step is indexed by its destination version — MIGRATIONS[2] is
 * the function that takes v1 data and returns v2 data. To add v2:
 *
 *   MIGRATIONS[2] = (prev) => ({ ...(prev as object), newField: 42 });
 *
 * Keep the steps pure — they run on load, every boot.
 */
export const MIGRATIONS: Record<number, MigrationStep> = {
  // v0 -> v1: fold a raw bare-object blob (pre-versioning) into the
  // versioned envelope. Spreading over the caller's `defaults` is the
  // caller's job; this step just preserves whatever keys were there.
  1: (prev) => (prev && typeof prev === "object" ? prev : {}),
};

function isVersionedEnvelope(raw: unknown): raw is VersionedSettings<unknown> {
  return !!raw
    && typeof raw === "object"
    && "version" in (raw as Record<string, unknown>)
    && "data" in (raw as Record<string, unknown>)
    && typeof (raw as Record<string, unknown>).version === "number";
}

/**
 * Read `localStorage[key]`, run the migration chain, and return the
 * data blob at the current schema version. Callers then spread it
 * over their `defaults` to produce a full `SettingsState`.
 *
 * Corrupt / unparseable input returns `null` — caller falls back to
 * defaults.
 */
export function loadSettingsWithMigration<T>(key: string): T | null {
  if (typeof localStorage === "undefined") return null;
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return null;
  }

  let version: number;
  let data: unknown;

  if (isVersionedEnvelope(parsed)) {
    version = parsed.version;
    data = parsed.data;
  } else {
    version = 0;
    data = parsed;
  }

  // Apply each migration step from (version + 1) up to the current
  // schema version. A missing step between two known versions is a
  // programming error — we warn and return null so the caller falls
  // back to defaults instead of silently skipping.
  for (let v = version + 1; v <= CURRENT_SETTINGS_VERSION; v++) {
    const step = MIGRATIONS[v];
    if (!step) {
      console.warn(
        `settingsMigrations: missing step to v${v} (loaded v${version}, target v${CURRENT_SETTINGS_VERSION})`,
      );
      return null;
    }
    data = step(data);
  }

  return data as T;
}

/**
 * Serialise the current settings object into a versioned envelope and
 * write it to localStorage. Idempotent — writing the same data twice
 * produces the same bytes.
 */
export function saveSettingsWithVersion<T>(key: string, data: T): void {
  if (typeof localStorage === "undefined") return;
  const envelope: VersionedSettings<T> = {
    version: CURRENT_SETTINGS_VERSION,
    data,
  };
  try {
    localStorage.setItem(key, JSON.stringify(envelope));
  } catch {
    // Quota exceeded / private-mode localStorage — drop the write
    // rather than throwing into the UI. Settings are regenerated from
    // memory on next save, or from defaults on next boot.
  }
}

/**
 * Back-compat shim: older call sites used `parseStoredJson` directly.
 * When a caller hasn't migrated yet, they can keep using this for
 * non-versioned blobs without disturbing the settings schema.
 */
export { parseStoredJson };
