-- ============================================================================
-- Housing Decor Guide - Store (Simplified)
-- Direct state management with persistence
-- ============================================================================

local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide

-- Deep copy helper (only used for array operations)
local function DeepCopy(orig)
    local copy
    if type(orig) == "table" then
        copy = {}
        for k, v in pairs(orig) do
            copy[k] = DeepCopy(v)
        end
    else
        copy = orig
    end
    return copy
end

-- Default state template
local DEFAULT_STATE = {
    config = {
        debug = false,
        showMinimapButton = true,
        theme = "HDGDefault",
        scale = 1.0,
        fontScale = 0,
        fontFamily = "FRIZQT__",
        bgOpacity = 0.95,
        quotesEnabled = true,
        vamooseChattiness = 30,
        vamooseChatOutput = false,
        lumberQuotesEnabled = true,
        craftQuotesEnabled = true,
        quotesOnlyChat = false,
        closeProfWindowAfterCraft = true,
        showProfessionButtons = true,
        preferredPriceAddon = nil,
        tsmPriceMode = "server",
        showRawMaterials = false,
        myMaterialsMode = false,
        materialSortMode = "totals",
        craftableFilter = false,
        knownFilter = false,
        expansionFilter = "All",
        searchReagents = false,
        uncollectedFilter = false,
        lumberCollapsed = false,
        lumberAutoShow = false,
        showLumberMaterials = false,
        lumberHUDEnabled = true,
        lumberHUDPosition = nil,
        lumberHUDVisible = false,
        lumberRadarPinToTracker = false,
        tooltipDecorTag = true,
        zoneScannerEnabled = false,
        zoneScannerPopup = false,
        zoneScannerSquirrels = false,
        zoneScannerChat = true,
        zoneScannerSound = false,
        shopListAutoDecrement = true,
        defaultTab = nil,  -- If set, switches to this tab on first main-frame show per session. Right-click any tab button to set.
    },
    ui = {
        framePosition = nil,
        activeTab = "recipes",
        professionFilter = "All",
        characterProfessionFilter = {},
        collapsedMaterials = {},
        trainersCollapsed = {},
        stylesCollapsed = {},
        hiddenCharacters = {},
        lumberPinned = false,
        pinnedLumberPosition = nil,
        vendorListPosition = nil,
        vendorListPinned = false,
        materialsListPosition = nil,
        materialsListPinned = false,
    },
    craftingQueue = {},
    characters = {},
    craftingHistory = {},
    trackedReagents = {},
    minimap = {
        hide = false,
        minimapPos = 220,
        lock = false,
        showMinimapButton = true,
        showInCompartment = true,
        icon = nil,
    },
    lumberCoordinates = {},
    farmingHistory = {},
    vendorShoppingLists = {}, -- map of listId -> { name, items, meta, createdAt }
    activeShoppingListId = "default",
    customStyles = {},
    customStyleOrder = {},
    layout = {},
    userNotes = {},
    vendorNotes = {},
    vendorNameCache = {},
    vendorCurrencyCache = {},
    -- Projects module — house topology, crates, library, plans, etc.
    -- Schema seeded for the full Projects feature surface; P1 uses houses/rooms/ui.
    -- See docs/superpowers/specs/2026-05-01-hdg-projects-module-design.md
    projects = {
        schemaVersion  = 1,
        houses         = {},
        rooms          = {},
        crates         = {},
        library        = {},
        shippingCrates = {},
        plans          = {},
        orphans        = {},
        ui = {
            activeView      = "landing",
            selectedHouse   = nil,
            selectedFloor   = 1,
            selectedRoomID  = nil,
            selectedCrateID = nil,
            activeMode      = "reflect",
            activePlanID    = nil,
        },
    },
    -- Module-owned state (migrated from direct-to-HDG_DB). Modules read/write
    -- via HDG.Store.state.X + QueueSave().
    collection = nil,        -- HDG_Data.lua — {version, ownedDecorIDs, lastSweep}
    directPriceCache = {},   -- HDG_PriceIntegration.lua — itemID -> price
    directPriceCacheTime = nil, -- HDG_PriceIntegration.lua — last AH scan timestamp
    ownedAuctions = {},      -- HDG_GoblinTab.lua — { [auctionID] = {...} }
    -- NOTE: HDG_DB.recentActivity still written direct by HDG_RecentActivity.lua
    -- (migration deferred). Preserve-line in SaveToSavedVariables keeps it alive.
    _runtime = { pendingCraftInfo = { itemID = nil, remaining = 0 } },
}

HDG.Store = {
    state = DeepCopy(DEFAULT_STATE),
    saveTimer = nil,
    onThemeChange = nil,  -- Theme change callback
}

function HDG.Store:GetState()
    return self.state
end

-- ============================================================================
-- PERSISTENCE (Simplified auto-serialize)
-- ============================================================================

-- Valid tab keys for activeTab validation
local VALID_TABS = { recipes=1, acquisition=1, preview=1, goblin=1, trainers=1, characters=1, styles=1, data=1, config=1 }

-- Type-safe merge: only overwrite when saved value matches default's type
local function MergeTable(target, source)
    if not source or type(source) ~= "table" then return end
    for k, v in pairs(source) do
        local targetVal = target[k]
        if type(v) == "table" and type(targetVal) == "table" then
            MergeTable(targetVal, v)
        elseif targetVal == nil or type(v) == type(targetVal) then
            target[k] = v
        end -- type mismatch on non-nil default: silently keep default
    end
end

-- Clamp numeric config values to safe ranges
local function ValidateConfig(config)
    if type(config.scale) == "number" then
        config.scale = math.max(0.5, math.min(2.0, config.scale))
    end
    if type(config.fontScale) == "number" then
        config.fontScale = math.max(-5, math.min(5, config.fontScale))
    end
    if type(config.bgOpacity) == "number" then
        config.bgOpacity = math.max(0.3, math.min(1.0, config.bgOpacity))
    end
    if type(config.vamooseChattiness) == "number" then
        config.vamooseChattiness = math.max(0, math.min(99, config.vamooseChattiness))
    end
end

-- Validate UI state values
local function ValidateUI(ui)
    if ui.activeTab and not VALID_TABS[ui.activeTab] then
        ui.activeTab = "recipes"
    end
end

-- Validate minimap values
local function ValidateMinimap(minimap)
    if type(minimap.minimapPos) == "number" then
        minimap.minimapPos = minimap.minimapPos % 360
    end
end

-- Sanitize an array table: keep only entries passing filterFn
local function SanitizeArray(saved, filterFn)
    if type(saved) ~= "table" then return nil end
    local clean = {}
    for _, entry in ipairs(saved) do
        if filterFn(entry) then
            table.insert(clean, entry)
        end
    end
    return clean
end

function HDG.Store:LoadFromSavedVariables()
    HDG_DB = HDG_DB or {}

    -- Merge config/ui/minimap with type safety
    MergeTable(self.state.config, HDG_DB.config)
    MergeTable(self.state.ui, HDG_DB.ui)
    MergeTable(self.state.minimap, HDG_DB.minimap)

    -- Validate merged values
    ValidateConfig(self.state.config)
    ValidateUI(self.state.ui)
    ValidateMinimap(self.state.minimap)

    -- Array tables: sanitize entries
    local clean
    -- Migration: craftingQueue was formerly named shoppingList
    local queueKey = HDG_DB.craftingQueue or HDG_DB.shoppingList
    clean = SanitizeArray(queueKey, function(e) return type(e) == "table" and e.id end)
    if clean then self.state.craftingQueue = clean end

    -- Multi-list shopping: migrate old format or load new format
    if type(HDG_DB.vendorShoppingLists) == "table" then
        -- New multi-list format: validate each list
        for id, list in pairs(HDG_DB.vendorShoppingLists) do
            if type(list) == "table" and type(list.items) == "table" then
                local items = SanitizeArray(list.items, function(e) return type(e) == "table" and e.itemID end)
                self.state.vendorShoppingLists[id] = {
                    name = type(list.name) == "string" and list.name or ("List " .. id),
                    items = items or {},
                    meta = type(list.meta) == "table" and list.meta or {},
                    createdAt = list.createdAt or time(),
                }
            end
        end
        if type(HDG_DB.activeShoppingListId) == "string" and self.state.vendorShoppingLists[HDG_DB.activeShoppingListId] then
            self.state.activeShoppingListId = HDG_DB.activeShoppingListId
        end
    elseif type(HDG_DB.vendorShoppingList) == "table" then
        -- Old single-list format: migrate to multi-list
        clean = SanitizeArray(HDG_DB.vendorShoppingList, function(e) return type(e) == "table" and e.itemID end)
        self.state.vendorShoppingLists["default"] = {
            name = "Shopping List",
            items = clean or {},
            meta = type(HDG_DB.vendorShoppingListMeta) == "table" and HDG_DB.vendorShoppingListMeta or {},
            createdAt = time(),
        }
        self.state.activeShoppingListId = "default"
    end

    clean = SanitizeArray(HDG_DB.craftingHistory, function(e) return type(e) == "table" and e.itemID end)
    if clean then self.state.craftingHistory = clean end

    clean = SanitizeArray(HDG_DB.farmingHistory, function(e) return type(e) == "table" and (e.sessionId or e.timestamp) end)
    if clean then self.state.farmingHistory = clean end

    clean = SanitizeArray(HDG_DB.customStyleOrder, function(e) return type(e) == "string" end)
    if clean then self.state.customStyleOrder = clean end

    -- Dict tables: type-checked assignment
    if type(HDG_DB.characters) == "table" then self.state.characters = HDG_DB.characters end
    if type(HDG_DB.trackedReagents) == "table" then self.state.trackedReagents = HDG_DB.trackedReagents end
    if type(HDG_DB.lumberCoordinates) == "table" then self.state.lumberCoordinates = HDG_DB.lumberCoordinates end
    if type(HDG_DB.customStyles) == "table" then
        self.state.customStyles = HDG_DB.customStyles
    end
    if type(HDG_DB.layout) == "table" then self.state.layout = HDG_DB.layout end
    if type(HDG_DB.userNotes) == "table" then self.state.userNotes = HDG_DB.userNotes end
    if type(HDG_DB.vendorNotes) == "table" then self.state.vendorNotes = HDG_DB.vendorNotes end
    if type(HDG_DB.vendorNameCache) == "table" then self.state.vendorNameCache = HDG_DB.vendorNameCache end
    if type(HDG_DB.vendorCurrencyCache) == "table" then self.state.vendorCurrencyCache = HDG_DB.vendorCurrencyCache end
    if type(HDG_DB.projects) == "table" then MergeTable(self.state.projects, HDG_DB.projects) end

    -- Module-owned state (formerly direct-to-HDG_DB). recentActivity is
    -- deferred — still owned by HDG_RecentActivity.lua until that module
    -- is migrated.
    if type(HDG_DB.collection) == "table" then self.state.collection = HDG_DB.collection end
    if type(HDG_DB.directPriceCache) == "table" then self.state.directPriceCache = HDG_DB.directPriceCache end
    if type(HDG_DB.directPriceCacheTime) == "number" then self.state.directPriceCacheTime = HDG_DB.directPriceCacheTime end
    if type(HDG_DB.ownedAuctions) == "table" then self.state.ownedAuctions = HDG_DB.ownedAuctions end

    -- Prune bloated SavedVariables (prevent unbounded growth)
    self:PruneSavedVariables()

    if self.state.config.debug then
        print("|cFF14b8a6[HDG Store]|r Loaded state from SavedVariables")
    end
end

function HDG.Store:QueueSave()
    if self.saveTimer then self.saveTimer:Cancel() end
    self.saveTimer = C_Timer.NewTimer(1, function() self:SaveToSavedVariables() end)
end

function HDG.Store:SaveToSavedVariables()
    HDG_DB = {
        config = self.state.config,
        ui = self.state.ui,
        minimap = self.state.minimap,
        craftingQueue = self.state.craftingQueue,
        characters = self.state.characters,
        craftingHistory = self.state.craftingHistory,
        trackedReagents = self.state.trackedReagents,
        lumberCoordinates = self.state.lumberCoordinates,
        farmingHistory = self.state.farmingHistory,
        vendorShoppingLists = self.state.vendorShoppingLists,
        activeShoppingListId = self.state.activeShoppingListId,
        customStyles = self.state.customStyles,
        customStyleOrder = self.state.customStyleOrder,
        layout = self.state.layout,
        userNotes = self.state.userNotes,
        vendorNotes = self.state.vendorNotes,
        vendorNameCache = self.state.vendorNameCache,
        vendorCurrencyCache = self.state.vendorCurrencyCache,
        projects = self.state.projects,
        -- Module-owned state — migrated to proper Store state ownership:
        collection = self.state.collection,
        directPriceCache = self.state.directPriceCache,
        directPriceCacheTime = self.state.directPriceCacheTime,
        ownedAuctions = self.state.ownedAuctions,
        -- recentActivity is still owned by HDG_RecentActivity.lua (migration
        -- deferred — see plans/PLAN_persist_owned_decor.md "Timeline"). Until
        -- that module is refactored, preserve through save by reading from
        -- HDG_DB directly. Once migrated, move to self.state.recentActivity.
        recentActivity = HDG_DB and HDG_DB.recentActivity or nil,
        -- House-tab cockpit state — owned by widgets, not the Store. These
        -- are also passed through verbatim to avoid being wiped on save:
        --   account.houses     = MultiHouse cross-faction house cache
        --   houseTab.*         = Picker / Editor overrides + dim tweaks
        --   records.dailyXP    = DailyXPTracker per-day XP delta log
        account  = HDG_DB and HDG_DB.account  or nil,
        houseTab = HDG_DB and HDG_DB.houseTab or nil,
        records  = HDG_DB and HDG_DB.records  or nil,
        favorites = HDG_DB and HDG_DB.favorites or nil,
    }
    if self.state.config.debug then
        print("|cFF14b8a6[HDG Store]|r Saved state to SavedVariables")
    end
end

function HDG.Store:Flush()
    if self.saveTimer then
        self.saveTimer:Cancel()
        self.saveTimer = nil
    end
    self:SaveToSavedVariables()
end

-- ============================================================================
-- SAVEDVARIABLES PRUNING (prevent unbounded growth)
-- ============================================================================

function HDG.Store:PruneSavedVariables()
    local CONFIG = privateDB.CONFIG
    if not CONFIG then return end

    -- Prune userNotes (oldest by timestamp)
    if self.state.userNotes then
        local count = 0
        for _ in pairs(self.state.userNotes) do count = count + 1 end
        if count > CONFIG.MAX_USER_NOTES then
            local sorted = {}
            for itemID, note in pairs(self.state.userNotes) do
                table.insert(sorted, { itemID = itemID, ts = note.ts or 0 })
            end
            table.sort(sorted, function(a, b) return a.ts < b.ts end)
            local toRemove = count - CONFIG.MAX_USER_NOTES
            for i = 1, toRemove do
                self.state.userNotes[sorted[i].itemID] = nil
            end
        end
    end

    -- Prune vendorNotes (oldest by timestamp)
    if self.state.vendorNotes then
        local count = 0
        for _ in pairs(self.state.vendorNotes) do count = count + 1 end
        if count > CONFIG.MAX_VENDOR_NOTES then
            local sorted = {}
            for npcID, note in pairs(self.state.vendorNotes) do
                table.insert(sorted, { npcID = npcID, ts = note.ts or 0 })
            end
            table.sort(sorted, function(a, b) return a.ts < b.ts end)
            local toRemove = count - CONFIG.MAX_VENDOR_NOTES
            for i = 1, toRemove do
                self.state.vendorNotes[sorted[i].npcID] = nil
            end
        end
    end

    -- Prune farmingHistory (oldest sessions)
    if self.state.farmingHistory and #self.state.farmingHistory > CONFIG.MAX_FARMING_HISTORY then
        local newHistory = {}
        for i = 1, CONFIG.MAX_FARMING_HISTORY do
            newHistory[i] = self.state.farmingHistory[i]
        end
        self.state.farmingHistory = newHistory
    end

    -- Prune characters (least recently seen)
    if self.state.characters then
        local count = 0
        for _ in pairs(self.state.characters) do count = count + 1 end
        if count > CONFIG.MAX_CHARACTERS then
            local sorted = {}
            for charKey, data in pairs(self.state.characters) do
                table.insert(sorted, { charKey = charKey, lastUpdated = data.lastUpdated or 0 })
            end
            table.sort(sorted, function(a, b) return a.lastUpdated < b.lastUpdated end)
            local toRemove = count - CONFIG.MAX_CHARACTERS
            for i = 1, toRemove do
                self.state.characters[sorted[i].charKey] = nil
            end
        end
    end
end

-- ============================================================================
-- DIRECT STATE METHODS (replaces reducers for simple operations)
-- ============================================================================

function HDG.Store:SetConfig(key, value)
    self.state.config[key] = value
    self:QueueSave()
end

function HDG.Store:SetTheme(themeName)
    self.state.config.theme = themeName
    if self.onThemeChange then self.onThemeChange(themeName) end
    self:QueueSave()
end

function HDG.Store:SetUI(key, value)
    self.state.ui[key] = value
    self:QueueSave()
end

function HDG.Store:SetMinimap(key, value)
    self.state.minimap[key] = value
    self:QueueSave()
end

function HDG.Store:UpdateLayout(frameName, layoutData)
    self.state.layout[frameName] = layoutData
    self:QueueSave()
end

-- ============================================================================
-- CRAFTING QUEUE METHODS
-- ============================================================================

function HDG.Store:AddToCraftingQueue(item)
    if not item or not item.id then
        if HDG.Config and HDG.Config.Get("debug") then
            print("|cFFdc322f[HDG Store]|r Rejected non-craftable item from queue:", item and item.name or "nil")
        end
        return
    end
    for i, existing in ipairs(self.state.craftingQueue) do
        if existing.id == item.id then
            self.state.craftingQueue[i].quantity = (existing.quantity or 1) + (item.quantity or 1)
            self:QueueSave()
            return
        end
    end
    table.insert(self.state.craftingQueue, {
        id = item.id,
        name = item.name,
        spell = item.spell,
        reagents = item.reagents,
        expansion = item.expansion,
        quantity = item.quantity or 1,
    })
    self:QueueSave()
end

function HDG.Store:RemoveFromCraftingQueue(itemID)
    for i, item in ipairs(self.state.craftingQueue) do
        if item.id == itemID then
            table.remove(self.state.craftingQueue, i)
            self:QueueSave()
            return
        end
    end
end

function HDG.Store:UpdateCraftingQueueItemQty(itemID, quantity)
    for i, item in ipairs(self.state.craftingQueue) do
        if item.id == itemID then
            if quantity <= 0 then
                table.remove(self.state.craftingQueue, i)
            else
                self.state.craftingQueue[i].quantity = quantity
            end
            self:QueueSave()
            return
        end
    end
end

function HDG.Store:ClearCraftingQueue()
    self.state.craftingQueue = {}
    self:QueueSave()
end

-- ============================================================================
-- CHARACTER DATA METHODS
-- ============================================================================

function HDG.Store:UpdateCharacterData(charKey, data)
    if not self.state.characters[charKey] then
        self.state.characters[charKey] = { professions = {}, lastUpdated = 0 }
    end
    local char = self.state.characters[charKey]

    if data.professions then
        -- Replace entire professions table so dropped professions are removed
        char.professions = {}
        for profName, profData in pairs(data.professions) do
            char.professions[profName] = profData
        end
    end

    if data.knowsFindLumber ~= nil then char.knowsFindLumber = data.knowsFindLumber end
    if data.lumberInventory then char.lumberInventory = data.lumberInventory end
    if data.lumberUpdated then char.lumberUpdated = data.lumberUpdated end
    char.lastUpdated = data.lastUpdated or time()

    self:QueueSave()
end

function HDG.Store:RemoveCharacter(charKey)
    self.state.characters[charKey] = nil
    self:QueueSave()
end

-- ============================================================================
-- CRAFTING HISTORY METHODS
-- ============================================================================

function HDG.Store:AddCraftingHistory(entry)
    entry.timestamp = entry.timestamp or time()
    table.insert(self.state.craftingHistory, 1, entry)
    local maxHistory = privateDB.CONFIG and privateDB.CONFIG.MAX_CRAFTING_HISTORY or 100
    while #self.state.craftingHistory > maxHistory do
        table.remove(self.state.craftingHistory)
    end
    self:QueueSave()
end

function HDG.Store:ClearCraftingHistory()
    self.state.craftingHistory = {}
    self:QueueSave()
end

-- ============================================================================
-- TRACKED REAGENTS METHODS
-- ============================================================================

function HDG.Store:AddTrackedReagent(itemID, name, targetQty)
    local CONFIG = privateDB.CONFIG
    if CONFIG then
        local count = 0
        for _ in pairs(self.state.trackedReagents) do count = count + 1 end
        if count >= CONFIG.MAX_TRACKED_REAGENTS then
            print("|cFFFF0000[HDG]|r Reagent tracking limit reached (max " .. CONFIG.MAX_TRACKED_REAGENTS .. "). Remove some items first.")
            return false
        end
    end
    self.state.trackedReagents[itemID] = {
        name = name,
        targetQty = targetQty or 0,
        addedTime = time(),
    }
    self:QueueSave()
    return true
end

function HDG.Store:RemoveTrackedReagent(itemID)
    self.state.trackedReagents[itemID] = nil
    self:QueueSave()
end

function HDG.Store:UpdateTrackedReagent(itemID, targetQty)
    if self.state.trackedReagents[itemID] then
        self.state.trackedReagents[itemID].targetQty = targetQty
        self:QueueSave()
    end
end

function HDG.Store:ClearTrackedReagents()
    self.state.trackedReagents = {}
    self:QueueSave()
end

-- ============================================================================
-- USER NOTES METHODS
-- ============================================================================

function HDG.Store:SetUserNote(itemID, text)
    self.state.userNotes[itemID] = { text = text, ts = time() }
    self:QueueSave()
end

function HDG.Store:GetUserNote(itemID)
    return self.state.userNotes[itemID]
end

function HDG.Store:RemoveUserNote(itemID)
    self.state.userNotes[itemID] = nil
    self:QueueSave()
end

function HDG.Store:GetAllUserNotes()
    return self.state.userNotes
end

-- ============================================================================
-- VENDOR NOTES METHODS
-- ============================================================================

function HDG.Store:SetVendorNote(npcID, text)
    self.state.vendorNotes[npcID] = { text = text, ts = time() }
    self:QueueSave()
end

function HDG.Store:GetVendorNote(npcID)
    return self.state.vendorNotes[npcID]
end

function HDG.Store:RemoveVendorNote(npcID)
    self.state.vendorNotes[npcID] = nil
    self:QueueSave()
end

function HDG.Store:GetAllVendorNotes()
    return self.state.vendorNotes
end

-- ============================================================================
-- VENDOR NAME CACHE METHODS
-- ============================================================================

function HDG.Store:SetVendorNameCache(cache)
    self.state.vendorNameCache = cache
    self:QueueSave()
end

-- ============================================================================
-- EXPORT ALL NOTES
-- ============================================================================

function HDG.Store:ExportUserNotes()
    local itemNotes = self.state.userNotes
    local vendorNotes = self.state.vendorNotes
    local itemCount, vendorCount = 0, 0
    for _ in pairs(itemNotes) do itemCount = itemCount + 1 end
    for _ in pairs(vendorNotes) do vendorCount = vendorCount + 1 end
    if itemCount == 0 and vendorCount == 0 then return "No notes to export." end

    local lines = {}
    table.insert(lines, "=== HDG Notes Export ===")
    table.insert(lines, "Date: " .. date("%Y-%m-%d"))
    table.insert(lines, string.format("Total: %d item notes, %d vendor notes", itemCount, vendorCount))
    table.insert(lines, "")

    if vendorCount > 0 then
        table.insert(lines, "--- Vendor Notes ---")
        table.insert(lines, "")
        local sorted = {}
        for npcID, note in pairs(vendorNotes) do
            table.insert(sorted, { npcID = npcID, text = note.text, ts = note.ts })
        end
        table.sort(sorted, function(a, b) return a.ts > b.ts end)
        for _, entry in ipairs(sorted) do
            local ve = HDG_VendorDB and HDG_VendorDB[entry.npcID]
            local name = ve and ve[1] or ("NPC #" .. entry.npcID)
            local zone = ve and ve[2] or "Unknown Zone"
            table.insert(lines, string.format("[NPC %d] %s — %s", entry.npcID, name, zone))
            table.insert(lines, "  Note: " .. entry.text)
            if entry.ts and entry.ts > 0 then
                table.insert(lines, "  Added: " .. date("%Y-%m-%d %H:%M", entry.ts))
            end
            table.insert(lines, "")
        end
    end

    if itemCount > 0 then
        table.insert(lines, "--- Item Notes ---")
        table.insert(lines, "")
        local sorted = {}
        for itemID, note in pairs(itemNotes) do
            table.insert(sorted, { itemID = itemID, text = note.text, ts = note.ts })
        end
        table.sort(sorted, function(a, b) return a.ts > b.ts end)
        for _, entry in ipairs(sorted) do
            local name = HDG_Data and HDG_Data.GetCachedItemName(entry.itemID) or ("Unknown Item")
            table.insert(lines, string.format("[%d] %s", entry.itemID, name))
            table.insert(lines, "  Note: " .. entry.text)
            if entry.ts and entry.ts > 0 then
                table.insert(lines, "  Added: " .. date("%Y-%m-%d %H:%M", entry.ts))
            end
            table.insert(lines, "")
        end
    end

    return table.concat(lines, "\n")
end

-- ============================================================================
-- PROJECTS — house/room CRUD + UI state
-- ============================================================================

function HDG.Store:UpsertProjectHouse(houseID, fields)
    if type(houseID) ~= "string" or type(fields) ~= "table" then return nil end
    local houses = self.state.projects.houses
    houses[houseID] = houses[houseID] or {}
    for k, v in pairs(fields) do
        houses[houseID][k] = v
    end
    self:QueueSave()
    if HDG.EventBus then
        HDG.EventBus:Trigger("HDG_PROJECT_HOUSE_UPDATED", { houseID = houseID })
    end
    return houses[houseID]
end

function HDG.Store:UpsertProjectRoom(roomID, fields)
    if type(roomID) ~= "string" or type(fields) ~= "table" then return nil end
    local rooms = self.state.projects.rooms
    rooms[roomID] = rooms[roomID] or { cell = { x = 0, y = 0, rotation = 0 } }
    for k, v in pairs(fields) do
        rooms[roomID][k] = v
    end
    self:QueueSave()
    if HDG.EventBus then
        HDG.EventBus:Trigger("HDG_PROJECT_ROOM_UPDATED", { roomID = roomID })
    end
    return rooms[roomID]
end

function HDG.Store:DeleteProjectRoom(roomID)
    if type(roomID) ~= "string" then return end
    self.state.projects.rooms[roomID] = nil
    self:QueueSave()
    if HDG.EventBus then
        HDG.EventBus:Trigger("HDG_PROJECT_ROOM_UPDATED", { roomID = roomID, deleted = true })
    end
end

function HDG.Store:SetProjectUI(field, value)
    if type(field) ~= "string" then return end
    self.state.projects.ui[field] = value
    self:QueueSave()
end

-- ============================================================================
-- LUMBER COORDINATES METHODS
-- ============================================================================

function HDG.Store:AddLumberCoordinate(node)
    table.insert(self.state.lumberCoordinates, node)
    self:QueueSave()
end

function HDG.Store:SetLumberCoordinates(coordinates)
    self.state.lumberCoordinates = coordinates or {}
    self:QueueSave()
end

function HDG.Store:ClearLumberCoordinates()
    self.state.lumberCoordinates = {}
    self:QueueSave()
end

-- ============================================================================
-- FARMING HISTORY METHODS
-- ============================================================================

function HDG.Store:UpsertFarmingSession(session)
    local sessionId = session.sessionId
    if sessionId then
        for _, entry in ipairs(self.state.farmingHistory) do
            if entry.sessionId == sessionId then
                entry.collected = session.collected or entry.collected
                entry.duration = session.duration or entry.duration
                entry.perHour = session.perHour or entry.perHour
                entry.timestamp = session.timestamp or entry.timestamp
                entry.zone = session.zone or entry.zone
                self:QueueSave()
                return
            end
        end
    end
    table.insert(self.state.farmingHistory, 1, session)
    local maxFarming = privateDB.CONFIG and privateDB.CONFIG.MAX_FARMING_HISTORY or 100
    while #self.state.farmingHistory > maxFarming do
        table.remove(self.state.farmingHistory)
    end
    self:QueueSave()
end

function HDG.Store:ClearFarmingHistory()
    self.state.farmingHistory = {}
    self:QueueSave()
end

function HDG.Store:DeleteMostRecentFarmingSession()
    if #self.state.farmingHistory > 0 then
        table.remove(self.state.farmingHistory, 1)  -- Most recent is at index 1
        self:QueueSave()
        return true
    end
    return false
end

-- ============================================================================
-- VENDOR SHOPPING LIST METHODS (Multi-List)
-- ============================================================================

local function GenerateListId()
    local chars = "abcdefghijklmnopqrstuvwxyz0123456789"
    local id = {}
    for i = 1, 6 do
        local r = math.random(1, #chars)
        id[i] = chars:sub(r, r)
    end
    return table.concat(id)
end

function HDG.Store:GetActiveShoppingList()
    local id = self.state.activeShoppingListId or "default"
    local list = self.state.vendorShoppingLists[id]
    if not list then
        list = { name = "Shopping List", items = {}, meta = {}, createdAt = time() }
        self.state.vendorShoppingLists[id] = list
    end
    return list, id
end

function HDG.Store:AddToVendorShoppingList(itemID, npcID, qty)
    if not itemID then return false end
    local amount = qty or 1
    local items = self:GetActiveShoppingList().items
    for _, entry in ipairs(items) do
        local match = npcID and (entry.itemID == itemID and entry.npcID == npcID)
                           or (entry.itemID == itemID and not entry.npcID)
        if match then
            entry.qty = (entry.qty or 1) + amount
            self:QueueSave()
            return true
        end
    end
    table.insert(items, { itemID = itemID, npcID = npcID, qty = amount, addedAt = time() })
    self:QueueSave()
    return true
end

function HDG.Store:RemoveFromVendorShoppingList(itemID, npcID)
    local items = self:GetActiveShoppingList().items
    for i = #items, 1, -1 do
        local entry = items[i]
        local match = npcID and (entry.itemID == itemID and entry.npcID == npcID)
                           or (entry.itemID == itemID and not entry.npcID)
        if match then
            local newQty = (entry.qty or 1) - 1
            if newQty <= 0 then
                table.remove(items, i)
            else
                entry.qty = newQty
            end
            self:QueueSave()
            return true
        end
    end
    return false
end

function HDG.Store:ClearVendorShoppingList()
    local list = self:GetActiveShoppingList()
    wipe(list.items)
    wipe(list.meta)
    self:QueueSave()
end

function HDG.Store:SetVendorShoppingListMeta(meta)
    self:GetActiveShoppingList().meta = meta or {}
    self:QueueSave()
end

function HDG.Store:IsOnVendorShoppingList(itemID, npcID)
    local items = self:GetActiveShoppingList().items
    for _, entry in ipairs(items) do
        local match = npcID and (entry.itemID == itemID and entry.npcID == npcID)
                           or (entry.itemID == itemID and not entry.npcID)
        if match then return true end
    end
    return false
end

function HDG.Store:PruneCollectedFromVendorShoppingList()
    if not HDG_Data or not HDG_Data.IsItemCollected then return end
    local items = self:GetActiveShoppingList().items
    local changed = false
    for i = #items, 1, -1 do
        if HDG_Data.IsItemCollected(items[i].itemID) then
            table.remove(items, i)
            changed = true
        end
    end
    if changed then self:QueueSave() end
end

function HDG.Store:GetVendorShoppingListCount()
    local items = self:GetActiveShoppingList().items
    local total = 0
    for _, entry in ipairs(items) do
        total = total + (entry.qty or 1)
    end
    return total
end

-- Multi-list management methods

function HDG.Store:CreateShoppingList(name)
    local id = GenerateListId()
    local count = 0
    for _ in pairs(self.state.vendorShoppingLists) do count = count + 1 end
    self.state.vendorShoppingLists[id] = {
        name = name or ("List " .. (count + 1)),
        items = {},
        meta = {},
        createdAt = time(),
    }
    self:QueueSave()
    return id
end

function HDG.Store:SetActiveShoppingList(id)
    if self.state.vendorShoppingLists[id] then
        self.state.activeShoppingListId = id
        self:QueueSave()
        return true
    end
    return false
end

function HDG.Store:GetShoppingLists()
    local result = {}
    for id, list in pairs(self.state.vendorShoppingLists) do
        local count = 0
        for _, entry in ipairs(list.items) do count = count + (entry.qty or 1) end
        result[#result + 1] = { id = id, name = list.name, count = count, createdAt = list.createdAt or 0 }
    end
    table.sort(result, function(a, b) return a.createdAt < b.createdAt end)
    return result
end

function HDG.Store:RenameShoppingList(id, newName)
    local list = self.state.vendorShoppingLists[id]
    if list and newName and newName ~= "" then
        list.name = newName
        self:QueueSave()
        return true
    end
    return false
end

function HDG.Store:DeleteShoppingList(id)
    -- Can't delete last list
    local count = 0
    for _ in pairs(self.state.vendorShoppingLists) do count = count + 1 end
    if count <= 1 then return false end

    self.state.vendorShoppingLists[id] = nil

    -- If deleting active list, switch to another
    if self.state.activeShoppingListId == id then
        for otherId in pairs(self.state.vendorShoppingLists) do
            self.state.activeShoppingListId = otherId
            break
        end
    end

    self:QueueSave()
    return true
end

-- ============================================================================
-- CUSTOM STYLES METHODS
-- ============================================================================

function HDG.Store:SaveCustomStyle(key, def)
    if not key then
        key = "custom-" .. time() .. "-" .. math.random(1000, 9999)
    end
    self.state.customStyles[key] = DeepCopy(def)
    -- Add to order if not already present
    local found = false
    for _, k in ipairs(self.state.customStyleOrder) do
        if k == key then found = true; break end
    end
    if not found then
        table.insert(self.state.customStyleOrder, key)
    end
    self:QueueSave()
    return key
end

function HDG.Store:DeleteCustomStyle(key)
    self.state.customStyles[key] = nil
    for i = #self.state.customStyleOrder, 1, -1 do
        if self.state.customStyleOrder[i] == key then
            table.remove(self.state.customStyleOrder, i)
        end
    end
    self:QueueSave()
end

function HDG.Store:GetCustomStyles()
    return self.state.customStyles, self.state.customStyleOrder
end

-- ============================================================================
-- LEGACY DISPATCH SUPPORT (for backward compatibility during transition)
-- ============================================================================

HDG.Store.reducers = {}

function HDG.Store:RegisterReducer(action, reducerFn)
    self.reducers[action] = reducerFn
end

function HDG.Store:Dispatch(action, payload)
    if self.state.config.debug then
        print("|cFF14b8a6[HDG Store]|r Dispatching:", action)
    end

    -- Map dispatch calls to new methods
    if action == "SET_CONFIG" and payload.key then
        self:SetConfig(payload.key, payload.value)
    elseif action == "SET_THEME" then
        self:SetTheme(payload.theme)
    elseif action == "SET_FRAME_POSITION" then
        self:SetUI("framePosition", payload.position)
    elseif action == "SET_ACTIVE_TAB" then
        self:SetUI("activeTab", payload.tab)
    elseif action == "SET_PROFESSION_FILTER" then
        self:SetUI("professionFilter", payload.filter)
    elseif action == "SET_CHARACTER_PROFESSION_FILTER" then
        if payload.charKey then
            self.state.ui.characterProfessionFilter = self.state.ui.characterProfessionFilter or {}
            self.state.ui.characterProfessionFilter[payload.charKey] = payload.filter
        end
        self:SetUI("professionFilter", payload.filter)
    elseif action == "SET_COLLAPSED_MATERIAL" then
        self.state.ui.collapsedMaterials[payload.category] = payload.collapsed
        self:QueueSave()
    elseif action == "SET_TRAINER_COLLAPSED" then
        self.state.ui.trainersCollapsed[payload.profession] = payload.collapsed
        self:QueueSave()
    elseif action == "SET_LUMBER_PINNED" then
        self:SetUI("lumberPinned", payload.pinned)
    elseif action == "SET_LUMBER_POSITION" then
        self:SetUI("pinnedLumberPosition", payload.position)
    elseif action == "SET_MINIMAP" and payload.key then
        self:SetMinimap(payload.key, payload.value)
    elseif action == "UPDATE_LAYOUT" then
        self:UpdateLayout(payload.frame, {
            width = payload.width, height = payload.height,
            point = payload.point, relPoint = payload.relPoint,
            x = payload.x, y = payload.y,
        })
    elseif action == "ADD_TO_CRAFTING_QUEUE" then
        self:AddToCraftingQueue(payload.item)
        if HDG_LumberTracker and HDG_LumberTracker.InvalidateLumberNeeds then HDG_LumberTracker.InvalidateLumberNeeds() end
        if HDG_MaterialsList and HDG_MaterialsList.IsShown and HDG_MaterialsList.IsShown() then HDG_MaterialsList.Refresh() end
    elseif action == "REMOVE_FROM_CRAFTING_QUEUE" then
        self:RemoveFromCraftingQueue(payload.itemID)
        if HDG_LumberTracker and HDG_LumberTracker.InvalidateLumberNeeds then HDG_LumberTracker.InvalidateLumberNeeds() end
        if HDG_MaterialsList and HDG_MaterialsList.IsShown and HDG_MaterialsList.IsShown() then HDG_MaterialsList.Refresh() end
    elseif action == "UPDATE_CRAFTING_QUEUE_ITEM_QTY" then
        self:UpdateCraftingQueueItemQty(payload.itemID, payload.quantity)
        if HDG_LumberTracker and HDG_LumberTracker.InvalidateLumberNeeds then HDG_LumberTracker.InvalidateLumberNeeds() end
        if HDG_MaterialsList and HDG_MaterialsList.IsShown and HDG_MaterialsList.IsShown() then HDG_MaterialsList.Refresh() end
    elseif action == "CLEAR_CRAFTING_QUEUE" then
        self:ClearCraftingQueue()
        if HDG_LumberTracker and HDG_LumberTracker.InvalidateLumberNeeds then HDG_LumberTracker.InvalidateLumberNeeds() end
        if HDG_MaterialsList and HDG_MaterialsList.IsShown and HDG_MaterialsList.IsShown() then HDG_MaterialsList.Refresh() end
    elseif action == "UPDATE_CHARACTER_DATA" then
        self:UpdateCharacterData(payload.charKey, payload)
    elseif action == "REMOVE_CHARACTER" then
        self:RemoveCharacter(payload.charKey)
    elseif action == "ADD_CRAFTING_HISTORY" then
        self:AddCraftingHistory(payload)
    elseif action == "CLEAR_CRAFTING_HISTORY" then
        self:ClearCraftingHistory()
    elseif action == "ADD_TRACKED_REAGENT" then
        self:AddTrackedReagent(payload.itemID, payload.name, payload.targetQty)
    elseif action == "REMOVE_TRACKED_REAGENT" then
        self:RemoveTrackedReagent(payload.itemID)
    elseif action == "UPDATE_TRACKED_REAGENT" then
        self:UpdateTrackedReagent(payload.itemID, payload.targetQty)
    elseif action == "CLEAR_TRACKED_REAGENTS" then
        self:ClearTrackedReagents()
    elseif action == "ADD_LUMBER_COORDINATE" then
        self:AddLumberCoordinate(payload.node)
    elseif action == "SET_LUMBER_COORDINATES" then
        self:SetLumberCoordinates(payload.coordinates)
    elseif action == "CLEAR_LUMBER_COORDINATES" then
        self:ClearLumberCoordinates()
    elseif action == "UPSERT_FARMING_SESSION" then
        self:UpsertFarmingSession(payload.session)
    elseif action == "CLEAR_FARMING_HISTORY" then
        self:ClearFarmingHistory()
    elseif action == "DELETE_MOST_RECENT_FARMING_SESSION" then
        self:DeleteMostRecentFarmingSession()
    elseif action == "ADD_TO_VENDOR_SHOPPING_LIST" then
        self:AddToVendorShoppingList(payload.itemID, payload.npcID)
    elseif action == "REMOVE_FROM_VENDOR_SHOPPING_LIST" then
        self:RemoveFromVendorShoppingList(payload.itemID, payload.npcID)
    elseif action == "CLEAR_VENDOR_SHOPPING_LIST" then
        self:ClearVendorShoppingList()
    else
        -- Fallback to legacy reducer if exists
        local reducer = self.reducers[action]
        if reducer then
            local newState = reducer(self.state, payload)
            if newState then self.state = newState end
            self:QueueSave()
        elseif self.state.config.debug then
            print("|cFFdc322f[HDG Store]|r Unknown action:", action)
        end
    end

    -- Trigger EventBus for listeners (backward compat)
    if HDG.EventBus then
        HDG.EventBus:Trigger("HDG_STATE_CHANGED", { action = action, state = self.state })
    end
end

-- Expose DeepCopy for other modules
HDG.Store.DeepCopy = DeepCopy
