/**
 * src/http/prefs-store.ts (v0.6 Projects) — persistent store of the Projects
 * launcher's cross-device UI preferences (`UiPrefs`).
 *
 * The app's whole point is multi-device access ("check from any device"), yet
 * favourites lived in per-device localStorage. This store persists them (and the
 * namespace group collapse-state) host-side so the view is identical on laptop
 * and phone. Served over GET /prefs, written via PUT /prefs (Origin-guarded).
 *
 * The file holds NO secrets (just project paths + collapse booleans), but it IS
 * attacker-writable via a same-origin PUT, so every field is bounded and coerced
 * on the way in (`sanitizePrefs`) — never trust the request body, never trust the
 * persisted JSON. Loading is best-effort: missing file → defaults; malformed →
 * defaults (logged). All mutations replace state immutably.
 */

import { readFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import type { UiPrefs } from '../types.js';

/* ── Bounds (DoS guard — a flood of PUTs cannot grow the file without limit) ── */
const MAX_FAVOURITES = 500; // sane cap on starred projects
const MAX_COLLAPSED = 1000; // sane cap on remembered group keys
const MAX_STRING_LEN = 4096; // per path / group-key length cap

/** Empty defaults — a fresh install (or any load failure) is a usable blank slate. */
export function emptyPrefs(): UiPrefs {
  return { favourites: [], collapsed: {} };
}

/**
 * Coerce untrusted input (request body OR persisted JSON) into a safe `UiPrefs`.
 * Drops anything malformed; enforces length/size caps. Never throws.
 */
export function sanitizePrefs(input: unknown): UiPrefs {
  if (input === null || typeof input !== 'object') return emptyPrefs();
  const o = input as Record<string, unknown>;

  const favSource = Array.isArray(o['favourites']) ? o['favourites'] : [];
  const favourites: string[] = [];
  const seen = new Set<string>();
  for (const v of favSource) {
    if (typeof v !== 'string' || v.length === 0 || v.length > MAX_STRING_LEN) continue;
    if (seen.has(v)) continue; // de-dup — a path is favourited once
    seen.add(v);
    favourites.push(v);
    if (favourites.length >= MAX_FAVOURITES) break;
  }

  const collapsed: Record<string, boolean> = {};
  const collSource = o['collapsed'];
  if (collSource !== null && typeof collSource === 'object' && !Array.isArray(collSource)) {
    let count = 0;
    for (const [k, v] of Object.entries(collSource as Record<string, unknown>)) {
      if (k.length === 0 || k.length > MAX_STRING_LEN) continue;
      if (v !== true) continue; // only persist the "collapsed" fact; expanded is the default
      collapsed[k] = true;
      if (++count >= MAX_COLLAPSED) break;
    }
  }

  return { favourites, collapsed };
}

export interface PrefsStore {
  /** Current preferences (a fresh object each call — callers never mutate state). */
  get(): UiPrefs;
  /** Replace preferences with a sanitized copy of `next`. */
  set(next: unknown): void;
  /** Persist to disk. Best-effort: logs and resolves on failure. */
  persist(): Promise<void>;
}

function loadPrefs(filePath: string): UiPrefs {
  let raw: string;
  try {
    raw = readFileSync(filePath, 'utf8');
  } catch (err) {
    if ((err as NodeJS.ErrnoException)?.code !== 'ENOENT') {
      console.error('prefs-store: load failed, starting empty', err);
    }
    return emptyPrefs();
  }
  try {
    return sanitizePrefs(JSON.parse(raw));
  } catch (err) {
    console.error('prefs-store: malformed JSON, starting empty', err);
    return emptyPrefs();
  }
}

/**
 * Load (or initialise empty) a prefs store backed by `filePath`.
 * @param filePath absolute path to the JSON store (cfg.prefsStorePath)
 */
export function loadPrefsStore(filePath: string): PrefsStore {
  let prefs: UiPrefs = loadPrefs(filePath);

  return {
    get(): UiPrefs {
      // Return a defensive copy so external callers can't mutate internal state.
      return { favourites: [...prefs.favourites], collapsed: { ...prefs.collapsed } };
    },

    set(next: unknown): void {
      prefs = sanitizePrefs(next);
    },

    async persist(): Promise<void> {
      try {
        await writeFile(filePath, JSON.stringify(prefs, null, 2));
      } catch (err) {
        console.error('prefs-store: persist failed', err);
      }
    },
  };
}
