/**
 * editor_storm.js — THE storm + second selection for the editor page.
 * [Lane C owns the file, SPRINT15 gate 2.2 — one storm picker, CONVERGED with
 * Lane B's THREADS proposal (2026-07-20): B's filename, B's global name
 * (`EDITOR_STORM`), B's `source` argument and B's default storm are all taken;
 * the API keeps my `storm/time` symmetric shape because the gate asks for a
 * shared storm+TIME selection — B's panel simply ignores the half it doesn't
 * use. Countered-and-matched the same way we converged the gate-2.3 pin.]
 *
 * Sprint 14 shipped two storm pickers on one page: the WIND panel defaulted the
 * southerly (mine), SCORE IT defaulted the wildnight (B's), and D scored the
 * wildnight WHILE LOOKING AT the southerly — two harnesses disagreeing inside a
 * single page, the exact bug class this repo keeps paying for (three harnesses
 * measured a funnel-off yard in Sprint 11 for the same species of reason).
 *
 * The fix is ownership, not UI: this module is the ONE owner of "which storm,
 * which second" on the editor page. Panels are views of it — any number of
 * selects and scrubbers may DISPLAY the selection, but they all write here and
 * they all re-read from here, so there is nothing for two panels to disagree
 * about. Same shape as A's editor owning the site and every panel calling
 * `markDirty()`: one place remembers, nobody else has to.
 *
 * ── The contract ────────────────────────────────────────────────────────────
 *   · `stormSel()` returns the page singleton (import identity — both panels
 *     import this same specifier, so they cannot get different stores).
 *   · `setStorm(key)` REFUSES garbage (throws on a key not in STORM_KEYS) —
 *     a selection that can silently hold a storm that doesn't exist is the
 *     `_v1`-suffix 404 one layer up.
 *   · No-op writes are SILENT: setting the value it already holds fires no
 *     listener and returns false. This is load-bearing, not politeness — it is
 *     what lets a subscriber clamp the selection from inside its own change
 *     handler (wind panel: `time > duration` after a storm switch) without
 *     ringing forever.
 *   · `time` is seconds into the storm, ≥ 0, finite. The store deliberately
 *     does NOT know storm durations (it would have to fetch to learn them, and
 *     a synchronous store is the point) — the wind panel, which has the loaded
 *     def, clamps over-long times back into the storm via the same setTime.
 *   · Listener errors are caught and logged, never propagated — one broken
 *     panel must not sever the selection for the others (A's emit() rule).
 *
 * DEFAULT: the southerly at t=40 — B's converged pick, and the reasons are
 * authoring reasons, not courtesy: the wind overlay is the surface that is
 * LIVE the moment the page opens and needs a storm that shows something (a
 * funnel authored under the gentle storm looks like it does nothing); SCORE IT
 * sits behind an explicit click; and site_03 — the yard most likely to be
 * iterated next — was authored and played on the southerly. t=40 is the second
 * my S14 receipt demonstrated: align 100%, peak gain ×1.46 on site_02. (My
 * first cut defaulted the gate-2.3 pin, wildnight@60 — retired when B argued
 * the authoring case; the pin stays pinned in c.test.js either way, it never
 * needed to be the default.) c.test.js asserts THIS pair so neither lane's
 * refactor can silently regrow a private default.
 */

/** THE storm list, one copy. editor.wind.js re-exports it; keep in step with
 *  data/storms/ (c.test.js pins this list against its own, so drift is red). */
export const STORM_KEYS = [
  'storm_01_gentle',
  'storm_02_wildnight',
  'storm_02b_icenight',
  'storm_03_southerly',
  'storm_03b_earlybuster',
];

export const DEFAULT_SEL = { storm: 'storm_03_southerly', time: 40 };

/**
 * A fresh store — exported for c.test.js, which must be able to make throwaway
 * instances without touching the page singleton.
 */
export function createStormSel({ storm = DEFAULT_SEL.storm, time = DEFAULT_SEL.time } = {}) {
  if (!STORM_KEYS.includes(storm)) throw new Error(`[stormsel] unknown storm '${storm}'`);
  const listeners = new Set();
  const state = { storm, time };

  const emit = (source) => {
    // snapshot payload + snapshot listener set: a listener that unsubscribes
    // (or subscribes) mid-emit must not skew delivery for the rest. `source`
    // is B's ask: a debug string saying WHO wrote ('wind-panel', 'score',
    // 'console'…), carried in the payload, never used for logic.
    const payload = { storm: state.storm, time: state.time, source: source ?? null };
    for (const fn of [...listeners]) {
      try { fn(payload); } catch (err) { console.error('[stormsel] listener threw:', err); }
    }
  };

  return {
    get storm() { return state.storm; },
    get time() { return state.time; },

    /** @returns {boolean} true if the selection changed (false = no-op, silent) */
    setStorm(key, source) {
      if (!STORM_KEYS.includes(key)) {
        throw new Error(`[stormsel] unknown storm '${key}' — keys: ${STORM_KEYS.join(', ')}`);
      }
      if (key === state.storm) return false;
      state.storm = key;
      emit(source);
      return true;
    },

    /** @returns {boolean} true if the selection changed (false = no-op, silent) */
    setTime(t, source) {
      const v = Number(t);
      if (!Number.isFinite(v) || v < 0) {
        throw new Error(`[stormsel] time must be a finite second ≥ 0, got ${t}`);
      }
      if (v === state.time) return false;
      state.time = v;
      emit(source);
      return true;
    },

    /** @returns {() => void} unsubscribe */
    onChange(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
  };
}

// --- the page singleton ------------------------------------------------------
// Module-import identity IS the sharing mechanism: editor.wind.js and
// editor_score.js import this same file, so stormSel() hands both the same
// object and there is no registration step to forget. Exposed on globalThis
// as EDITOR_STORM (B's name) for consoles and spelunking.
let _sel = null;
export function stormSel() {
  if (!_sel) {
    _sel = createStormSel();
    if (typeof globalThis !== 'undefined') globalThis.EDITOR_STORM = _sel;
  }
  return _sel;
}
