EbonholdHub.AffixOwnership = {}

local AD = EbonholdHub.AffixData

local STATUS_HAVE    = "have"
local STATUS_ITEM    = "item"
local STATUS_PARTIAL = "partial"
local STATUS_NEED    = "need"

local BORDER_COLORS = {
    [STATUS_HAVE]    = { 0.20, 1.00, 0.20 },
    [STATUS_ITEM]    = { 0.30, 0.60, 1.00 },
    [STATUS_PARTIAL] = { 1.00, 0.85, 0.12 },
    [STATUS_NEED]    = { 1.00, 0.45, 0.45 },
}

local TEXT_COLORS = {
    [STATUS_HAVE]    = { 0.20, 1.00, 0.20 },
    [STATUS_ITEM]    = { 0.30, 0.75, 1.00 },
    [STATUS_PARTIAL] = { 1.00, 0.85, 0.12 },
    [STATUS_NEED]    = { 1.00, 0.45, 0.45 },
}

local STATUS_LABELS = {
    [STATUS_HAVE]    = "Learned",
    [STATUS_ITEM]    = "Have item",
    [STATUS_PARTIAL] = "Lower rank",
    [STATUS_NEED]    = "Need",
}

local cache
local cacheAt = 0
local refreshListeners = {}
local notifyFrame
local notifyElapsed = 0
local notifyPending = false
local NOTIFY_DELAY = 0.45

local function EnsureEntry(table, name)
    local e = table[name]
    if not e then
        e = { ranks = {} }
        table[name] = e
    end
    return e
end

local function RecordRank(table, name, rank, link, location)
    if not name or name == "" then return end
    local e = EnsureEntry(table, name)
    if rank then
        e.ranks[rank] = true
        if link then
            local key = name .. "@" .. rank
            e.itemLinks = e.itemLinks or {}
            e.itemLinks[key] = e.itemLinks[key] or {}
            e.itemLinks[key][#e.itemLinks[key] + 1] = { link = link, location = location }
        end
    else
        e.rankless = true
        if link then
            local key = name .. "@W"
            e.itemLinks = e.itemLinks or {}
            e.itemLinks[key] = e.itemLinks[key] or {}
            e.itemLinks[key][#e.itemLinks[key] + 1] = { link = link, location = location }
        end
    end
end

local function RecordLearned(learned, name, rank, rankless)
    if not name or name == "" then return end
    local e = EnsureEntry(learned, name)
    if rankless or not rank then
        e.rankless = true
    elseif rank then
        e.ranks[rank] = true
    else
        e.any = true
    end
end

local function ImportAffixDexLearned(learned)
    if type(_G.AffixDexCharDB) ~= "table" or type(_G.AffixDexCharDB.learned) ~= "table" then
        return
    end
    local romanToRank = {}
    for rank, roman in ipairs(AD.RANK_ROMAN) do
        romanToRank[roman] = rank
    end
    for name, entry in pairs(_G.AffixDexCharDB.learned) do
        if type(name) == "string" and type(entry) == "table" then
            if entry.rankless then
                RecordLearned(learned, name, nil, true)
            elseif entry.ranks then
                for roman in pairs(entry.ranks) do
                    RecordLearned(learned, name, romanToRank[roman] or AD.RomanToRank(roman), false)
                end
            elseif entry.any then
                RecordLearned(learned, name, nil, false)
            end
        end
    end
end

local function ImportServerLearned(learned)
    local svc = _G.ExtractionService
    if not svc or type(svc.learnedAffixes) ~= "table" then return end
    for _, affix in ipairs(svc.learnedAffixes) do
        if affix.learned and type(affix.name) == "string" and affix.name ~= "" then
            local base, roman = affix.name:match("^(.-)%s+([IVXLCivxlc]+)$")
            if base and roman then
                RecordLearned(learned, base, AD.RomanToRank(roman:upper()), false)
            else
                RecordLearned(learned, affix.name, nil, affix.weaponOnly == true)
            end
        end
    end
end

local function ScanSpellbookLearned(learned)
    local bookType = BOOKTYPE_SPELL or "spell"
    local numTabs = GetNumSpellTabs and GetNumSpellTabs() or 0
    for tabIdx = 1, numTabs do
        local _, _, offset, numSpells = GetSpellTabInfo(tabIdx)
        if offset and numSpells then
            for slot = offset + 1, offset + numSpells do
                local name, subText = GetSpellName(slot, bookType)
                if name and name ~= "" then
                    local parsed = AD.ParseBookLabel(name .. (subText and subText ~= "" and (" " .. subText) or ""))
                    if parsed and parsed.name and AD.GetByName(parsed.name) then
                        if parsed.rank then
                            RecordLearned(learned, parsed.name, parsed.rank, false)
                        else
                            local affix = AD.GetByName(parsed.name)
                            RecordLearned(learned, parsed.name, nil, affix and affix.weapon == true)
                        end
                    end
                end
            end
        end
    end
end

local function ScanEquippedItems(items)
    for slot = 1, 19 do
        local link = GetInventoryItemLink and GetInventoryItemLink("player", slot)
        if link then
            local entry = AD.ReadAffixFromLink(link) or AD.ReadAffixFromInventorySlot("player", slot)
            if entry and entry.name then
                RecordRank(items, entry.name, entry.rank, link, "equipped")
            end
        end
    end
end

local function ScanBagItems(items)
    for bag = 0, 4 do
        local slots = GetContainerNumSlots and GetContainerNumSlots(bag) or 0
        for slot = 1, slots do
            local link = GetContainerItemLink and GetContainerItemLink(bag, slot)
            if link then
                local entry = AD.ReadAffixFromLink(link)
                if entry and entry.name then
                    RecordRank(items, entry.name, entry.rank, link, "bag")
                end
            end
        end
    end
end

local function HighestLearnedRank(learnedEntry)
    if not learnedEntry or not learnedEntry.ranks then return nil end
    local best
    for rank in pairs(learnedEntry.ranks) do
        if not best or rank > best then best = rank end
    end
    return best
end

local function HighestItemRank(itemEntry, requiredRank)
    if not itemEntry then return nil end
    if itemEntry.rankless then return nil end
    local best
    for rank in pairs(itemEntry.ranks or {}) do
        if rank >= (requiredRank or 1) and (not best or rank > best) then
            best = rank
        end
    end
    return best
end

local function HasExactItemRank(itemEntry, rank)
    if not itemEntry then return false end
    if rank then
        return itemEntry.ranks and itemEntry.ranks[rank] or false
    end
    return itemEntry.rankless == true or (itemEntry.ranks and next(itemEntry.ranks) ~= nil)
end

local function IsLearnedAtRank(learnedEntry, rank, weapon)
    if not learnedEntry then return false end
    if weapon then
        return learnedEntry.rankless or learnedEntry.any
            or (learnedEntry.ranks and next(learnedEntry.ranks) ~= nil)
    end
    if not rank then return false end
    if learnedEntry.ranks and learnedEntry.ranks[rank] then return true end
    local best = HighestLearnedRank(learnedEntry)
    return best ~= nil and best >= rank
end

function EbonholdHub.AffixOwnership.Invalidate()
    cache = nil
    cacheAt = 0
end

function EbonholdHub.AffixOwnership.RegisterRefresh(fn)
    if type(fn) == "function" then
        refreshListeners[#refreshListeners + 1] = fn
    end
end

local function NotifyRefresh()
    for i = 1, #refreshListeners do
        local ok, err = pcall(refreshListeners[i])
        if not ok and DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cffff5555[EbonholdHub]|r Affix refresh error: " .. tostring(err))
        end
    end
end

local function ScheduleNotifyRefresh()
    notifyPending = true
    notifyElapsed = 0
    if notifyFrame then return end
    notifyFrame = CreateFrame("Frame")
    notifyFrame:SetScript("OnUpdate", function(_, dt)
        if not notifyPending then return end
        notifyElapsed = notifyElapsed + dt
        if notifyElapsed < NOTIFY_DELAY then return end
        notifyPending = false
        notifyElapsed = 0
        NotifyRefresh()
    end)
end

function EbonholdHub.AffixOwnership.CollectSnapshot(force)
    local now = GetTime and GetTime() or 0
    if not force and cache and (now - cacheAt) < 1 then
        return cache
    end

    AD.GetCatalog()

    local snapshot = {
        learned  = {},
        equipped = {},
        bag      = {},
        allItems = {},
    }

    ImportServerLearned(snapshot.learned)
    ImportAffixDexLearned(snapshot.learned)
    ScanSpellbookLearned(snapshot.learned)
    ScanEquippedItems(snapshot.equipped)
    ScanBagItems(snapshot.bag)

    for name, entry in pairs(snapshot.equipped) do
        snapshot.allItems[name] = {
            ranks = {},
            rankless = entry.rankless,
            itemLinks = {},
        }
        local target = snapshot.allItems[name]
        for rank in pairs(entry.ranks or {}) do target.ranks[rank] = true end
        if entry.itemLinks then
            for key, links in pairs(entry.itemLinks) do
                target.itemLinks[key] = target.itemLinks[key] or {}
                for _, linkEntry in ipairs(links) do
                    target.itemLinks[key][#target.itemLinks[key] + 1] = linkEntry
                end
            end
        end
    end
    for name, entry in pairs(snapshot.bag) do
        local target = EnsureEntry(snapshot.allItems, name)
        for rank in pairs(entry.ranks or {}) do target.ranks[rank] = true end
        if entry.rankless then target.rankless = true end
        if entry.itemLinks then
            target.itemLinks = target.itemLinks or {}
            for key, links in pairs(entry.itemLinks) do
                target.itemLinks[key] = target.itemLinks[key] or {}
                for _, linkEntry in ipairs(links) do
                    target.itemLinks[key][#target.itemLinks[key] + 1] = linkEntry
                end
            end
        end
    end

    cache = snapshot
    cacheAt = now
    return snapshot
end

function EbonholdHub.AffixOwnership.GetStatus(entry, snapshot)
    snapshot = snapshot or EbonholdHub.AffixOwnership.CollectSnapshot()
    if not entry or not entry.name then
        return {
            status = STATUS_NEED,
            label = STATUS_LABELS[STATUS_NEED],
            learned = false,
            inBag = false,
            equipped = false,
            satisfied = false,
        }
    end

    local weapon = AD.IsWeaponAffix(entry)
    local rank = weapon and nil or entry.rank
    local learnedEntry = snapshot.learned[entry.name]
    local itemEntry = snapshot.allItems[entry.name]
    local bagEntry = snapshot.bag[entry.name]
    local equippedEntry = snapshot.equipped[entry.name]

    local learned = IsLearnedAtRank(learnedEntry, rank, weapon)
    local inBag = HasExactItemRank(bagEntry, rank)
    local equipped = HasExactItemRank(equippedEntry, rank)
    local hasItem = inBag or equipped or HasExactItemRank(itemEntry, rank)

    local status = STATUS_NEED
    if learned then
        status = STATUS_HAVE
    elseif hasItem then
        status = STATUS_ITEM
    elseif not weapon and rank then
        local learnedBest = HighestLearnedRank(learnedEntry)
        local itemBest = HighestItemRank(itemEntry, 1)
        if (learnedBest and learnedBest < rank) or (itemBest and itemBest < rank) then
            status = STATUS_PARTIAL
        end
    end

    return {
        status = status,
        label = STATUS_LABELS[status],
        learned = learned,
        inBag = inBag,
        equipped = equipped,
        satisfied = learned or hasItem,
        learnedBest = HighestLearnedRank(learnedEntry),
        itemBest = HighestItemRank(itemEntry, 1),
        itemLinks = itemEntry and itemEntry.itemLinks or nil,
    }
end

function EbonholdHub.AffixOwnership.ApplySlotBorder(btn, entry, snapshot)
    if not btn or not btn._border then return end
    if not entry or not entry.name then
        btn._border.top:SetVertexColor(unpack(EbonholdHub.UI.Colors.border))
        btn._border.bottom:SetVertexColor(unpack(EbonholdHub.UI.Colors.border))
        btn._border.left:SetVertexColor(unpack(EbonholdHub.UI.Colors.border))
        btn._border.right:SetVertexColor(unpack(EbonholdHub.UI.Colors.border))
        btn._ownershipStatus = nil
        return
    end
    local status = EbonholdHub.AffixOwnership.GetStatus(entry, snapshot)
    local color = BORDER_COLORS[status.status] or BORDER_COLORS[STATUS_NEED]
    btn._border.top:SetVertexColor(unpack(color))
    btn._border.bottom:SetVertexColor(unpack(color))
    btn._border.left:SetVertexColor(unpack(color))
    btn._border.right:SetVertexColor(unpack(color))
    btn._ownershipStatus = status
end

function EbonholdHub.AffixOwnership.AddTooltipLines(entry, snapshot)
    if not entry or not entry.name then return end
    local status = EbonholdHub.AffixOwnership.GetStatus(entry, snapshot)
    local rgb = TEXT_COLORS[status.status] or TEXT_COLORS[STATUS_NEED]
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(status.label, rgb[1], rgb[2], rgb[3])
    if status.learned then
        local desc = entry.description
        if desc and desc ~= "" then
            GameTooltip:AddLine(desc, 0.7, 0.7, 0.7, true)
        end
    end
    if status.equipped then
        GameTooltip:AddLine("Matching item equipped", 1.0, 0.85, 0.12, true)
    end
    if status.inBag then
        GameTooltip:AddLine("Matching item in bags", 0.3, 0.75, 1.0, true)
    end
    if status.status == STATUS_PARTIAL then
        if status.learnedBest then
            GameTooltip:AddLine("Best learned rank: " .. AD.RankToRoman(status.learnedBest), 0.8, 0.8, 0.8, true)
        end
        if status.itemBest then
            GameTooltip:AddLine("Best item rank: " .. AD.RankToRoman(status.itemBest), 0.8, 0.8, 0.8, true)
        end
    end
    if status.status == STATUS_NEED then
        GameTooltip:AddLine("Still need to find or buy this affix", 0.8, 0.8, 0.8, true)
    end
end

function EbonholdHub.AffixOwnership.CountLoadout(loadout, snapshot)
    snapshot = snapshot or EbonholdHub.AffixOwnership.CollectSnapshot()
    loadout = loadout or {}
    local filled, have, need = 0, 0, 0
    for _, slotDef in ipairs(AD.SLOTS) do
        local entry = loadout[slotDef.key]
        if entry and entry.name then
            filled = filled + 1
            local status = EbonholdHub.AffixOwnership.GetStatus(entry, snapshot)
            if status.satisfied then
                have = have + 1
            else
                need = need + 1
            end
        end
    end
    return filled, have, need
end

function EbonholdHub.AffixOwnership.Init()
    if EbonholdHub.AffixOwnership._initialized then return end
    EbonholdHub.AffixOwnership._initialized = true

    local frame = CreateFrame("Frame")
    frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
    frame:RegisterEvent("BAG_UPDATE")
    frame:RegisterEvent("SPELLS_CHANGED")
    frame:RegisterEvent("LEARNED_SPELL_IN_TAB")
    frame:SetScript("OnEvent", function(_, event)
        EbonholdHub.AffixOwnership.Invalidate()
        -- Always debounce: SPELLS_CHANGED can fire dozens of times in rapid
        -- succession when echoes are cleared on death, and calling
        -- NotifyRefresh() synchronously for each event triggers an expensive
        -- CollectSnapshot() scan on every single event.
        ScheduleNotifyRefresh()
    end)

    if _G.ExtractionService and _G.ExtractionService.RequestLearnedAffixes then
        pcall(_G.ExtractionService.RequestLearnedAffixes)
    end
end
