local addonName, ns = ...

------------------------------------------------------------
-- Ways into the shopping list from the rest of the game:
--  * Ctrl+Alt+Click on catalog entries and item/decor links
--  * /hh add <item link>   (Command.lua delegates here)
------------------------------------------------------------

local DECOR_ENTRY_TYPE = 1
local MAX_DECOR_ID = 50000  -- same ceiling the editor's DumpCatalog uses
local SCAN_BATCH = 500

------------------------------------------------------------
-- item → decor lookup
------------------------------------------------------------

-- Direct API names to probe; pruned in-game once the real one is known.
local DIRECT_LOOKUP_FNS = {
    "GetCatalogEntryInfoByItem", -- confirmed live via /hh catalogprobe
    "GetCatalogEntryForItem",
    "GetCatalogEntryInfoForItem",
    "GetDecorForItem",
}

local function probeDirectLookup(itemID)
    if not C_HousingCatalog then return nil end
    for _, fname in ipairs(DIRECT_LOOKUP_FNS) do
        local fn = C_HousingCatalog[fname]
        if fn then
            local ok, result = pcall(fn, itemID)
            if ok and result then
                if type(result) == "number" then return result end
                if type(result) == "table" then
                    local id = result.decorID or result.entryID or result.recordID
                    if id then return id end
                end
            end
        end
    end
    return nil
end

local function mapStore()
    local build = "unknown"
    if GetBuildInfo then
        local _, buildNumber = GetBuildInfo()
        build = buildNumber or "unknown"
    end
    local store = ns.db.itemDecorMap
    if not store or store.build ~= build then
        store = { build = build, map = {}, scanned = 0 }
        ns.db.itemDecorMap = store
    end
    return store
end

local scanTicker

local function StartBackgroundScan(store)
    if scanTicker or store.scanned >= MAX_DECOR_ID then return end
    if not (C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID
        and C_Timer and C_Timer.NewTicker) then
        return
    end
    ns.Print("Building the decor item index in the background...")
    scanTicker = C_Timer.NewTicker(0.1, function()
        local startID = store.scanned + 1
        local endID = math.min(startID + SCAN_BATCH - 1, MAX_DECOR_ID)
        for decorID = startID, endID do
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                DECOR_ENTRY_TYPE, decorID, false)
            if ok and info and info.itemID then
                store.map[info.itemID] = decorID
            end
        end
        store.scanned = endID
        if endID >= MAX_DECOR_ID then
            scanTicker:Cancel()
            scanTicker = nil
            ns.Print("Decor item index complete.")
        end
    end)
end

function ns.GetDecorForItem(itemID)
    local direct = probeDirectLookup(itemID)
    if direct then return direct end
    local store = mapStore()
    local cached = store.map[itemID]
    if cached then return cached end
    StartBackgroundScan(store)
    return nil
end

------------------------------------------------------------
-- item → dye lookup
--
-- Housing dye ITEMS are distinct per shade and named "<dye name> Dye"
-- (e.g. "Lush Green Dye" for our "Lush Green" record). ns.DYES only carries
-- the shared dye-pigment itemID, not each dye item's own id, so a clicked dye
-- item is matched to its shade by name (dye names are unique).
------------------------------------------------------------

local dyeNameIndex

local function dyeByName(name)
    if not dyeNameIndex then
        dyeNameIndex = {}
        for id, dye in pairs(ns.DYES or {}) do
            if dye.name then dyeNameIndex[dye.name:lower()] = id end
        end
    end
    return dyeNameIndex[name:lower()]
end

local function itemName(itemID)
    if C_Item and type(C_Item.GetItemNameByID) == "function" then
        local ok, name = pcall(C_Item.GetItemNameByID, itemID)
        if ok and type(name) == "string" then return name end
    end
    if type(GetItemInfo) == "function" then
        local name = GetItemInfo(itemID)
        if type(name) == "string" then return name end
    end
    return nil
end

function ns.GetDyeForItem(itemID)
    local name = itemName(itemID)
    if not name then return nil end
    -- Drop the trailing " Dye" suffix the dye items carry. Some dye names
    -- (Gold, Copper, Bronze, ...) are plain nouns shared with unrelated
    -- decor items, so only treat this as a dye when the suffix was actually
    -- present and stripped -- an unmodified name must fall through to the
    -- decor lookup instead.
    local base, replaced = name:gsub("%s+[Dd]ye$", "")
    if replaced == 0 then return nil end
    return dyeByName(base)
end

------------------------------------------------------------
-- dye → item lookup (the reverse direction, for availability counts)
--
-- ns.DYES carries only the shared dye-PIGMENT itemID: one pigment makes many
-- shades (Green Dye Pigment 274470 covers Lush Green, Emerald Dreaming and a
-- dozen more). Counting that would report the same total for every shade of a
-- pigment and green-check dyes the player owns none of. Resolve the shade's
-- own item ("<dye name> Dye") instead, remember it in SavedVariables, and
-- report unknown when it can't be resolved -- the display assumes 0, which
-- beats a pigment count we know is wrong.
------------------------------------------------------------

local function dyeItemStore()
    if not ns.db then return nil end
    ns.db.dyeItemMap = ns.db.dyeItemMap or {}
    return ns.db.dyeItemMap
end

-- Seeded from the click path, which already knows the real item id.
function ns.RememberDyeItem(dyeID, itemID)
    local store = dyeItemStore()
    if store and type(dyeID) == "number" and type(itemID) == "number" then
        store[dyeID] = itemID
    end
end

-- Name lookups resolve only once the client has seen the item, so this can
-- return nil early in a session and succeed later.
local function itemIDByName(name)
    local fn = (C_Item and C_Item.GetItemInfoInstant) or GetItemInfoInstant
    if type(fn) ~= "function" then return nil end
    local ok, id = pcall(fn, name)
    if ok and type(id) == "number" then return id end
    return nil
end

function ns.GetDyeItemID(dyeID)
    local store = dyeItemStore()
    local cached = store and store[dyeID]
    if cached then return cached end

    local dye = ns.DYES and ns.DYES[dyeID]
    if not (dye and dye.name) then return nil end

    local itemID = itemIDByName(dye.name .. " Dye")
    if itemID then
        ns.RememberDyeItem(dyeID, itemID)
        return itemID
    end
    return nil
end

------------------------------------------------------------
-- Item links (chat shift-click and /hh add)
------------------------------------------------------------

local function addDecor(decorID)
    local name = ns.ResolveName and ns.ResolveName("DECOR", decorID) or nil
    ns.ShoppingList.Increment("DECOR", decorID, 1, name)
end

local function addDye(dyeID)
    local dye = ns.DYES and ns.DYES[dyeID]
    ns.ShoppingList.Increment("DYE", dyeID, 1, dye and dye.name or nil)
end

-- Accepts either a full hyperlink (|H...|h) or a bare link payload, of two
-- kinds confirmed on the live client via /hh linkprobe:
--   * housingdecor:<decorID>  — the decor record id directly (chat links,
--     the catalog "link" feature). No item lookup needed.
--   * item:<itemID>           — a normal inventory item; mapped to a decor
--     via GetCatalogEntryInfoByItem / the background index.
-- Returns true if the link was a kind we handle (even if the item is not a
-- known decor), false otherwise so callers can pass unknown links through.
function ns:AddItemLink(link)
    if type(link) ~= "string" then return false end
    local payload = link:match("|H(.-)|h") or link
    local linkType, rest = payload:match("^(%a+):(.+)$")
    if not linkType then return false end

    if linkType == "housingdecor" then
        local decorID = tonumber(rest:match("^(%d+)"))
        if not decorID then return false end
        addDecor(decorID)
        return true
    elseif linkType == "item" then
        local itemID = tonumber(rest:match("^(%d+)"))
        if not itemID then return false end
        -- Dye first: it's a cheap name lookup, and it avoids kicking off the
        -- decor background scan for an item that is actually a dye.
        local dyeID = ns.GetDyeForItem(itemID)
        if dyeID then
            -- This click is the cheapest source of truth for the shade's own
            -- item id; cache it so availability can count it later.
            ns.RememberDyeItem(dyeID, itemID)
            addDye(dyeID)
            return true
        end
        local decorID = ns.GetDecorForItem(itemID)
        if decorID then
            addDecor(decorID)
        else
            ns.Print("That item isn't a known housing decor or dye"
                .. " (the decor index may still be building).")
        end
        return true
    end
    return false
end

------------------------------------------------------------
-- Add gesture + catalog entry buttons
------------------------------------------------------------

-- Ctrl+Alt+Click is the "add to shopping list" gesture. Ctrl+Alt has no stock
-- item binding, so it works anywhere and never collides with Blizzard's own
-- gestures — plain shift (link into chat / find-the-vendor objective) and
-- ctrl (dress-up/preview) both stay untouched.
local function isAddModifier()
    return IsControlKeyDown and IsAltKeyDown
        and IsControlKeyDown() and IsAltKeyDown()
end

-- A clicked catalog entry button carries its decor in
-- elementData.entryVariantID = { entryType, recordID, variantIdentifier }
-- (confirmed via /hh mouseprobe). entryType 1 is decor; recordID is the
-- decor record id we store.
local function decorFromButton(button)
    if type(button) ~= "table" or type(button.GetElementData) ~= "function" then
        return nil
    end
    local ok, data = pcall(button.GetElementData, button)
    if not ok or type(data) ~= "table" then return nil end
    local variant = data.entryVariantID
    if type(variant) ~= "table" then return nil end
    if variant.entryType == DECOR_ENTRY_TYPE and type(variant.recordID) == "number" then
        return variant.recordID
    end
    return nil
end

-- Ctrl+Alt+Click handler for a catalog entry button. Blizzard's own click
-- (select/preview) still runs — this is a post-hook that adds on top.
function ns:AddFromCatalogButton(button)
    if not isAddModifier() then return false end
    local decorID = decorFromButton(button)
    if not decorID then return false end
    addDecor(decorID)
    return true
end

local function onCatalogEntryClick(button)
    ns:AddFromCatalogButton(button)
end

local function catalogScrollBox()
    local dash = _G.HousingDashboardFrame
    local content = dash and dash.CatalogContent
    local container = content and content.OptionsContainer
    return container and container.ScrollBox or nil
end

-- Catalog entry buttons run their own OnClick and never reach the link hooks,
-- and their mixin methods are copied per-frame (so hooking the mixin table is
-- unreliable). Instead HookScript each pooled ScrollBox button once; the pool
-- is small and reused, so a frame stays hooked across data changes.
local function tagCatalogButtons()
    local scrollBox = catalogScrollBox()
    if not scrollBox or type(scrollBox.ForEachFrame) ~= "function" then return end
    pcall(scrollBox.ForEachFrame, scrollBox, function(button)
        if type(button) == "table" and not button.hhClickHooked
            and type(button.HookScript) == "function" then
            button.hhClickHooked = true
            pcall(button.HookScript, button, "OnClick", onCatalogEntryClick)
        end
    end)
end

-- Hook the catalog once the dashboard frames exist. Tag the current buttons,
-- then keep them tagged as the pool grows (ScrollBox:Update on scroll/refresh)
-- and whenever the dashboard is shown.
local function installCatalogEntryHook()
    local scrollBox = catalogScrollBox()
    if not scrollBox then return false end
    tagCatalogButtons()
    if not scrollBox.hhUpdateHooked and hooksecurefunc
        and type(scrollBox.Update) == "function" then
        scrollBox.hhUpdateHooked = true
        hooksecurefunc(scrollBox, "Update", tagCatalogButtons)
    end
    local dash = _G.HousingDashboardFrame
    if dash and not dash.hhShowHooked and type(dash.HookScript) == "function" then
        dash.hhShowHooked = true
        pcall(dash.HookScript, dash, "OnShow", tagCatalogButtons)
    end
    return true
end
ns._installCatalogEntryHook = installCatalogEntryHook

------------------------------------------------------------
-- Hooks
------------------------------------------------------------

function ns:InitCatalogHook()
    -- Item links (a craftable decor in bags/chat) route through the universal
    -- HandleModifiedItemClick; AddItemLink maps the item to its decor.
    if hooksecurefunc and type(_G.HandleModifiedItemClick) == "function" then
        hooksecurefunc("HandleModifiedItemClick", function(link)
            if isAddModifier() then
                ns:AddItemLink(link)
            end
        end)
    end

    -- Housing decor links (|Hhousingdecor:<id>|h) are hyperlinks, not item
    -- links: a modified click on one routes to SetItemRef, NOT
    -- HandleModifiedItemClick (confirmed via /hh linkprobe →
    -- "housingdecor:25566"). Catch those here. Blizzard's stock behaviour
    -- still runs; we add on top of it.
    if hooksecurefunc and type(_G.SetItemRef) == "function" then
        hooksecurefunc("SetItemRef", function(link)
            if type(link) == "string" and link:find("housingdecor:")
                and isAddModifier() then
                ns:AddItemLink(link)
            end
        end)
    end

    -- Catalog entry buttons: Ctrl+Alt+Click adds the decor. The buttons live
    -- in the load-on-demand Blizzard_HousingDashboard, so install now if it's
    -- already loaded, otherwise wait for its ADDON_LOADED.
    if not installCatalogEntryHook() then
        local watcher = CreateFrame("Frame")
        watcher:RegisterEvent("ADDON_LOADED")
        watcher:SetScript("OnEvent", function(self, _, name)
            if name == "Blizzard_HousingDashboard" and installCatalogEntryHook() then
                self:UnregisterEvent("ADDON_LOADED")
            end
        end)
    end
end
