-- VDS_ActionMeta
-- ============================================================================
-- Action meta (invalidation paths, persists, combatUnsafe) for every entry in
-- Constants.ACTIONS. Boot validator in Init.lua errors if any action is missing.
-- Separate file so the test harness can dofile it without slash/BE side effects.

VDS = VDS or {}

local A = VDS.Constants.ACTIONS
local P = VDS.Paths

local function persisted(paths) return { persists = true,  combatUnsafe = false, noop = false, invalidates = paths } end
local function session(paths)   return { persists = false, combatUnsafe = false, noop = false, invalidates = paths } end

VDS.Store:RegisterActionMeta({
    -- ===== Sandbox =====

    -- ===== Engine =====
    [A.MAIN_WINDOW_TOGGLE]       = session({ "session.ui.windowShown" }),
    [A.MAIN_WINDOW_SHOW]         = session({ "session.ui.windowShown" }),
    [A.MAIN_CONFIG_TOGGLE]       = session({ "session.ui.configShown" }),
    [A.MAIN_WINDOW_HIDE]         = session({ "session.ui.windowShown" }),
    [A.CONFIG_SET]               = { persists = true, combatUnsafe = false, noop = false,
        invalidates = function(action)
            local k = action.payload and action.payload.key  -- exception(optional): payload optional on some actions
            if k then return { P.Join("account.config", tostring(k)) } end
            return { "account.config" }
        end,
    },
    [A.HARD_RESET]               = persisted({ "account" }),
    [A.BAG_TICK]                 = session({ "session.bag.tick" }),
    [A.ITEM_NAME_RESOLVED]       = session({ "session.items.names", "session.items.tick" }),
    [A.ITEM_NAMES_BATCH_RESOLVED] = session({ "session.items.names", "session.items.tick" }),
    [A.SESSION_IDENTITY_SET]     = session({ "session.identity" }),

    -- ===== Filter / mode =====
    [A.SET_FILTER_MODE]          = session({ "session.ui.filterMode" }),
    [A.SET_SEARCH_TEXT]          = session({ "session.ui.searchText" }),
    [A.SET_OWNED_ONLY]           = persisted({ "account.config.ownedOnly" }),
    [A.SET_REPLACE_BLIZZARD]     = persisted({ "account.config.replaceBlizzard" }),
    [A.SET_ACTIVE_TAB]           = session({ "session.ui.activeTab" }),
    [A.SET_ACTIVE_SUBFILTER]     = { persists = false, combatUnsafe = false, noop = false,
        -- byView[filterMode] is not in scope at Dispatch time; coarse invalidate. (R4)
        invalidates = function(_action) return { "session.ui.byView" } end,
    },
    [A.SET_HERB_SORT_MODE]       = { persists = false, combatUnsafe = false, noop = false,
        -- herbSortMode lives in byView[filterMode]; coarse invalidate. (R4)
        invalidates = function(_action) return { "session.ui.byView" } end,
    },
    -- Selection at session.ui (NOT byView): narrow invalidation keeps hover-preview cheap.
    -- byView placement caused ~16MB/s allocation during R2 (every byView selector re-fired).
    [A.SET_SELECTED_DYE]         = session({ "session.ui.selectedDyeID" }),
    [A.SET_SELECTED_HERB]        = session({ "session.ui.selectedHerbID" }),

    -- ===== Custom themes =====
    -- CREATE auto-enters edit mode (CUSTOM_THEME_EDIT_START effect); meta covers both writes.
    -- editingThemeName + activeTheme live in byView[mode] (R3); coarse invalidate. (filterMode not in scope)
    [A.CUSTOM_THEME_CREATE]      = persisted({
        "account.customThemes",
        "session.ui.byView",
    }),
    [A.CUSTOM_THEME_DELETE]      = persisted({
        "account.customThemes", "session.ui.byView",
    }),
    [A.CUSTOM_THEME_RENAME]      = persisted({
        "account.customThemes", "session.ui.byView",
    }),
    [A.CUSTOM_THEME_EDIT_START]  = { persists = false, combatUnsafe = false, noop = false,
        invalidates = function(_action) return { "session.ui.byView" } end,
    },
    [A.CUSTOM_THEME_EDIT_END]    = { persists = false, combatUnsafe = false, noop = false,
        invalidates = function(_action) return { "session.ui.byView" } end,
    },
    [A.CUSTOM_THEME_TOGGLE_DYE]  = persisted({ "account.customThemes" }),

    -- ===== Canvas =====
    [A.CANVAS_SET_DECOR]         = session({
        "session.ui.canvas.decorID", "session.ui.canvas.dyes",
        "session.ui.canvas.activeChannel", "session.ui.canvas.numChannels",
        "session.ui.canvas.channelSet",
    }),
    [A.CANVAS_SET_CHANNEL_DYE]   = session({ "session.ui.canvas.dyes" }),
    [A.CANVAS_CLEAR_CHANNEL]     = session({ "session.ui.canvas.dyes" }),
    [A.CANVAS_RANDOM]            = session({ "session.ui.canvas.dyes" }),
    [A.CANVAS_SET_ACTIVE_CHANNEL] = session({ "session.ui.canvas.activeChannel" }),
    [A.CANVAS_CYCLE_ACTIVE_CHANNEL] = session({ "session.ui.canvas.activeChannel" }),
    [A.CANVAS_SET_PICKER_MODE]   = session({ "session.ui.canvas.pickerMode" }),
    [A.CANVAS_SET_PICKER_SEARCH] = session({ "session.ui.canvas.pickerSearch" }),
    [A.CANVAS_SAVE_LOADOUT]      = persisted({ "account.canvas.loadouts" }),
    [A.CANVAS_DELETE_LOADOUT]    = persisted({ "account.canvas.loadouts" }),
    [A.CANVAS_RENAME_LOADOUT]    = persisted({ "account.canvas.loadouts" }),
    [A.CANVAS_AUTOSAVE_SET]      = persisted({ "account.canvas.autoSave" }),
    [A.CANVAS_PREVIEW_DETACH_SET] = persisted({ "account.canvas.previewDetached" }),
    [A.CANVAS_SET_SCOPE]         = session({ "session.ui.byView.canvas.scope" }),
    [A.CANVAS_SET_CATEGORY]      = session({ "session.ui.byView.canvas.selectedCategory" }),
    -- Harmonise: fills all channels in one dispatch (same invalidation as CANVAS_RANDOM).
    [A.CANVAS_HARMONISE]         = session({ "session.ui.canvas.dyes" }),

    -- ===== Live mode =====
    [A.LIVE_MODE_ENTER]          = session({
        "session.ui.live.active", "session.ui.live.decorID", "session.ui.live.decorName",
        "session.ui.live.slots", "session.ui.live.originalDyes",
        "session.ui.live.focusedSlot",
        "session.ui.byView",  -- clears stale custom-tab editingThemeName
    }),
    [A.LIVE_MODE_EXIT]           = session({
        "session.ui.live.active", "session.ui.live.decorID", "session.ui.live.decorName",
        "session.ui.live.slots", "session.ui.live.originalDyes",
        "session.ui.live.intent", "session.ui.live.focusedSlot",
        "session.ui.live.cost",
    }),
    [A.LIVE_SLOTS_UPDATE]        = session({ "session.ui.live.slots" }),
    [A.LIVE_SET_SLOT_DYE_INTENT] = session({ "session.ui.live.intent" }),
    [A.LIVE_SLOT_FOCUS_SET]      = session({ "session.ui.live.focusedSlot" }),
    [A.LIVE_RECENT_DYE_ADD]      = persisted({ "account.live.recentDyes" }),
    [A.LIVE_AUTOSAVE_SET]        = persisted({ "account.live.autoSave" }),
    [A.LIVE_COST_UPDATE]         = session({ "session.ui.live.cost" }),
    [A.LIVE_COMMIT_SNAPSHOT]     = session({ "session.ui.live.originalDyes" }),

    -- ===== Ownership + scan =====
    [A.OWNERSHIP_TICK]           = session({ "session.dyes.ownership" }),
    [A.DYEABLE_ITEMS_UPDATE]     = persisted({ "account.dyeableItems" }),
    [A.DYE_DATA_INIT]            = session({ "session.dyes.catalog" }),

    -- ===== Catalog (ADR-001 amendment) =====
    -- CATALOG_READY is the invalidation seam for DecorInfoResolver; widgets reading
    -- catalog data declare calls = { "catalog.ready" } to re-resolve on repopulate.
    [A.CATALOG_READY]            = session({ "session.catalog.tick" }),
    -- CANVAS_CATALOG_UPDATE: writes session view AND persists account snapshot
    -- for cross-session fallback. persisted() makes the snapshot survive /reload.
    [A.CANVAS_CATALOG_UPDATE]    = persisted({
        "session.canvas.dyeableDecor",
        "session.canvas.decorCategoryNames",
        "account.canvas.catalogSnapshot",
    }),
    -- CATALOG_STATUS_SET: tracks { status, attempt } so the emptyHint label can
    -- show "Loading.../Retrying N/M/Failed" to users during cold-start churn.
    [A.CATALOG_STATUS_SET]       = session({ "session.canvas.catalog" }),
    -- [A.CANVAS_TOGGLE_EXPAND] retired in Phase 5 along with the tree picker.
    [A.CANVAS_APPLY_LOADOUT]     = session({
        "session.ui.canvas.decorID", "session.ui.canvas.dyes",
        "session.ui.canvas.numChannels", "session.ui.canvas.channelSet",
        "session.ui.canvas.activeChannel",
    }),

    -- ===== Window + minimap =====
    [A.PANEL_POSITION_SAVE]      = persisted({ "account.panelPosition" }),
    [A.MINIMAP_HIDE_SET]         = persisted({ "account.minimap.hide" }),
    [A.MINIMAP_POSITION_SAVE]    = persisted({ "account.minimap.minimapPos" }),

    -- ===== Warehouse =====
    [A.STOCK_TICK]               = session({ "session.stock.tick" }),
    [A.STOCK_SET_GROUP_TARGET]   = persisted({ "account.warehouse.targetGroups" }),
    [A.STOCK_BUMP_ALL_GROUPS]    = persisted({ "account.warehouse.targetGroups" }),
    [A.STOCK_RESET_ALL_TARGETS]  = persisted({ "account.warehouse.targetGroups", "account.warehouse.targetDyes" }),
    [A.STOCK_SET_DYE_TARGET]     = persisted({ "account.warehouse.targetDyes" }),
    [A.STOCK_RESET_DYE_OVERRIDE] = persisted({ "account.warehouse.targetDyes" }),
    [A.STOCK_SET_FILL_MODE]      = persisted({ "account.warehouse.fillMode" }),
    [A.STOCK_SET_BUY_GAP]        = persisted({ "account.warehouse.buyGap" }),
    [A.STOCK_TOGGLE_GROUP]        = persisted({ "account.warehouse.collapsedGroups" }),
    [A.STOCK_COLLAPSE_ALL_GROUPS] = persisted({ "account.warehouse.collapsedGroups" }),
    [A.STOCK_EXPAND_ALL_GROUPS]   = persisted({ "account.warehouse.collapsedGroups" }),
})
