import { createSlice } from '@genesislcap/foundation-redux';
import { emptyUsage, sumUsage } from '../utils/sum-usage';
/**
 * Build a fresh default session state. A factory (not just a shared const) so
 * each call returns independent collections — `resetSession` can hand back a
 * clean slate without aliasing the initial state's arrays.
 */
export function createDefaultSessionState() {
    return {
        messages: [],
        state: 'idle',
        showToolCalls: false,
        showThinkingSteps: false,
        showNarration: false,
        showAgentSwitchIndicator: false,
        enabledAnimations: [],
        suggestionsState: { status: 'idle' },
        contextTokens: undefined,
        contextLimit: undefined,
        sessionUsage: emptyUsage(),
        activeModel: undefined,
        activeProviderName: undefined,
        providerStatuses: [],
        activeAgent: undefined,
        pinnedAgentName: null,
        flowOwnerAgentName: null,
        flowActivationPrompt: null,
        agentPickerOpen: false,
        sessionMenuOpen: false,
        compacting: false,
        restoring: false,
        blocked: false,
        blockedReason: null,
        blockedVendors: [],
        sweptVendors: [],
        blockedVendorReasons: {},
        vendorBudgets: {},
        inputValue: '',
        liveSubAgentTrace: [],
        liveSubAgentName: null,
        inputOverrides: [],
    };
}
export const defaultSessionState = createDefaultSessionState();
export const aiAssistantSlice = createSlice({
    name: 'aiAssistant',
    initialState: defaultSessionState,
    reducers: {
        setMessages(state, action) {
            state.messages = action.payload;
        },
        setState(state, action) {
            state.state = action.payload;
        },
        setShowToolCalls(state, action) {
            state.showToolCalls = action.payload;
        },
        setShowThinkingSteps(state, action) {
            state.showThinkingSteps = action.payload;
        },
        setShowNarration(state, action) {
            state.showNarration = action.payload;
        },
        setShowAgentSwitchIndicator(state, action) {
            state.showAgentSwitchIndicator = action.payload;
        },
        setEnabledAnimations(state, action) {
            state.enabledAnimations = action.payload;
        },
        setSuggestionsState(state, action) {
            state.suggestionsState = action.payload;
        },
        setContextTokens(state, action) {
            state.contextTokens = action.payload;
        },
        setContextLimit(state, action) {
            state.contextLimit = action.payload;
        },
        setSessionUsage(state, action) {
            state.sessionUsage = action.payload;
        },
        setActiveModel(state, action) {
            state.activeModel = action.payload;
        },
        setActiveProviderName(state, action) {
            state.activeProviderName = action.payload;
        },
        setProviderStatuses(state, action) {
            state.providerStatuses = action.payload;
        },
        setActiveAgent(state, action) {
            state.activeAgent = action.payload;
        },
        setPinnedAgentName(state, action) {
            state.pinnedAgentName = action.payload;
        },
        setFlowOwnerAgentName(state, action) {
            state.flowOwnerAgentName = action.payload;
        },
        setFlowActivationPrompt(state, action) {
            state.flowActivationPrompt = action.payload;
        },
        setAgentPickerOpen(state, action) {
            state.agentPickerOpen = action.payload;
        },
        setSessionMenuOpen(state, action) {
            state.sessionMenuOpen = action.payload;
        },
        setCompacting(state, action) {
            state.compacting = action.payload;
        },
        setRestoring(state, action) {
            state.restoring = action.payload;
        },
        /**
         * Latch (or release) the backend-blocked state.
         *
         * `reason` is only meaningful while blocking, and it distinguishes three
         * cases rather than two:
         *
         * - **omitted** — a re-latch that leaves the existing explanation intact.
         *   Load-bearing: the block has two writers (the host, and the driver's
         *   automatic latch), and the driver's write lands AFTER a bus-subscribed
         *   host has already set its own detailed copy. Treating "no reason" as
         *   "clear the reason" made that sequence replace the host's figures with
         *   generic copy every time, deterministically.
         * - **a string** — replaces the explanation.
         * - **`null`** — explicitly clears the explanation while staying blocked;
         *   the banner falls back to the element's default copy.
         *
         * Unblocking always clears it, so a stale explanation can never outlive the
         * condition it described.
         *
         * Unblocking **also clears every per-vendor latch**, and that is the single
         * rule that keeps per-vendor budgets non-breaking. A host asserting "the wall
         * is gone" (the documented pre-flight, migration-GENC-1464 Option B) must not be
         * silently overruled by a driver latch it cannot see; without this, the same
         * `setBlocked(false)` that used to unblock would leave the composer locked.
         */
        setBlocked(state, action) {
            state.blocked = action.payload.blocked;
            if (!action.payload.blocked) {
                state.blockedReason = null;
                state.blockedVendors = [];
                state.sweptVendors = [];
                state.blockedVendorReasons = {};
            }
            else if (action.payload.reason !== undefined) {
                state.blockedReason = action.payload.reason;
            }
        },
        /**
         * Write the banner explanation without touching the flag.
         *
         * Separate from {@link setBlocked} rather than expressed as
         * `setBlocked({ blocked: current, reason })`, because the element's public
         * `blocked` is now DERIVED: re-latching with the derived value would pass
         * `false` for a partially-walled session and so clear every per-vendor latch
         * as a side effect of setting a string.
         */
        setBlockedReason(state, action) {
            state.blockedReason = action.payload;
        },
        /**
         * Wall (or release) a single vendor's budget.
         *
         * The `reason` trichotomy mirrors {@link setBlocked}'s exactly, and for the
         * same reason: the same two-writer race (the driver's latch landing after a
         * bus-subscribed host has written its own copy) applies per vendor.
         *
         * Adding an already-walled vendor is idempotent — the array is a set with an
         * order, so the vendor keeps the position it was first walled at.
         */
        setVendorBlocked(state, action) {
            const { vendor, blocked, reason, swept } = action.payload;
            const index = state.blockedVendors.indexOf(vendor);
            const sweptIndex = state.sweptVendors.indexOf(vendor);
            if (blocked) {
                if (index < 0)
                    state.blockedVendors.push(vendor);
                if (swept) {
                    // Provenance is FIRST-writer-wins: marking swept only records a wall
                    // this call itself created. The sweep site already skips walled
                    // vendors, so this guard is belt-and-braces against a future caller
                    // downgrading a refused wall to "just the proxy's word".
                    if (index < 0 && sweptIndex < 0)
                        state.sweptVendors.push(vendor);
                }
                else if (sweptIndex >= 0) {
                    state.sweptVendors.splice(sweptIndex, 1);
                }
                if (reason === null) {
                    delete state.blockedVendorReasons[vendor];
                }
                else if (reason !== undefined) {
                    state.blockedVendorReasons[vendor] = reason;
                }
            }
            else {
                if (index >= 0)
                    state.blockedVendors.splice(index, 1);
                if (sweptIndex >= 0)
                    state.sweptVendors.splice(sweptIndex, 1);
                delete state.blockedVendorReasons[vendor];
            }
        },
        /**
         * Store (or clear, with `null`) one vendor's budget figures for the meter.
         *
         * Validation lives at the element boundary (`setVendorBudget` on the
         * element), not here — the reducer is the storage layer and trusts its one
         * caller, exactly as `setVendorBlocked` above trusts its.
         *
         * Idempotent by VALUE, not just by effect: re-feeding the figures a vendor
         * already holds leaves the stored object untouched. The documented feed
         * re-runs after every turn, so without this every turn would publish a new
         * store reference for the same dollars and re-render a meter that has not
         * changed.
         */
        setVendorBudget(state, action) {
            const { vendor, figures } = action.payload;
            if (figures === null) {
                delete state.vendorBudgets[vendor];
                return;
            }
            const existing = state.vendorBudgets[vendor];
            if (existing &&
                existing.budgetUsd === figures.budgetUsd &&
                existing.spentUsd === figures.spentUsd) {
                return;
            }
            // A fresh object rather than the caller's: the store may freeze what it
            // holds, and adopting the host's object would freeze state the host still
            // owns (and let later host mutations bypass the store).
            state.vendorBudgets[vendor] = { budgetUsd: figures.budgetUsd, spentUsd: figures.spentUsd };
        },
        setInputValue(state, action) {
            state.inputValue = action.payload;
        },
        setLiveSubAgentTrace(state, action) {
            state.liveSubAgentTrace = action.payload;
        },
        setLiveSubAgentName(state, action) {
            state.liveSubAgentName = action.payload;
        },
        addInputOverride(state, action) {
            state.inputOverrides.push(action.payload);
        },
        removeInputOverride(state, action) {
            state.inputOverrides = state.inputOverrides.filter((o) => o.id !== action.payload.id);
        },
        /**
         * Bulk-hydrate the restorable fields of a persisted session (page-reload
         * restore) and set `state` back to `idle`. Agent identity and the per-agent
         * machine snapshot are applied separately by the host — see
         * `LoadSessionPayload`.
         */
        loadSession(state, action) {
            const p = action.payload;
            state.messages = p.messages;
            state.pinnedAgentName = p.pinnedAgentName;
            state.flowOwnerAgentName = p.flowOwnerAgentName;
            // Derive the totals from the restored transcript rather than trusting a stored
            // counter. Restore dispatches this action directly, bypassing the host's
            // `messages` setter — the thing that normally recomputes — so a stored total
            // would sit stale (historically at 0) until the next turn arrived. The
            // transcript is the source of truth: per-message usage survives the snapshot,
            // and a compacted summary carries the usage of the turns it replaced.
            state.sessionUsage = sumUsage(p.messages);
            state.contextTokens = p.contextTokens;
            state.contextLimit = p.contextLimit;
            state.activeModel = p.activeModel;
            state.activeProviderName = p.activeProviderName;
            state.state = 'idle';
        },
        /**
         * Wipe the session back to a fresh default — the "Clear" / "New chat" action.
         *
         * Three categories survive, for three different reasons — keep them distinct:
         *
         * 1. The user's **display preferences** (tool-call / thinking / narration /
         *    agent-switch visibility + animations). They are settings, not
         *    conversation state, and are persisted independently of the snapshot
         *    (GENC-1351). Without this they'd flip to their all-off defaults in the
         *    live store on Clear, since `clearSession` re-applies no baseline and the
         *    idempotent setters suppress a re-apply.
         * 2. The **backend block** (`blocked` / `blockedReason`, and the per-vendor
         *    latches beside them). Not a preference at
         *    all — a standing backend condition that Clear cannot change. It is here
         *    because starting a new chat does not refill the AI budget, so clearing
         *    it would hand the user a working-looking composer that fails on the very
         *    next send.
         * 3. The **provider statuses**. They describe the REGISTRY, not the
         *    conversation — provider registration is static for the store's lifetime
         *    and this snapshot is refreshed from the registry, never from a turn. They
         *    are session state only because that is where the element's store lives.
         *
         *    Dropping them on Clear is not cosmetic: the element derives `blocked`
         *    from "every REACHABLE vendor is walled", and an empty reachable set fails
         *    safe to "blocked". So wiping the statuses while keeping the per-vendor
         *    latches (category 2) escalated one walled vendor out of two into a fully
         *    locked composer, and nothing reloads them — `loadProviderStatuses()` runs
         *    only on connect/session-switch and from the observable-registry
         *    subscription, so a host with a static registry stayed locked until a
         *    remount. Preserving is the fix rather than re-loading after reset,
         *    because the reload is async: it would still flash a fully locked composer
         *    for a turn of the event loop, and would break a host whose registry is
         *    immutable (nothing to re-listen to) or whose `listStatuses()` rejects.
         *
         * Stated separately so a future contributor deciding what belongs in this
         * list does not "tidy" the block out on the grounds that it is not a display
         * preference.
         */
        resetSession(state) {
            // `satisfies` gives this literal excess-property checking and per-field type
            // checking, neither of which `Object.assign` provides — its source parameter
            // is structurally permissive, so a typo (`blockedResaon`) would compile
            // clean, silently stop preserving the real key, AND add a junk key to the
            // draft that then leaks into serialised state.
            const preservedAcrossReset = {
                showToolCalls: state.showToolCalls,
                showThinkingSteps: state.showThinkingSteps,
                showNarration: state.showNarration,
                showAgentSwitchIndicator: state.showAgentSwitchIndicator,
                enabledAnimations: state.enabledAnimations,
                providerStatuses: state.providerStatuses,
                blocked: state.blocked,
                blockedReason: state.blockedReason,
                blockedVendors: state.blockedVendors,
                sweptVendors: state.sweptVendors,
                blockedVendorReasons: state.blockedVendorReasons,
                // Category 2, beside the blocked latch: the budget figures are a fact
                // about the USER'S DEPLOYMENT (what the server meters), not about this
                // conversation — "Clear" refills no budget, so wiping them would blank
                // the meter until the host's next re-feed while the dollars are
                // unchanged. Opposite side from `contextTokens`, which IS conversation
                // state (a new chat genuinely has an empty context window).
                vendorBudgets: state.vendorBudgets,
            };
            Object.assign(state, createDefaultSessionState(), preservedAcrossReset);
        },
    },
    selectors: {},
});
