// Core "working tuning" capability domain — the live, host-authoritative CURRENT
// instrument tuning (session state), distinct from the soft opt-in default and from
// any one song's tuning. This is the single source of truth the whole app reads:
// the highway, the library/song-picker, Virtuoso, and the minigames all consult it,
// and the tuner is the sole WRITER (it updates this when the player retunes, clears
// the gate, or switches instruments).
//
// PER-INSTRUMENT: a player has separate physical instruments, each in its OWN tuning
// ("I'm not tuning two instruments when I pick a song"). So state is a MAP keyed by
// instrument — `${instrument}-${stringCount}` (e.g. "guitar-6", "bass-4"), the same key
// the v3 instrument selector uses. `get()` returns the CURRENTLY-SELECTED instrument's
// tuning; switching the selector surfaces that instrument's own remembered tuning. You
// only ever deal with the one you've picked.
//
// Design: WORKING-TUNING-STATE-DESIGN.md (host-first PR series, PR 1 = this file).
// Pattern mirrors `capabilities/tuning.js` (capability registration) + the host theme
// read-API (`window.feedBack.theme`): a synchronous `get()` plus a `working-tuning-
// changed` event that also fires once on hydration.
//
// State is IN-MEMORY and NOT persisted — reset-to-home on restart is deliberate (a
// stale "you're in drop-A" assumption is worse than re-asking). The opt-in "default
// tuning on app open" lands later; for now we seed the selected instrument from
// /api/settings.
//
// PR 1 is PURE PLUMBING: it introduces the state + read/write surface + event, but
// nothing writes to it yet and no behavior changes. The tuner becomes the writer (and
// the gate's E->C# asymmetry is fixed) in a later PR.
(function () {
    'use strict';

    window.feedBack = window.feedBack || {};
    // Idempotent: a second injection of this module must not replace the live state
    // with a fresh (empty) one — once we're registered, re-running is a no-op.
    if (window.feedBack.workingTuning && window.feedBack.workingTuning.version === 1) return;
    const capabilities = window.feedBack.capabilities;

    const _byInstrument = {};   // key -> tuning state (the per-instrument map)
    let _currentKey = null;     // the selected instrument's key; cached so get() is sync
    let _hydrated = false;
    let _touched = false;       // set once anything explicitly writes/selects; gates the async seed

    function _normInstrument(instrument) {
        return instrument === 'bass' ? 'bass' : 'guitar';
    }
    function _keyOf(instrument, stringCount) {
        const inst = _normInstrument(instrument);
        const sc = Number(stringCount) || (inst === 'bass' ? 4 : 6);
        return inst + '-' + sc;
    }
    // Like _keyOf, but when the caller omits a string count we resolve it against the
    // current selection (if it's the same instrument) before falling back to the
    // per-instrument default — so `set({instrument:'bass'})` targets the selected
    // bass-5, not a hard-coded bass-4.
    function _keyOfResolved(instrument, stringCount) {
        const inst = _normInstrument(instrument);
        let sc = Number(stringCount);
        if (!sc) {
            if (_currentKey) {
                const cur = _splitKey(_currentKey);
                if (cur.instrument === inst) sc = cur.stringCount;
            }
            if (!sc) sc = (inst === 'bass' ? 4 : 6);
        }
        return inst + '-' + sc;
    }
    function _splitKey(key) {
        const parts = (typeof key === 'string' ? key : '').split('-');
        const inst = parts[0] === 'bass' ? 'bass' : 'guitar';
        return { instrument: inst, stringCount: Number(parts[1]) || (inst === 'bass' ? 4 : 6) };
    }

    // The shape every consumer reads. `offsets` are per-string semitone offsets from
    // standard (same vocabulary as song_info.tuning and /api/tunings); `instrument`
    // disambiguates the open-string base so offsets resolve to real pitches. A drop-A
    // 8-string is just an offsets array — fully custom tunings are first-class.
    // `provenance` is the honesty flag: 'verified' means the tuner did a choreographed
    // per-string mic check this session; everything else is 'assumed'.
    function _defaultState(key) {
        const id = _splitKey(key);
        return {
            offsets: null,
            stringCount: id.stringCount,
            instrument: id.instrument,
            referencePitch: 440,
            provenance: 'assumed',
            verifiedStrings: null,
            verifiedAt: null,
            source: 'default',
        };
    }

    // Resolve which instrument key a get/set targets: an explicit arg wins (a string
    // key "guitar-6", a bare "guitar"/"bass", or { instrument, stringCount }); else the
    // cached current selection.
    function _resolveKey(instrument) {
        if (instrument && typeof instrument === 'object') return _keyOfResolved(instrument.instrument, instrument.stringCount);
        if (typeof instrument === 'string' && instrument) {
            return instrument.indexOf('-') > 0 ? instrument : _keyOfResolved(instrument, null);
        }
        return _currentKey || _keyOf('guitar', 6);
    }

    // Synchronous read of an instrument's current tuning (default = selected
    // instrument). Returns a deep-enough copy — the object plus its mutable array
    // fields (`offsets`, `verifiedStrings`) — so a reader can't mutate the live state.
    function get(instrument) {
        const key = _resolveKey(instrument);
        const state = Object.assign(_defaultState(key), _byInstrument[key] || {});
        if (Array.isArray(state.offsets)) state.offsets = state.offsets.slice();
        if (Array.isArray(state.verifiedStrings)) state.verifiedStrings = state.verifiedStrings.slice();
        return state;
    }

    function _emitChanged(key) {
        if (window.feedBack && typeof window.feedBack.emit === 'function') {
            window.feedBack.emit('working-tuning-changed', { key: key, instrument: _splitKey(key).instrument, tuning: get(key) });
        }
    }

    // The single mutator. The tuner calls this on retune / gate-clear / swap. Writes to
    // the instrument the state targets (opts.instrument, or next.instrument+stringCount,
    // or the current selection) and makes that the active instrument. `opts.provenance`
    // stamps 'verified' (mic-confirmed) vs the default 'assumed'. Changing the tuning
    // invalidates a prior verification unless fresh verifiedStrings are supplied — fail
    // toward "assumed".
    function set(next, opts) {
        opts = opts || {};
        next = next || {};
        // Resolve the target key. An explicit opts.instrument wins; otherwise a
        // next.instrument/next.stringCount targets that slot — but a bare stringCount
        // (no instrument) applies to the CURRENTLY-SELECTED instrument, not a hard-coded
        // guitar, so `set({stringCount:5})` on a selected bass writes bass-5.
        let key;
        if (opts.instrument) {
            key = _resolveKey(opts.instrument);
        } else if (next.instrument || next.stringCount) {
            const inst = next.instrument ? _normInstrument(next.instrument)
                : (_currentKey ? _splitKey(_currentKey).instrument : 'guitar');
            key = _keyOfResolved(inst, next.stringCount);
        } else {
            key = _currentKey || _resolveKey();
        }
        const id = _splitKey(key);
        const merged = Object.assign(get(key), next);   // get() gives copies, so `merged` is ours to mutate
        merged.instrument = id.instrument;              // keep coherent with the key
        merged.stringCount = id.stringCount;            // the key is authoritative for string count
        const tuningChanged = ('offsets' in next) || ('stringCount' in next) || ('referencePitch' in next);

        // Provenance: explicit opts wins; a bare tuning change downgrades to 'assumed'.
        if (opts.provenance) {
            merged.provenance = opts.provenance;
        } else if (tuningChanged) {
            merged.provenance = 'assumed';
        }

        // Verification metadata is coherent by construction: a tuning change invalidates
        // prior per-string verification unless the caller supplies a fresh bundle, and the
        // metadata exists ONLY while provenance === 'verified'. So verified <=> we hold
        // verifiedStrings — a "verified with no strings" state is impossible.
        if (!('verifiedStrings' in next) && tuningChanged) {
            merged.verifiedStrings = null;
        }
        if (merged.provenance === 'verified' && !Array.isArray(merged.verifiedStrings)) {
            merged.provenance = 'assumed';              // claimed verified but no evidence — fail toward assumed
        }
        if (merged.provenance === 'verified') {
            // verified always carries a real timestamp — a caller-supplied null/NaN/absent
            // verifiedAt is stamped now, so 'verified' can never mean "at no known time".
            if (typeof merged.verifiedAt !== 'number' || !isFinite(merged.verifiedAt)) {
                merged.verifiedAt = Date.now();
            }
        } else {
            merged.verifiedStrings = null;
            merged.verifiedAt = null;
        }

        // Store copies of the mutable arrays so a caller can't mutate live state post-set.
        if (Array.isArray(merged.offsets)) merged.offsets = merged.offsets.slice();
        if (Array.isArray(merged.verifiedStrings)) merged.verifiedStrings = merged.verifiedStrings.slice();
        _byInstrument[key] = merged;
        _currentKey = key;        // writing a tuning makes that instrument the active one
        _touched = true;          // an explicit write must not be clobbered by the async seed
        _emitChanged(key);
        return get(key);
    }

    // Tell the host which instrument is now selected (the v3 selector calls this when
    // the player switches guitar<->bass / string count) so get() returns the right
    // instrument's tuning. Emits if the selection actually changed.
    function setCurrentInstrument(instrument, stringCount) {
        const key = (typeof instrument === 'string' && instrument.indexOf('-') > 0) ? instrument : _keyOfResolved(instrument, stringCount);
        _touched = true;          // an explicit selection must not be reverted by the async seed
        if (key === _currentKey) return get(key);
        _currentKey = key;
        _emitChanged(key);
        return get(key);
    }

    // Reset an instrument's live tuning back to its baseline (the home/default).
    function resetToDefault(instrument) {
        const key = _resolveKey(instrument);
        _byInstrument[key] = _defaultState(key);
        _touched = true;
        _emitChanged(key);
        return get(key);
    }

    // Per-string semitone offsets of a named tuning relative to Standard, derived from
    // the /api/tunings frequency tables. The reference pitch cancels in the ratio, so
    // this is pitch-independent. Returns null if either row is missing/mismatched.
    function _offsetsFromFreqs(named, standard) {
        if (!Array.isArray(named) || !Array.isArray(standard) || named.length !== standard.length) return null;
        const out = [];
        for (let i = 0; i < named.length; i++) {
            const a = Number(named[i]);
            const b = Number(standard[i]);
            if (!(a > 0) || !(b > 0)) return null;
            out.push(Math.round(12 * Math.log2(a / b)));
        }
        return out;
    }

    // ---- Opt-in "launch tuning" default (soft, per-instrument) -------------------
    // A convenience the player opts into: "start me in THIS tuning on app open." Off
    // by default (nothing stored) → boot seeds from /api/settings as before. It is
    // only a SEED — the live working tuning still resets on restart.
    const LAUNCH_KEY = 'v3-working-tuning-launch-default';
    function _readLaunchMap() {
        try { return JSON.parse(localStorage.getItem(LAUNCH_KEY) || '{}') || {}; }
        catch (_) { return {}; }
    }
    function _writeLaunchMap(map) {
        try {
            if (map && Object.keys(map).length) localStorage.setItem(LAUNCH_KEY, JSON.stringify(map));
            else localStorage.removeItem(LAUNCH_KEY);
        } catch (_) { /* private mode */ }
    }
    function getLaunchDefault(instrument) {
        const key = _resolveKey(instrument);
        const d = _readLaunchMap()[key];
        return d ? Object.assign(_defaultState(key), d, { source: 'launch-default' }) : null;
    }
    // Remember an instrument's CURRENT working tuning (or a supplied state) as its
    // launch default. Opt-in — nothing calls this unless the player asks.
    function setLaunchDefault(instrument, state) {
        const key = _resolveKey(instrument);
        const src = state || get(key);
        const map = _readLaunchMap();
        map[key] = {
            offsets: Array.isArray(src.offsets) ? src.offsets.slice() : null,
            stringCount: src.stringCount,
            instrument: _splitKey(key).instrument,
            referencePitch: src.referencePitch || 440,
        };
        _writeLaunchMap(map);
        return getLaunchDefault(key);
    }
    function clearLaunchDefault(instrument) {
        const key = _resolveKey(instrument);
        const map = _readLaunchMap();
        if (key in map) { delete map[key]; _writeLaunchMap(map); }
    }

    // Seed the SELECTED instrument's slot on boot (best-effort 'assumed' starting
    // point, NOT a persisted working tuning): the player's opt-in launch default if one
    // is set for this instrument, else /api/settings — where a NAMED tuning ("Drop D") is
    // resolved to offsets via /api/tunings so it isn't lost. If neither can be read we
    // still hydrate so consumers aren't stuck waiting; an explicit set()/select before we
    // resolve wins (no clobber).
    function _seedFromSettings() {
        fetch('/api/settings')
            .then(function (r) { return r && r.ok ? r.json() : null; })
            .then(function (s) {
                if (!s || _touched) return;   // nothing to seed, or a consumer already wrote — don't clobber
                const inst = _normInstrument(s.instrument);
                const sc = Number(s.string_count) || (inst === 'bass' ? 4 : 6);
                const key = _keyOf(inst, sc);

                function commit(offsets) {
                    if (_touched) return;     // re-check: a write may have raced the /api/tunings fetch
                    _currentKey = key;
                    // Opt-in launch default wins over the raw profile; otherwise use the
                    // resolved `offsets` (a named settings tuning was already turned into
                    // offsets via /api/tunings before commit()).
                    const launch = _readLaunchMap()[key];
                    _byInstrument[key] = launch
                        ? {
                            offsets: Array.isArray(launch.offsets) ? launch.offsets.slice(0, sc) : null,
                            stringCount: sc, instrument: inst,
                            referencePitch: Number(launch.referencePitch) || 440,
                            provenance: 'assumed', verifiedStrings: null, verifiedAt: null,
                            source: 'launch-default',
                        }
                        : {
                            offsets: Array.isArray(offsets) ? offsets.slice(0, sc) : null,
                            stringCount: sc, instrument: inst,
                            referencePitch: Number(s.reference_pitch) || 440,
                            provenance: 'assumed', verifiedStrings: null, verifiedAt: null,
                            source: 'settings',
                        };
                }

                if (Array.isArray(s.tuning)) { commit(s.tuning); return; }
                if (typeof s.tuning === 'string' && s.tuning) {
                    return fetch('/api/tunings')
                        .then(function (r) { return r && r.ok ? r.json() : null; })
                        .then(function (t) {
                            const byName = t && ((t.tunings && t.tunings[key]) || t[key]);
                            commit(byName ? _offsetsFromFreqs(byName[s.tuning], byName.Standard) : null);
                        })
                        .catch(function () { commit(null); });
                }
                commit(null);
            })
            .catch(function () { /* keep defaults */ })
            .then(function () { _hydrate(); });
    }

    function _hydrate() {
        if (_hydrated) return;
        _hydrated = true;
        _emitChanged(_currentKey || _resolveKey());
    }

    // A per-string mic verification is only trustworthy for the context it was done
    // in — a new song means the player may have retuned, so a stale 'verified' must
    // never suppress a needed prompt (fail toward re-checking). Decay the CURRENT
    // instrument's verification back to 'assumed' on each song load; offsets are kept.
    function _decayVerifiedOnSongLoad() {
        const key = _currentKey || _resolveKey();
        const st = _byInstrument[key];
        if (st && st.provenance === 'verified') {
            st.provenance = 'assumed';
            st.verifiedStrings = null;
            st.verifiedAt = null;
            _emitChanged(key);
        }
    }
    if (typeof window.feedBack.on === 'function') {
        window.feedBack.on('song:loading', _decayVerifiedOnSongLoad);
    }

    // ---- Capability registration (mirrors capabilities/tuning.js) ----------------
    if (capabilities && capabilities.version === 1 &&
        !(window.feedBack.workingTuning && window.feedBack.workingTuning.version === 1)) {
        capabilities.registerOwner('working-tuning', {
            description: 'The live, host-authoritative current instrument tuning (session state), per ' +
                'instrument: offsets + string-count + reference pitch + assumed/verified provenance. ' +
                'Written by the tuner, read by the highway/library/Virtuoso/minigames.',
            operations: ['get-working-tuning', 'set-working-tuning'],
            events: ['working-tuning-changed'],
            kind: 'command',
            ownership: 'exclusive-owner',
        });
        capabilities.registerParticipant('plugin.tuner', {
            'working-tuning': {
                roles: ['contributor', 'requester'],
                operations: ['get-working-tuning', 'set-working-tuning'],
                emits: ['working-tuning-changed'],
                mode: 'active',
                compatibility: 'none',
                safety: 'safe',
            },
        });
        capabilities.registerParticipant('core.settings.instruments', {
            'working-tuning': {
                roles: ['requester'],
                operations: ['get-working-tuning'],
                events: ['working-tuning-changed'],
                mode: 'active',
                compatibility: 'none',
                safety: 'safe',
            },
        });
    }

    // ---- Public read/write surface (attached defensively, like feedBack.theme) ----
    window.feedBack.workingTuning = Object.freeze({
        version: 1,
        get: get,
        set: set,
        setCurrentInstrument: setCurrentInstrument,
        resetToDefault: resetToDefault,
        getLaunchDefault: getLaunchDefault,
        setLaunchDefault: setLaunchDefault,
        clearLaunchDefault: clearLaunchDefault,
    });

    _seedFromSettings();
})();
