-- VDS_LiveCustomizeObserver
-- ============================================================================
-- Boundary observer for the in-game live-dye flow. Subscribes to
-- HOUSING_CUSTOMIZE_MODE_SELECTED_TARGET_CHANGED + HOUSING_DECOR_CUSTOMIZATION_CHANGED;
-- dispatches LIVE_MODE_ENTER / LIVE_MODE_EXIT / LIVE_SLOTS_UPDATE.
--
-- Blizzard_HouseEditor is LoD. We wait for ADDON_LOADED("Blizzard_HouseEditor")
-- to install hooksecurefunc hooks; if the addon is already loaded at our boot
-- (a /reload while inside the editor), install immediately. This mirrors the
-- donor's VDS_LivePreview.lua:822-830 pattern.
--
-- OWNS:        C_HousingCustomizeMode.*, HOUSING_CUSTOMIZE_*, HOUSING_DECOR_CUSTOMIZATION_*
-- FORBIDDEN:   any other file reading OR writing C_HousingCustomizeMode
--              directly. Controllers use the WRITE surface published below
--              (ApplyDye / CancelEditing / CommitDyes); reads stay on the
--              event-driven Scan/Update path.

VDS = VDS or {}

local A = VDS.Constants.ACTIONS

VDS.Modules:Declare({
    name = "LiveCustomizeObserver",
    dependencies = {},

    ownsBlizzardNamespaces = { "C_HousingCustomizeMode" },

    blizzardEvents = {
        HOUSING_CUSTOMIZE_MODE_SELECTED_TARGET_CHANGED = {
            handler = "OnTargetChanged",
        },
        HOUSING_DECOR_CUSTOMIZATION_CHANGED = {
            handler = "OnCustomizationChanged",
        },
        -- ADDON_LOADED is also relevant (for LoD HouseEditor) but we handle it
        -- in onEnable via an explicit BlizzardEvents:Subscribe so we can filter
        -- on the loaded addon name.
    },

    onEnable = function(self)
        -- Deferral: HouseEditor frames may not be realized the same frame ADDON_LOADED fires;
        -- hooks attached too early miss their target (donor VDS_LivePreview.lua:790,823).
        local function deferredInstall()
            _G.C_Timer.After(0, function() self:InstallHousingHooks() end)
        end

        if _G.C_AddOns.IsAddOnLoaded("Blizzard_HouseEditor") then
            deferredInstall()
        else
            -- Filter must be in opts, not the handler: engine checks opts.filter before _Invoke,
            -- so a name check inside the handler with once=true kills the subscriber on the first
            -- unrelated ADDON_LOADED before Blizzard_HouseEditor ever fires.
            VDS.BlizzardEvents:Subscribe("ADDON_LOADED", {
                mode    = "handler",
                handler = function() deferredInstall() end,
                opts    = {
                    once   = true,
                    filter = function(name) return name == "Blizzard_HouseEditor" end,
                },
                _event  = "ADDON_LOADED",
            })
        end
    end,

    OnTargetChanged = function(self, hasTarget, targetType)
        if hasTarget and targetType == _G.Enum.HousingCustomizeModeTargetType.Decor then
            local info = _G.C_HousingCustomizeMode.GetSelectedDecorInfo()  -- combat-safe: HOUSING_* events fire out of combat
            if info and info.canBeCustomized and info.dyeSlots and #info.dyeSlots > 0 then  -- exception(boundary): GetSelectedDecorInfo can return nil/partial
                -- originalDyes derived from dyeSlots inside the LIVE_MODE_ENTER
                -- reducer via the shared buildOriginalDyes helper (single source
                -- of truth shared with LIVE_COMMIT_SNAPSHOT).
                VDS.Store:Dispatch({
                    type = A.LIVE_MODE_ENTER,
                    payload = {
                        decorID   = info.decorID,
                        decorName = info.name,
                        dyeSlots  = info.dyeSlots,
                    },
                })
                return
            end
        end
        VDS.Store:Dispatch({ type = A.LIVE_MODE_EXIT })
    end,

    OnCustomizationChanged = function(self)
        local st = VDS.Store:GetState()
        if not st.session.ui.live.active then return end
        local cmode = _G.C_HousingCustomizeMode
        local info = cmode.GetSelectedDecorInfo()  -- combat-safe: HOUSING_* events fire out of combat
        if info and info.dyeSlots then  -- exception(boundary): GetSelectedDecorInfo can return nil/partial
            VDS.Store:Dispatch({
                type = A.LIVE_SLOTS_UPDATE,
                payload = { dyeSlots = info.dyeSlots },
            })
        end
        -- Move cost to state so costText label binds state instead of smuggling C_HousingCustomizeMode reads inside a formatFn.
        VDS.Store:Dispatch({
            type    = A.LIVE_COST_UPDATE,
            payload = {
                spend  = cmode.GetNumDyesToSpendOnSelectedDecor(),
                remove = cmode.GetNumDyesToRemoveOnSelectedDecor(),
            },
        })
    end,

    InstallHousingHooks = function(self)
        if self._hooksInstalled then return end
        self._hooksInstalled = true
        -- hooksecurefunc + pane-alpha swapping is controller behavior; delegate.
        VDS.Controller_LiveBridge:Init()

        -- Donor (VDS_LivePreview.lua:827-831): /reload while in customize mode fires
        -- SELECTED_TARGET_CHANGED before our subscription; probe and replay.
        if _G.C_HousingCustomizeMode.IsDecorSelected() then  -- combat-safe: housing mode is not combat
            local info = _G.C_HousingCustomizeMode.GetSelectedDecorInfo()  -- combat-safe: housing mode
            if info and info.canBeCustomized and info.dyeSlots and #info.dyeSlots > 0 then  -- exception(boundary): GetSelectedDecorInfo can return nil/partial
                -- /reload replay: same payload shape as OnTargetChanged.
                -- originalDyes derived inside the reducer (shared helper).
                VDS.Store:Dispatch({
                    type    = VDS.Constants.ACTIONS.LIVE_MODE_ENTER,
                    payload = {
                        decorID   = info.decorID,
                        decorName = info.name,
                        dyeSlots  = info.dyeSlots,
                    },
                })
            end
        end

        VDS.Log:Info("live", "Blizzard_HouseEditor loaded; live bridge hooks installed")
    end,

    -- WRITE surface: wraps C_HousingCustomizeMode writes so namespace stays owned here (invariant §9).
    -- combat-safe: housing mode exits at combat start; no InCombat gate needed.
    ApplyDye = function(_self, slotID, dyeID)
        _G.C_HousingCustomizeMode.ApplyDyeToSelectedDecor(slotID, dyeID)  -- combat-safe: housing customize mode exits at combat start, controllers call this from Button OnClick
    end,

    -- Clear a single slot. Donor (VDS_LivePreview.lua:178-184) passed `nil`;
    -- MCP gotcha claimed `0` clears but it was a no-op in-game on 12.0.5 retail.
    -- `nil` triggers HOUSING_DECOR_CUSTOMIZATION_CHANGED correctly.
    ClearSlot = function(_self, slotID)
        _G.C_HousingCustomizeMode.ApplyDyeToSelectedDecor(slotID, nil)  -- combat-safe: housing mode
    end,

    -- Wipe every dye slot back to default (no-dye) state. Both bulk APIs are
    -- broken on 12.0.5 retail: ClearDyesOnSelectedDecor is nil (despite the
    -- 'verified' MCP entry, same trap as GetNumDyeSlots) and the `For` variant
    -- returns success but is a silent no-op (visual didn't update in-game).
    -- Workaround: iterate the current dyeSlots and clear each via the verified
    -- ApplyDyeToSelectedDecor(slotID, nil) path -- the same mechanism the
    -- right-click slot clear uses.
    ClearAllSlots = function(_self)
        local info = _G.C_HousingCustomizeMode.GetSelectedDecorInfo()  -- combat-safe: housing mode
        if not (info and info.dyeSlots) then return end  -- exception(boundary): GetSelectedDecorInfo can return nil/partial
        for _, slot in ipairs(info.dyeSlots) do
            _G.C_HousingCustomizeMode.ApplyDyeToSelectedDecor(slot.ID, nil)  -- combat-safe
        end
    end,

    CancelEditing = function(_self)
        _G.C_HousingCustomizeMode.CancelActiveEditing()  -- combat-safe: Cancel button OnClick
    end,

    CommitDyes = function(_self)
        _G.C_HousingCustomizeMode.CommitDyesForSelectedDecor()  -- combat-safe: Apply button OnClick
    end,
})
