EbonholdHub.ItemCatalog = {}

local EL = EbonholdHub.EquipmentLoadout
local AD = EbonholdHub.AffixData

-- ~1 MB / 14k rows — loaded on first gear picker use, not at login (see EbonholdHub.toc).
local DATA
local DATA_PATH = "Interface\\AddOns\\EbonholdHub\\modules\\data\\ItemCatalogData.lua"
local dataLoaded = false

local scanTip
local byId
local indexReady = false
local enrichTick
local enrichJobs = {}
local MAX_LOOKUP_FRAMES = 45
local MAX_CATALOG_RESULTS = 80
local MIN_CATALOG_QUERY = 2

local QUALITY_HEX = {
    [0] = "9d9d9d",
    [1] = "ffffff",
    [2] = "1eff00",
    [3] = "0070dd",
    [4] = "a335ee",
    [5] = "ff8000",
}

local function EnsureCache()
    EbonholdHubDB.itemCache = EbonholdHubDB.itemCache or {}
end

local function CloneEntry(entry)
    if not entry then return nil end
    return {
        itemId  = entry.itemId,
        name    = entry.name,
        link    = entry.link,
        quality = entry.quality,
        texture = entry.texture,
        source  = entry.source,
    }
end

local function RowToEntry(row, source)
    return {
        itemId  = row.itemId,
        name    = row.name,
        quality = row.quality or 0,
        source  = source or "catalog",
    }
end

local function EnsureDataLoaded()
    if dataLoaded then return end
    dataLoaded = true
    EbonholdHub.ItemCatalogData = EbonholdHub.ItemCatalogData or {}
    local fn = loadfile and loadfile(DATA_PATH)
    if fn then
        fn()
    end
    DATA = EbonholdHub.ItemCatalogData
end

local function EnsureIndex()
    EnsureDataLoaded()
    if indexReady then return end
    byId = {}
    if DATA and DATA.ITEMS then
        for _, row in ipairs(DATA.ITEMS) do
            byId[row.itemId] = row
        end
    end
    indexReady = true
end

function EbonholdHub.ItemCatalog.GetCatalogCount()
    EnsureIndex()
    return (DATA and DATA.ITEM_COUNT) or (DATA and DATA.ITEMS and #DATA.ITEMS) or 0
end

function EbonholdHub.ItemCatalog.EntryFromCatalog(itemId)
    EnsureIndex()
    itemId = tonumber(itemId)
    if not itemId then return nil end
    local row = byId[itemId]
    if not row then return nil end
    return RowToEntry(row)
end

function EbonholdHub.ItemCatalog.SearchStaticCatalog(query)
    EnsureIndex()
    if not DATA or not DATA.ITEMS then return {} end
    query = strtrim(query or ""):lower()
    if query == "" or #query < MIN_CATALOG_QUERY then return {} end

    local results = {}
    for _, row in ipairs(DATA.ITEMS) do
        if row.name and row.name:lower():find(query, 1, true) then
            results[#results + 1] = RowToEntry(row)
            if #results >= MAX_CATALOG_RESULTS then break end
        end
    end
    return results
end

local function MergeResults(lists)
    local seen = {}
    local merged = {}
    for _, list in ipairs(lists) do
        for _, entry in ipairs(list) do
            if entry and entry.itemId and not seen[entry.itemId] then
                seen[entry.itemId] = true
                merged[#merged + 1] = CloneEntry(entry)
            end
        end
    end
    table.sort(merged, function(a, b)
        return (a.name or ""):lower() < (b.name or ""):lower()
    end)
    return merged
end

function EbonholdHub.ItemCatalog.CacheEntry(entry)
    if not entry or not entry.itemId or not entry.name then return end
    EnsureCache()
    EbonholdHubDB.itemCache[tostring(entry.itemId)] = CloneEntry(entry)
end

function EbonholdHub.ItemCatalog.ImportBuildItems()
    local loadout = EL.GetActive()
    if type(loadout) ~= "table" then return end
    for _, entry in pairs(loadout) do
        EbonholdHub.ItemCatalog.CacheEntry(entry)
    end
end

function EbonholdHub.ItemCatalog.BuildItemLink(entry)
    if not entry or not entry.itemId or not entry.name then return nil end
    if entry.link then return entry.link end
    local hex = QUALITY_HEX[entry.quality or 0] or "ffffff"
    local safeName = entry.name:gsub("%[", ""):gsub("%]", "")
    return string.format("|cff%s|Hitem:%d:0:0:0:0:0:0:0|h[%s]|h|r", hex, entry.itemId, safeName)
end

function EbonholdHub.ItemCatalog.GetItemHyperlink(entry)
    if not entry then return nil end
    if entry.link then return entry.link end
    if entry.itemId then
        return EbonholdHub.ItemCatalog.BuildItemLink(entry) or ("item:" .. entry.itemId)
    end
    return nil
end

function EbonholdHub.ItemCatalog.EnrichEntry(entry)
    if not entry or not entry.itemId then return entry end
    local name, link, quality, _, _, _, _, _, _, texture = GetItemInfo(entry.itemId)
    if name then
        entry.name = name
        if link then entry.link = link end
        entry.quality = quality or entry.quality or 0
        if texture then entry.texture = texture end
    end
    if not entry.link then
        entry.link = EbonholdHub.ItemCatalog.BuildItemLink(entry)
    end
    return entry
end

function EbonholdHub.ItemCatalog.IsClientLoaded(entry)
    if not entry or not entry.itemId then return false end
    local name, link, _, _, _, _, _, _, _, texture = GetItemInfo(entry.itemId)
    return name ~= nil and link ~= nil and texture ~= nil
end

function EbonholdHub.ItemCatalog.EntryFromItemId(itemId)
    itemId = tonumber(itemId)
    if not itemId then return nil end

    local entry = EbonholdHub.ItemCatalog.EntryFromCatalog(itemId)
    if not entry then
        entry = { itemId = itemId, quality = 0 }
    end
    return EbonholdHub.ItemCatalog.EnrichEntry(entry)
end

function EbonholdHub.ItemCatalog.PrimeItemId(itemId)
    if not scanTip then
        scanTip = CreateFrame("GameTooltip", "EbonholdHubItemScanTip", UIParent, "GameTooltipTemplate")
    end
    scanTip:SetOwner(UIParent, "ANCHOR_NONE")
    scanTip:ClearLines()
    scanTip:SetHyperlink("item:" .. tostring(itemId))
end

function EbonholdHub.ItemCatalog.RequestItemById(itemId, callback)
    itemId = tonumber(itemId)
    if not itemId or not callback then return end

    local entry = EbonholdHub.ItemCatalog.EntryFromItemId(itemId)
    if entry and EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
        EbonholdHub.ItemCatalog.CacheEntry(entry)
        callback(entry)
        return
    end

    EbonholdHub.ItemCatalog.PrimeItemId(itemId)
    local frames = 0
    local tick = CreateFrame("Frame")
    tick:SetScript("OnUpdate", function(self)
        frames = frames + 1
        entry = EbonholdHub.ItemCatalog.EntryFromItemId(itemId)
        if entry and EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
            self:SetScript("OnUpdate", nil)
            EbonholdHub.ItemCatalog.CacheEntry(entry)
            callback(entry)
        elseif frames >= MAX_LOOKUP_FRAMES then
            self:SetScript("OnUpdate", nil)
            if entry then
                EbonholdHub.ItemCatalog.EnrichEntry(entry)
                EbonholdHub.ItemCatalog.CacheEntry(entry)
                callback(entry)
            else
                callback(nil, itemId)
            end
        end
    end)
end

function EbonholdHub.ItemCatalog.EnrichList(entries, onUpdated)
    if type(entries) ~= "table" or #entries == 0 then
        if onUpdated then onUpdated(false) end
        return
    end

    local jobId = #enrichJobs + 1
    enrichJobs[jobId] = {
        entries = entries,
        onUpdated = onUpdated,
        frames = 0,
    }

    for _, entry in ipairs(entries) do
        EbonholdHub.ItemCatalog.EnrichEntry(entry)
        if entry.itemId and not EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
            EbonholdHub.ItemCatalog.PrimeItemId(entry.itemId)
        end
    end

    if not enrichTick then
        enrichTick = CreateFrame("Frame")
        enrichTick:SetScript("OnUpdate", function(self)
            local anyActive = false
            for id, job in pairs(enrichJobs) do
                job.frames = job.frames + 1
                local stillPending = false
                local changed = false
                for _, entry in ipairs(job.entries) do
                    if entry.itemId and not EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
                        local hadTexture = entry.texture ~= nil
                        EbonholdHub.ItemCatalog.EnrichEntry(entry)
                        if entry.texture and not hadTexture then
                            changed = true
                        end
                        if not EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
                            stillPending = true
                        end
                    end
                end
                if changed and job.onUpdated then
                    job.onUpdated(true)
                end
                if stillPending and job.frames < MAX_LOOKUP_FRAMES then
                    anyActive = true
                else
                    if job.onUpdated then job.onUpdated(true) end
                    enrichJobs[id] = nil
                end
            end
            if not anyActive then
                self:SetScript("OnUpdate", nil)
                enrichTick = nil
            end
        end)
    end
end

local function AddLink(items, seen, link, source)
    if not link then return end
    local entry = EL.ParseItemLink(link)
    if entry and entry.itemId and not seen[entry.itemId] then
        seen[entry.itemId] = true
        entry.source = source
        items[#items + 1] = entry
        EbonholdHub.ItemCatalog.CacheEntry(entry)
    end
end

function EbonholdHub.ItemCatalog.ScanPlayerItems()
    local items = {}
    local seen = {}

    for _, slotDef in ipairs(AD.SLOTS) do
        AddLink(items, seen, GetInventoryItemLink("player", slotDef.invSlot), "equipped")
    end

    if GetContainerNumSlots then
        for bag = 0, 4 do
            local slots = GetContainerNumSlots(bag)
            if slots and slots > 0 then
                for slot = 1, slots do
                    AddLink(items, seen, GetContainerItemLink(bag, slot), "bags")
                end
            end
        end
    end

    return items
end

function EbonholdHub.ItemCatalog.GetAllKnownItems()
    local seen = {}
    local list = {}

    local function merge(entry)
        if entry and entry.itemId and entry.name and not seen[entry.itemId] then
            seen[entry.itemId] = true
            list[#list + 1] = CloneEntry(entry)
        end
    end

    for _, entry in ipairs(EbonholdHub.ItemCatalog.ScanPlayerItems()) do
        merge(entry)
    end

    EnsureCache()
    for _, entry in pairs(EbonholdHubDB.itemCache) do
        merge(entry)
    end

    table.sort(list, function(a, b)
        return (a.name or ""):lower() < (b.name or ""):lower()
    end)
    return list
end

function EbonholdHub.ItemCatalog.ExtractItemId(text)
    text = strtrim(text or "")
    if text == "" then return nil end
    if text:find("|Hitem:") then
        return tonumber(text:match("item:(%d+)"))
    end
    local id = tonumber(text:match("^(%d+)$"))
    if id then return id end
    id = tonumber(text:match("[?&]item=(%d+)")) or tonumber(text:match("/(%d+)"))
    return id
end

function EbonholdHub.ItemCatalog.IsLookupQuery(text)
    text = strtrim(text or "")
    if text == "" then return false end
    if text:find("|Hitem:") then return false end
    if text:match("^%d+$") then return true end
    if text:find("item=") or text:find("wowhead%.") then
        return EbonholdHub.ItemCatalog.ExtractItemId(text) ~= nil
    end
    return false
end

local function SearchPlayerItems(query)
    local qlower = query:lower()
    local results = {}
    for _, entry in ipairs(EbonholdHub.ItemCatalog.GetAllKnownItems()) do
        if entry.name and entry.name:lower():find(qlower, 1, true) then
            results[#results + 1] = entry
        end
    end
    return results
end

function EbonholdHub.ItemCatalog.SearchItems(query)
    query = strtrim(query or "")

    if query == "" then
        return EbonholdHub.ItemCatalog.GetAllKnownItems(), false
    end

    if EbonholdHub.ItemCatalog.IsLookupQuery(query) then
        local itemId = EbonholdHub.ItemCatalog.ExtractItemId(query)
        if not itemId then return {}, false end
        local entry = EbonholdHub.ItemCatalog.EntryFromItemId(itemId)
        if entry and entry.name then
            EbonholdHub.ItemCatalog.CacheEntry(entry)
            if not EbonholdHub.ItemCatalog.IsClientLoaded(entry) then
                EbonholdHub.ItemCatalog.PrimeItemId(itemId)
            end
            return { entry }, not EbonholdHub.ItemCatalog.IsClientLoaded(entry)
        end
        return {
            {
                itemId = itemId,
                name = "Item #" .. itemId,
                pendingLookup = true,
            },
        }, true
    end

    if query:find("|Hitem:") then
        local entry = EL.ParseItemLink(query)
        if entry then
            return { entry }, false
        end
        return {}, false
    end

    local playerResults = SearchPlayerItems(query)
    local catalogResults = EbonholdHub.ItemCatalog.SearchStaticCatalog(query)
    for _, entry in ipairs(catalogResults) do
        EbonholdHub.ItemCatalog.EnrichEntry(entry)
    end
    return MergeResults({ playerResults, catalogResults }), false
end

function EbonholdHub.ItemCatalog.ParseInput(text)
    text = strtrim(text or "")
    if text == "" then return nil end

    if text:find("|Hitem:") then
        return EL.ParseItemLink(text)
    end

    local itemId = EbonholdHub.ItemCatalog.ExtractItemId(text)
    if itemId and EbonholdHub.ItemCatalog.IsLookupQuery(text) then
        return EbonholdHub.ItemCatalog.EntryFromItemId(itemId)
    end

    local results = select(1, EbonholdHub.ItemCatalog.SearchItems(text))
    if #results == 1 then
        return CloneEntry(results[1])
    end

    local lower = text:lower()
    for _, entry in ipairs(results) do
        if entry.name and entry.name:lower() == lower then
            return CloneEntry(entry)
        end
    end

    return nil
end

function EbonholdHub.ItemCatalog.Init()
    -- Lazy: EnsureDataLoaded + EnsureIndex run on first gear/search use.
end
