// The background's access to the persisted profile.
//
// There is no in-memory copy here, and that is the whole design. The MV3
// service worker is terminated when idle and revived by the next message, so
// anything held at module scope is either absent or arbitrarily stale, and
// src/shared/state.js's module-level `state` singleton — which nothing in the
// worker ever populates — silently served DEFAULT_STATE to whoever read it.
// Five defects came out of that (https://git.eeqj.de/sneak/AutistMask/issues/324),
// and every point fix for one of them added a loadState() that created the
// next: loading detaches the objects an in-flight handler is holding.
//
// So the background reads per call and writes read-modify-write:
//
//   getState()        one storage read, normalized, detached. Nothing else
//                     holds the object it returns, so a handler may keep it
//                     across any number of awaits and no concurrent work can
//                     move it.
//   updateState(fn)   read fresh, apply fn to that fresh record, write it
//                     back — all inside a queue, so two background writes
//                     never interleave, and the read is one storage round trip
//                     ahead of the write rather than a page lifetime ahead of
//                     it (which is what made the popup's saveState() need a
//                     per-field merge against a baseline at all).
//
// A handler that must both read and write therefore does its network work
// against a snapshot it owns, and applies the RESULT inside updateState().
// It never publishes an object other in-flight work is holding.

const { storageGet, storageSet } = require("../shared/browserApi");
const { normalizePersisted } = require("../shared/persistedState");
const {
    STATE_SCHEMA_VERSION,
    assertStateUsable,
} = require("../shared/stateSchema");

// A fresh, fully-normalized, detached copy of the persisted profile.
//
// Normalized rather than raw: a legacy or malformed record is self-healed the
// same way loadState() heals it for the popup, so the background is never the
// one context reasoning about a shape the rest of the extension repairs.
//
// Throws StateUnusableError for a record this build cannot make sense of,
// before normalization gets a chance to paper over it — the same gate, in the
// same place, as the popup's loadState(). Every handler that consults the
// profile comes through here, so a dApp call against such a record is answered
// with the specific error the dispatcher maps that to (src/background/index.js)
// rather than dereferencing its way into a generic -32603.
async function getState() {
    const result = await storageGet("autistmask");
    assertStateUsable(result.autistmask);
    return normalizePersisted(result.autistmask);
}

// Serializes the read-modify-write turns below. Two of them interleaved would
// each read before the other wrote, and the second write would carry the first
// one's fields back to their pre-turn values.
let updateQueue = Promise.resolve();

async function updateStateOnce(mutate) {
    const s = await getState();
    await mutate(s);
    s.hasWallet = Boolean(s.wallets && s.wallets.length > 0);
    // Stamped on every write, exactly as the popup's saveState() stamps it:
    // whichever context writes last, the record in storage is in this build's
    // shape and says so.
    s.schemaVersion = STATE_SCHEMA_VERSION;
    await storageSet({ autistmask: s });
    return s;
}

// Apply `mutate` to a record read fresh from storage and write the result
// back. `mutate` receives a detached, normalized profile and mutates it in
// place; it may be async, but it must not do anything slow — the window
// between the read and the write is the window in which another context's
// write is lost, and keeping it to one storage round trip is what makes a
// whole-record write safe here. Concretely: the write is the WHOLE record, so
// a popup write that lands inside that window is reverted, in every field, by
// the record this turn read before it. That is accepted because the window is
// one round trip long and the popup is not writing while the worker is;
// widening it is what would make it a real hazard.
//
// `mutate` must also not call updateState() itself, directly or through
// anything it awaits: the queue is strictly serial, so the inner turn waits on
// the outer one, which is waiting on the inner one. That deadlocks the whole
// background, not just the caller. Mutate the record you were handed.
//
// Resolves with the record that was written.
function updateState(mutate) {
    const turn = updateQueue.then(() => updateStateOnce(mutate));
    // The queue must advance even when a turn rejects, or every update after
    // it queues behind a promise that never settles.
    updateQueue = turn.catch(() => {});
    return turn;
}

module.exports = { getState, updateState };
