-- ETB Wishlist Panel — Visual wishlist with farming guide
-- Accessed via /etb wishlist, /etb farm, or Ctrl+Right-click items to add.
local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETB.WishlistPanel = {}
local WP = ETB.WishlistPanel

WP.frame = nil
WP.elements = {}  -- all created UI elements for cleanup

local FRAME_WIDTH = 500
local FRAME_HEIGHT = 550
local ROW_HEIGHT = 22

-- ============================================================================
-- ELEMENT CLEANUP — hides and releases ALL dynamically created children
-- ============================================================================

local function ClearElements(self)
    for _, el in ipairs(self.elements) do
        el:Hide()
        el:ClearAllPoints()
        el:SetParent(nil)
    end
    wipe(self.elements)
end

-- Helper: create a frame tracked for cleanup
local function MakeFrame(self, frameType, parent, template)
    local f = CreateFrame(frameType or "Frame", nil, parent, template)
    self.elements[#self.elements + 1] = f
    return f
end

-- Helper: create a fontstring on a tracked frame (fontstrings don't need separate tracking)
-- But standalone fontstrings on scrollChild DO need tracking via wrapper frames.

-- ============================================================================
-- CREATE MAIN FRAME
-- ============================================================================

function WP:Create()
    if self.frame then return self.frame end

    local f = CreateFrame("Frame", "ETBWishlistPanel", UIParent, "BackdropTemplate")
    f:SetSize(FRAME_WIDTH, FRAME_HEIGHT)
    f:SetPoint("CENTER", 0, 30)
    f:SetFrameStrata("HIGH")
    f:SetFrameLevel(100)
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 },
    })
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)

    -- ESC closes panel. v14.10.1: see Construction.lua for rationale.
    if UISpecialFrames then
        local already = false
        for _, name in ipairs(UISpecialFrames) do
            if name == "ETBWishlistPanel" then already = true; break end
        end
        if not already then tinsert(UISpecialFrames, "ETBWishlistPanel") end
    end

    local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -2, -2)

    f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.title:SetPoint("TOP", 0, -16)
    f.title:SetText("|cffffd100Wishlist|r")

    f.statsText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.statsText:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
    f.statsText:SetTextColor(0.5, 0.5, 0.5)

    -- Tab buttons
    f.tabItems = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.tabItems:SetSize(100, 22)
    f.tabItems:SetPoint("TOPLEFT", 20, -48)
    f.tabItems:SetText("Items")
    f.tabItems:SetScript("OnClick", function() WP:ShowTab("items") end)

    f.tabFarm = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.tabFarm:SetSize(100, 22)
    f.tabFarm:SetPoint("LEFT", f.tabItems, "RIGHT", 4, 0)
    f.tabFarm:SetText("Farming Guide")
    f.tabFarm:SetScript("OnClick", function() WP:ShowTab("farm") end)

    f.clearBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.clearBtn:SetSize(120, 22)
    f.clearBtn:SetPoint("TOPRIGHT", -20, -48)
    f.clearBtn:SetText("Clear Collected")
    f.clearBtn:SetScript("OnClick", function() WP:ClearCollected() end)
    f.clearBtn:SetScript("OnEnter", function(btn)
        GameTooltip:SetOwner(btn, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("Remove collected items from wishlist")
        GameTooltip:Show()
    end)
    f.clearBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Scroll frame
    f.scrollFrame = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    f.scrollFrame:SetPoint("TOPLEFT", 16, -78)
    f.scrollFrame:SetPoint("BOTTOMRIGHT", -32, 16)

    f.scrollChild = CreateFrame("Frame", nil, f.scrollFrame)
    f.scrollChild:SetWidth(FRAME_WIDTH - 52)
    f.scrollChild:SetHeight(1)
    f.scrollFrame:SetScrollChild(f.scrollChild)

    f:Hide()
    self.frame = f
    return f
end

-- ============================================================================
-- COLLECTION CHECK
-- ============================================================================

local function IsItemCollected(itemID)
    if not C_TransmogCollection or not C_TransmogCollection.GetItemInfo then return false end
    local ok, appID = pcall(C_TransmogCollection.GetItemInfo, itemID)
    if not ok or not appID then return false end
    if C_TransmogCollection.GetAllAppearanceSources then
        local sOK, allSrc = pcall(C_TransmogCollection.GetAllAppearanceSources, appID)
        if sOK and allSrc and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
            for _, sid in ipairs(allSrc) do
                local hOK, has = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sid)
                if hOK and has then return true end
            end
        end
    end
    return false
end

-- ============================================================================
-- ITEMS TAB
-- ============================================================================

function WP:RefreshItems()
    if not self.frame then return end
    local sc = self.frame.scrollChild
    ClearElements(self)

    if not ETB.Wishlist or not ETB.Wishlist.data then
        self.frame.statsText:SetText("0 items")
        return
    end

    local items = ETB.Wishlist:GetAllItems()
    table.sort(items)

    -- v15.8.3: Track items still pending name resolution so the GET_ITEM_
    -- INFO_RECEIVED listener (armed at end of function) knows when to
    -- refresh. Icons are resolved synchronously via GetItemIcon (iconID is
    -- static client data, always returns immediately) so they never need
    -- async re-fetch — but name + quality come from GetItemInfo which is
    -- async for uncached items.
    local pendingNames = {}

    local enriched = {}
    for _, itemID in ipairs(items) do
        local name, _, quality = GetItemInfo(itemID)
        local texture = (GetItemIcon and GetItemIcon(itemID)) or nil
        if not name then
            pendingNames[itemID] = true
        end
        local collected = IsItemCollected(itemID)
        local sourceText = ""
        if ETB.SourceMapper and ETB.SourceMapper.initialized then
            local src = ETB.SourceMapper:GetPrimarySource(itemID)
            if src then sourceText = src.name or "" end
        end

        enriched[#enriched + 1] = {
            itemID = itemID,
            name = name or ("Item " .. itemID),
            quality = quality or 0,
            texture = texture,
            collected = collected,
            source = sourceText,
        }
    end

    table.sort(enriched, function(a, b)
        if a.collected ~= b.collected then return not a.collected end
        if a.quality ~= b.quality then return a.quality > b.quality end
        return a.name < b.name
    end)

    local totalCount = #enriched
    local collectedCount = 0
    for _, item in ipairs(enriched) do
        if item.collected then collectedCount = collectedCount + 1 end
    end

    self.frame.statsText:SetText(string.format(
        "%d items â |cff00ff00%d collected|r â |cffff4444%d needed|r",
        totalCount, collectedCount, totalCount - collectedCount))

    if totalCount == 0 then
        local emptyRow = MakeFrame(self, "Frame", sc)
        emptyRow:SetSize(sc:GetWidth(), 80)
        emptyRow:SetPoint("TOPLEFT", 0, -30)
        local txt = emptyRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        txt:SetPoint("CENTER")
        txt:SetJustifyH("CENTER")
        txt:SetText("|cff888888Your wishlist is empty.\n\nCtrl+Right-click items in the Transmog Browser\nto add them to your wishlist.|r")
        emptyRow:Show()
        sc:SetHeight(120)
        return
    end

    local yOffset = 0
    for i, item in ipairs(enriched) do
        local row = MakeFrame(self, "Frame", sc)
        row:SetSize(sc:GetWidth(), ROW_HEIGHT)
        row:SetPoint("TOPLEFT", 0, yOffset)

        -- Alternating row bg
        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        bg:SetColorTexture(1, 1, 1, i % 2 == 0 and 0.06 or 0.02)
        
        -- Dim collected rows
        if item.collected then
            row:SetAlpha(0.5)
        end

        -- Icon
        local icon = row:CreateTexture(nil, "OVERLAY")
        icon:SetSize(18, 18)
        icon:SetPoint("LEFT", 4, 0)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        icon:SetTexture(item.texture or "Interface\\Icons\\INV_Misc_QuestionMark")

        -- Name
        local qColor = "|cffffffff"
        if item.quality > 0 then
            local r, g, b = GetItemQualityColor(item.quality)
            qColor = string.format("|cff%02x%02x%02x", r*255, g*255, b*255)
        end
        local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        nameFS:SetPoint("LEFT", icon, "RIGHT", 6, 0)
        nameFS:SetWidth(210)
        nameFS:SetJustifyH("LEFT")
        nameFS:SetWordWrap(false)
        nameFS:SetText(qColor .. item.name .. "|r")

        -- Source
        local srcFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        srcFS:SetPoint("LEFT", nameFS, "RIGHT", 8, 0)
        srcFS:SetWidth(130)
        srcFS:SetJustifyH("LEFT")
        srcFS:SetTextColor(0.5, 0.5, 0.5)
        srcFS:SetWordWrap(false)
        srcFS:SetText(item.source)

        -- Status
        local statusFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        statusFS:SetPoint("RIGHT", -28, 0)
        statusFS:SetJustifyH("RIGHT")
        if item.collected then
            statusFS:SetText("|cff00ff00Collected|r")
        else
            statusFS:SetText("|cffff4444Needed|r")
        end

        -- Remove button
        local removeBtn = CreateFrame("Button", nil, row)
        removeBtn:SetSize(16, 16)
        removeBtn:SetPoint("RIGHT", -4, 0)
        removeBtn:SetNormalTexture("Interface\\Buttons\\UI-StopButton")
        removeBtn:SetHighlightTexture("Interface\\Buttons\\UI-StopButton", "ADD")
        local capturedID = item.itemID
        removeBtn:SetScript("OnClick", function()
            ETB.Wishlist:RemoveItem(capturedID)
            WP:RefreshItems()
        end)
        removeBtn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_LEFT")
            GameTooltip:AddLine("Remove from wishlist")
            GameTooltip:Show()
        end)
        removeBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

        -- Item tooltip
        row.itemID = item.itemID
        row:EnableMouse(true)
        row:SetScript("OnEnter", function(self)
            if self.itemID then
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                GameTooltip:SetItemByID(self.itemID)
                GameTooltip:Show()
            end
        end)
        row:SetScript("OnLeave", function() GameTooltip:Hide() end)

        row:Show()
        yOffset = yOffset - ROW_HEIGHT
    end

    sc:SetHeight(math.max(1, math.abs(yOffset) + 10))

    -- v15.8.3: Arm one-shot listener for items whose name is still loading.
    -- When GET_ITEM_INFO_RECEIVED fires for any of them, refresh the panel.
    -- Tear down after either no items pending OR the panel is hidden.
    self:_armNameRefreshListener(pendingNames)
end

-- One-shot listener for async GetItemInfo loads. The wishlist + farming
-- guide both create rows with item names; uncached items show "Item <id>"
-- until the server responds with GET_ITEM_INFO_RECEIVED. This helper
-- registers a listener that refreshes the active tab when any pending
-- itemID resolves, then unregisters itself when nothing more is pending.
function WP:_armNameRefreshListener(pendingNames)
    -- Count pending entries (need at least 1 to bother with a listener)
    local hasPending = false
    for _ in pairs(pendingNames or {}) do hasPending = true; break end
    if not hasPending then
        if self._nameListener then self._nameListener:UnregisterAllEvents() end
        return
    end

    self._pendingNames = pendingNames
    if not self._nameListener then
        self._nameListener = CreateFrame("Frame")
        self._nameListener:SetScript("OnEvent", function(_, _, evtItemID)
            if not self._pendingNames or not self._pendingNames[evtItemID] then return end
            self._pendingNames[evtItemID] = nil
            -- Only refresh if the panel is still visible
            if self.frame and self.frame:IsShown() then
                if self.activeTab == "farming" then
                    self:RefreshFarmingGuide()
                else
                    self:RefreshItems()
                end
            end
        end)
    end
    self._nameListener:UnregisterAllEvents()
    self._nameListener:RegisterEvent("GET_ITEM_INFO_RECEIVED")
end

-- ============================================================================
-- FARMING GUIDE TAB
-- ============================================================================

function WP:RefreshFarmingGuide()
    if not self.frame then return end
    local sc = self.frame.scrollChild
    ClearElements(self)

    if not ETB.Wishlist or not ETB.Wishlist.data then
        self.frame.statsText:SetText("0 items")
        return
    end

    -- v15.8.3: Track items pending name resolution for async refresh
    local pendingNames = {}

    local byInstance, noSource = ETB.Wishlist:GetFarmingGuide()

    local instNames = {}
    for name in pairs(byInstance) do instNames[#instNames + 1] = name end
    table.sort(instNames)

    local totalNeeded = 0
    for _, instData in pairs(byInstance) do
        for _, bossItems in pairs(instData.bosses) do
            totalNeeded = totalNeeded + #bossItems
        end
    end
    totalNeeded = totalNeeded + #noSource

    self.frame.statsText:SetText(string.format(
        "%d uncollected items across %d locations",
        totalNeeded, #instNames + (#noSource > 0 and 1 or 0)))

    if totalNeeded == 0 then
        local emptyRow = MakeFrame(self, "Frame", sc)
        emptyRow:SetSize(sc:GetWidth(), 80)
        emptyRow:SetPoint("TOPLEFT", 0, -30)
        local txt = emptyRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        txt:SetPoint("CENTER")
        txt:SetJustifyH("CENTER")
        if ETB.Wishlist:GetItemCount() > 0 then
            txt:SetText("|cff00ff00All wishlisted items are collected!|r")
        else
            txt:SetText("|cff888888Your wishlist is empty.\n\nCtrl+Right-click items in the Transmog Browser\nto add them to your wishlist.|r")
        end
        emptyRow:Show()
        sc:SetHeight(120)
        return
    end

    local yOffset = 0

    for _, instName in ipairs(instNames) do
        local instData = byInstance[instName]
        local itemCount = 0
        local bossCount = 0
        for _ in pairs(instData.bosses) do bossCount = bossCount + 1 end
        for _, bossItems in pairs(instData.bosses) do itemCount = itemCount + #bossItems end

        -- Instance header
        local header = MakeFrame(self, "Frame", sc)
        header:SetSize(sc:GetWidth(), ROW_HEIGHT + 4)
        header:SetPoint("TOPLEFT", 0, yOffset)
        local hBG = header:CreateTexture(nil, "BACKGROUND")
        hBG:SetAllPoints()
        hBG:SetColorTexture(1, 0.82, 0, 0.08)
        local hText = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        hText:SetPoint("LEFT", 8, 0)
        hText:SetTextColor(1, 0.82, 0)
        hText:SetText(instName)
        local hCount = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        hCount:SetPoint("RIGHT", -8, 0)
        hCount:SetTextColor(0.5, 0.5, 0.5)
        hCount:SetText(itemCount .. " items, " .. bossCount .. " bosses")
        header:Show()
        yOffset = yOffset - (ROW_HEIGHT + 4)

        local bossNames = {}
        for name in pairs(instData.bosses) do bossNames[#bossNames + 1] = name end
        table.sort(bossNames)

        for _, bossName in ipairs(bossNames) do
            local bossItems = instData.bosses[bossName]

            -- Boss sub-header
            local bossRow = MakeFrame(self, "Frame", sc)
            bossRow:SetSize(sc:GetWidth(), ROW_HEIGHT)
            bossRow:SetPoint("TOPLEFT", 0, yOffset)
            local bText = bossRow:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            bText:SetPoint("LEFT", 16, 0)
            bText:SetText("|cffffff00" .. bossName .. "|r  |cff888888(" .. #bossItems .. ")|r")
            bossRow:Show()
            yOffset = yOffset - ROW_HEIGHT

            for _, itemID in ipairs(bossItems) do
                local row = MakeFrame(self, "Frame", sc)
                row:SetSize(sc:GetWidth(), ROW_HEIGHT)
                row:SetPoint("TOPLEFT", 0, yOffset)

                -- v15.8.3: Use GetItemIcon (sync) for icon, GetItemInfo for name.
                -- Track pending name resolution so the listener refreshes later.
                local name, _, quality = GetItemInfo(itemID)
                local texture = (GetItemIcon and GetItemIcon(itemID)) or nil
                if not name then pendingNames[itemID] = true end
                name = name or ("Item " .. itemID)

                local icon = row:CreateTexture(nil, "OVERLAY")
                icon:SetSize(16, 16)
                icon:SetPoint("LEFT", 32, 0)
                icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark")

                local qColor = "|cffffffff"
                if quality and quality > 0 then
                    local r, g, b = GetItemQualityColor(quality)
                    qColor = string.format("|cff%02x%02x%02x", r*255, g*255, b*255)
                end
                local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                nameFS:SetPoint("LEFT", icon, "RIGHT", 6, 0)
                nameFS:SetText(qColor .. name .. "|r")

                row.itemID = itemID
                row:EnableMouse(true)
                row:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                    GameTooltip:SetItemByID(self.itemID)
                    GameTooltip:Show()
                end)
                row:SetScript("OnLeave", function() GameTooltip:Hide() end)

                row:Show()
                yOffset = yOffset - ROW_HEIGHT
            end
        end

        yOffset = yOffset - 6
    end

    -- Unknown source items
    if #noSource > 0 then
        local header = MakeFrame(self, "Frame", sc)
        header:SetSize(sc:GetWidth(), ROW_HEIGHT + 4)
        header:SetPoint("TOPLEFT", 0, yOffset)
        local hBG = header:CreateTexture(nil, "BACKGROUND")
        hBG:SetAllPoints()
        hBG:SetColorTexture(1, 0.82, 0, 0.08)
        local hText = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        hText:SetPoint("LEFT", 8, 0)
        hText:SetTextColor(1, 0.82, 0)
        hText:SetText("Unknown Source")
        local hCount = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        hCount:SetPoint("RIGHT", -8, 0)
        hCount:SetTextColor(0.5, 0.5, 0.5)
        hCount:SetText(#noSource .. " items")
        header:Show()
        yOffset = yOffset - (ROW_HEIGHT + 4)

        for _, itemID in ipairs(noSource) do
            local row = MakeFrame(self, "Frame", sc)
            row:SetSize(sc:GetWidth(), ROW_HEIGHT)
            row:SetPoint("TOPLEFT", 0, yOffset)

            -- v15.8.3: same icon-sync pattern as the per-boss loop above
            local name, _, quality = GetItemInfo(itemID)
            local texture = (GetItemIcon and GetItemIcon(itemID)) or nil
            if not name then pendingNames[itemID] = true end
            name = name or ("Item " .. itemID)

            local icon = row:CreateTexture(nil, "OVERLAY")
            icon:SetSize(16, 16)
            icon:SetPoint("LEFT", 16, 0)
            icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark")

            local qColor = "|cffffffff"
            if quality and quality > 0 then
                local r, g, b = GetItemQualityColor(quality)
                qColor = string.format("|cff%02x%02x%02x", r*255, g*255, b*255)
            end
            local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            nameFS:SetPoint("LEFT", icon, "RIGHT", 6, 0)
            nameFS:SetText(qColor .. name .. "|r")

            row.itemID = itemID
            row:EnableMouse(true)
            row:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                GameTooltip:SetItemByID(self.itemID)
                GameTooltip:Show()
            end)
            row:SetScript("OnLeave", function() GameTooltip:Hide() end)

            row:Show()
            yOffset = yOffset - ROW_HEIGHT
        end
    end

    sc:SetHeight(math.max(1, math.abs(yOffset) + 10))

    -- v15.8.3: same async-refresh listener as RefreshItems
    self:_armNameRefreshListener(pendingNames)
end

-- ============================================================================
-- TAB SWITCHING
-- ============================================================================

WP.activeTab = "items"

function WP:ShowTab(tab)
    self.activeTab = tab
    if not self.frame then return end

    if tab == "items" then
        self.frame.tabItems:SetText("|cffffd100Items|r")
        self.frame.tabFarm:SetText("Farming Guide")
        self:RefreshItems()
    else
        self.frame.tabItems:SetText("Items")
        self.frame.tabFarm:SetText("|cffffd100Farming Guide|r")
        self:RefreshFarmingGuide()
    end
end

-- ============================================================================
-- CLEAR COLLECTED
-- ============================================================================

function WP:ClearCollected()
    if not ETB.Wishlist or not ETB.Wishlist.data or not ETB.Wishlist.data.items then return end

    local toRemove = {}
    for itemID in pairs(ETB.Wishlist.data.items) do
        if IsItemCollected(itemID) then
            toRemove[#toRemove + 1] = itemID
        end
    end

    for _, itemID in ipairs(toRemove) do
        ETB.Wishlist.data.items[itemID] = nil
    end

    if #toRemove > 0 then
        ETB:Print(string.format("Removed %d collected items from wishlist.", #toRemove))
        ETB.EventBus:Fire("ETB_WISHLIST_CHANGED", "clear")
    else
        ETB:Print("No collected items found on your wishlist.")
    end

    self:ShowTab(self.activeTab)
end

-- ============================================================================
-- SHOW / HIDE / TOGGLE
-- ============================================================================

function WP:Show()
    if not self.frame then self:Create() end
    self:ShowTab(self.activeTab)
    self.frame:Show()
end

function WP:Hide()
    if self.frame then self.frame:Hide() end
end

function WP:Toggle()
    if self.frame and self.frame:IsShown() then
        self:Hide()
    else
        self:Show()
    end
end
