-- TOG Tools — Item Database tab (UI) [developer tool]
-- Builds the item database (throttled ID walk) and searches it offline,
-- filtered by item type (class) / subtype (subclass). Results render as
-- interactive item links — hover for the tooltip, shift-click into chat —
-- via RowList's built-in |Hitem:...|h handling.
--
-- Dev-only: this tab appears only when Settings > General > Developer Tools
-- is enabled (MainWindow gates `devOnly` modules on db.global.devTools).

local _, addon = ...
local AceGUI = LibStub("AceGUI-3.0")

local BRAND = "|c" .. (addon.BrandColor or "ffFF8000")

local ItemDB = addon.modules["itemdb"]
local Tab    = ItemDB
Tab.tabKey      = "itemdb"
Tab.configKey   = "itemDB"
Tab.label       = "Item DB"
Tab.devOnly     = true
Tab.WINDOW_SIZE = { width = 760, height = 560, minWidth = 600, minHeight = 400 }
Tab.help = {
    title = "Item Database (developer tool)",
    lines = {
        "Builds a local, searchable copy of the game's item catalog by walking every item ID and recording what the server returns. WoW has no item-name search API, so this is the only way to resolve a name to a link offline.",
        "|cffffd700Build / Resume|r starts (or continues) the walk; |cffffd700Pause|r stops it; |cffffd700Rebuild|r wipes and starts fresh; |cffffd700Purge|r wipes. The first build issues a throttled burst of server requests and takes a few minutes — it's a one-time cost.",
        "Search by name (2+ characters) and/or filter by |cffffd700Type|r and |cffffd700Subtype|r — WoW's own item classes. Results are interactive: hover for the tooltip, shift-click to insert the link into chat.",
        "The database is bucketed by item class so type/subtype filtering is fast. Names are localized — if you change client language, rebuild.",
    },
}

-- Minimum query length before a name-only search runs (a type filter lets
-- shorter / empty queries through so you can browse a whole class).
local MIN_QUERY = 2
local MAX_RESULTS = 300

-- ---------------------------------------------------------------------------
-- Confirm popups for destructive actions (game-standard two-step gate).
-- ---------------------------------------------------------------------------
StaticPopupDialogs["TOGTOOLS_ITEMDB_PURGE"] = {
    text         = "Wipe the entire TOG Tools item database?\n\nYou'll need to rebuild it from scratch.",
    button1      = "Wipe",
    button2      = "Cancel",
    OnAccept     = function()
        ItemDB:Purge()
        if Tab._afterMutate then Tab._afterMutate() end
    end,
    timeout      = 0,
    whileDead    = true,
    hideOnEscape = true,
    preferredIndex = 3,
}
StaticPopupDialogs["TOGTOOLS_ITEMDB_REBUILD"] = {
    text         = "Wipe and rebuild the item database from scratch?\n\nThe first build takes a few minutes.",
    button1      = "Rebuild",
    button2      = "Cancel",
    OnAccept     = function()
        ItemDB:Purge()
        ItemDB:Start()
        if Tab._afterMutate then Tab._afterMutate() end
    end,
    timeout      = 0,
    whileDead    = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ---------------------------------------------------------------------------
-- Progress line
-- ---------------------------------------------------------------------------
function Tab:_renderProgress()
    local lbl = self._progressLabel
    if not lbl or not lbl.SetText then return end
    local p = ItemDB:GetProgress()

    if not p.available then
        lbl:SetText("|cffff5555This client lacks the item-existence API — the database builder isn't supported here.|r")
        return
    end

    local pct = (p.topID and p.topID > 0)
        and math.min(100, math.floor((p.cursor / p.topID) * 100)) or 0

    -- State label keyed off the engine phase. The Idle (paused) line spells
    -- out the resume point so it's obvious a stopped build continues where it
    -- left off — across reloads and logouts (cursor + pending live in SV).
    local state, where
    if p.phase == "complete" then
        state = "|cff44ee44Complete|r"
        where = string.format("|cffaaaaaa(%d items)|r", p.count)
    elseif p.phase == "walking" then
        state = "|cff44ee44Scanning|r"
        where = string.format("|cffaaaaaa(id %d / %d)|r", p.cursor, p.topID)
    elseif p.phase == "gapfill" then
        state = "|cff44ee44Filling gaps|r"
        where = string.format("|cffaaaaaa(%d awaiting reply)|r", p.pending or 0)
    else -- idle / paused, not complete
        state = "|cffffaa00Paused|r"
        where = string.format("|cffaaaaaa(resume at id %d / %d%s)|r",
            p.cursor, p.topID,
            (p.pending and p.pending > 0) and (" · " .. p.pending .. " pending") or "")
    end

    -- Persistent "gaps" count = items we know exist but don't have yet
    -- (the pending ledger). Shown alongside the have-count so the
    -- have / don't-have split is always visible. Suppressed during the
    -- gapfill phase since `where` already reports it live there.
    local gaps = ""
    if p.pending and p.pending > 0 and p.phase ~= "gapfill" then
        gaps = string.format("  •  |cffffaa00%d gaps|r", p.pending)
    end

    local stale = ""
    if p.locale and p.localeNow and p.locale ~= p.localeNow then
        stale = "  |cffff5555(locale changed — rebuild)|r"
    end

    lbl:SetText(string.format(
        "%sItem DB|r  |cffffffff%d|r items%s  •  %s %s  •  %d%%  •  |cffaaaaaalocale %s|r%s",
        BRAND, p.count, gaps, state, where, pct,
        tostring(p.locale or p.localeNow), stale))
end

-- ---------------------------------------------------------------------------
-- Draw
-- ---------------------------------------------------------------------------
function Tab:Draw(container)
    container:SetLayout("Flow")
    local self2 = self

    -- Layout = AddonLoad's two-child shape: ONE auto-height "controls" group
    -- (List) holding the progress line, button row, filter row, and
    -- result-info line, then the table body as the single full-height child.
    -- A full-height body that's a loose child of the outer Flow gets drawn on
    -- top of whatever row sits directly above it; collapsing the controls into
    -- one solidly-measured List group leaves only that group above the body.
    local controls = AceGUI:Create("SimpleGroup")
    controls:SetFullWidth(true)
    controls:SetLayout("List")
    container:AddChild(controls)

    local prog = AceGUI:Create("Label")
    prog:SetFullWidth(true)
    prog:SetText(" ")
    controls:AddChild(prog)
    self._progressLabel = prog

    -- ---- Build controls (own row, fixed-width children) -----------------
    local btnRow = AceGUI:Create("SimpleGroup")
    btnRow:SetFullWidth(true)
    btnRow:SetLayout("Flow")
    controls:AddChild(btnRow)

    local function addButton(text, width, onClick, tipTitle, tipBody)
        local b = AceGUI:Create("Button")
        b:SetText(text)
        b:SetWidth(width)
        b:SetCallback("OnClick", onClick)
        if tipTitle or tipBody then
            addon.UI.AttachTooltip(b, tipTitle, tipBody)
        end
        btnRow:AddChild(b)
        return b
    end
    addButton("Build / Resume", 140, function()
        ItemDB:Start(); self2:_renderProgress()
    end, addon.UI.Brand("Build / Resume"),
       "Start building the item database, or resume a paused build from where it left off. The walk is throttled and runs in the background — it's safe to stop and restart anytime; progress is saved.")
    addButton("Fill gaps", 95, function()
        ItemDB:FillGaps(); self2:_renderProgress()
    end, addon.UI.Brand("Fill gaps"),
       "Fetch ONLY the items we don't have yet — the known gaps. Skips the full re-walk and just re-requests the outstanding IDs. Items the server actually has are stored; items it has no name for (removed / test / cross-version stubs that can never be searched by name) are dropped after a patient retry window, so the count converges to 0 gaps. Does nothing if there are no gaps. This is the everyday \"catch up\" pass.")
    addButton("Rescan", 95, function()
        ItemDB:Rescan(); self2:_renderProgress()
    end, addon.UI.Brand("Rescan (full re-walk)"),
       "Re-walk the ENTIRE item-ID range from the start without wiping the database. Slower than Fill gaps, but it also finds existing items that were never walked at all (e.g. if a build never finished). Items you already have are skipped cheaply; the count only grows.")
    addButton("Pause", 85, function()
        ItemDB:Pause(); self2:_renderProgress()
    end, addon.UI.Brand("Pause"),
       "Stop the current build. The resume point and any items still awaiting a reply are saved, so Build / Resume continues exactly where you left off — even after a /reload or a logout.")
    addButton("Rebuild", 110, function()
        StaticPopup_Show("TOGTOOLS_ITEMDB_REBUILD")
    end, addon.UI.Brand("Rebuild"),
       "Wipe the database and build it again from scratch. Use after a game patch adds new items, or if you changed the client language (item names are localized). Asks for confirmation first.")
    addButton("Purge", 100, function()
        StaticPopup_Show("TOGTOOLS_ITEMDB_PURGE")
    end, addon.UI.Brand("Purge"),
       "Wipe the entire database without rebuilding. Frees the saved data; the tab stays empty until you Build again. Asks for confirmation first.")

    -- ---- Filters (own row) ----------------------------------------------
    self._filterClass = nil
    self._filterSub   = nil

    local filterRow = AceGUI:Create("SimpleGroup")
    filterRow:SetFullWidth(true)
    filterRow:SetLayout("Flow")
    controls:AddChild(filterRow)

    local classDD = AceGUI:Create("Dropdown")
    classDD:SetLabel(BRAND .. "Type|r")
    classDD:SetWidth(190)
    addon.UI.AttachTooltip(classDD, addon.UI.Brand("Type"),
        "Filter results to a single WoW item class (Weapon, Armor, Consumable, Trade Goods, ...). Pick a type to browse everything in it even without a search term. Only classes present in the built database are listed, with their item counts.")
    filterRow:AddChild(classDD)

    local subDD = AceGUI:Create("Dropdown")
    subDD:SetLabel(BRAND .. "Subtype|r")
    subDD:SetWidth(190)
    addon.UI.AttachTooltip(subDD, addon.UI.Brand("Subtype"),
        "Filter to a subclass within the selected Type (e.g. Cloth / Leather / Mail / Plate under Armor). Enabled once you pick a Type; the list repopulates from the subtypes actually present in that type.")
    filterRow:AddChild(subDD)

    local search = AceGUI:Create("EditBox")
    search:SetLabel(BRAND .. "Search by name|r")
    search:SetWidth(240)
    addon.UI.AttachTooltip(search, addon.UI.Brand("Search by name"),
        "Type at least 2 characters to find items by name (case-insensitive substring match). Combine with the Type / Subtype filters to narrow results. Matches are interactive — hover an item link for its tooltip, shift-click to insert it into your open chat box.")
    filterRow:AddChild(search)

    -- ---- Result-info line (inside the controls group) -------------------
    local resultInfo = AceGUI:Create("Label")
    resultInfo:SetFullWidth(true)
    resultInfo:SetText(" ")
    controls:AddChild(resultInfo)

    -- ---- Results table (final child — fills the remaining height) -------
    local body = AceGUI:Create("SimpleGroup")
    body:SetFullWidth(true)
    body:SetFullHeight(true)
    body:SetLayout("List")
    container:AddChild(body)

    local rl = addon.RowList:New(body.content, {
        rowHeight = 16,
        columns = {
            { key = "id", header = "ID", autoFit = true, minWidth = 45, justify = "RIGHT",
              headerTip = "Item ID",
              headerTipDesc = "The numeric item ID. Sort ascending/descending by clicking." },

            { key = "link", header = "Item", justify = "LEFT", sortKey = "_name",
              headerTip = "Item",
              headerTipDesc = "The item link. Hover for the tooltip; shift-click to insert into your open chat box. This is the stretchy column." },

            { key = "typeName", header = "Type", autoFit = true, minWidth = 55, justify = "LEFT",
              gapBefore = 5,
              headerTip = "Type",
              headerTipDesc = "WoW item class (Weapon, Armor, Consumable, Trade Goods, ...)." },

            { key = "subName", header = "Subtype", autoFit = true, minWidth = 55, justify = "LEFT",
              headerTip = "Subtype",
              headerTipDesc = "WoW item subclass within the type (e.g. Cloth / Leather under Armor)." },
        },
    })
    rl:SetSort("_name", false)
    self._rl = rl

    -- ---- Search execution ----------------------------------------------
    local function runSearch()
        local query = strtrim(search:GetText() or "")
        local hasFilter = self2._filterClass ~= nil
        if #query < MIN_QUERY and not hasFilter then
            rl:SetData({})
            resultInfo:SetText("|cffaaaaaaType at least " .. MIN_QUERY ..
                " characters, or pick a Type filter, to search.|r")
            return
        end
        local results = ItemDB:Search({
            query      = query,
            classID    = self2._filterClass,
            subClassID = self2._filterSub,
            max        = MAX_RESULTS,
        })
        local rows = {}
        for _, r in ipairs(results) do
            rows[#rows + 1] = {
                id       = r.id,
                link     = r.link,
                typeName = r.typeName or "",
                subName  = r.subName or "",
                _name    = r.name,
            }
        end
        rl:SetData(rows)
        if #rows == 0 then
            resultInfo:SetText("|cffaaaaaaNo matches in the database. If it's not built yet, hit Build.|r")
        elseif results.capped then
            resultInfo:SetText(string.format(
                "|cffaaaaaaShowing first %d matches (narrow with a Type or longer name).|r", #rows))
        else
            resultInfo:SetText(string.format("|cffaaaaaa%d match%s.|r", #rows, #rows == 1 and "" or "es"))
        end
    end
    self._runSearch = runSearch

    -- ---- Subtype dropdown population (depends on chosen class) ----------
    local function populateSub()
        local list, order = {}, {}
        list["*"] = "All subtypes"
        order[#order + 1] = "*"
        if self2._filterClass ~= nil then
            for _, s in ipairs(ItemDB:GetSubClasses(self2._filterClass)) do
                list[s.id] = s.name
                order[#order + 1] = s.id
            end
        end
        subDD:SetList(list, order)
        subDD:SetValue("*")
        self2._filterSub = nil
        subDD:SetDisabled(self2._filterClass == nil)
    end

    -- ---- Type dropdown population ---------------------------------------
    local classList, classOrder = {}, {}
    classList["*"] = "All types"
    classOrder[#classOrder + 1] = "*"
    for _, c in ipairs(ItemDB:GetClasses()) do
        classList[c.id] = string.format("%s (%d)", c.name, c.count)
        classOrder[#classOrder + 1] = c.id
    end
    classDD:SetList(classList, classOrder)
    classDD:SetValue("*")

    classDD:SetCallback("OnValueChanged", function(_, _, val)
        self2._filterClass = (val == "*") and nil or val
        populateSub()
        runSearch()
    end)
    subDD:SetCallback("OnValueChanged", function(_, _, val)
        self2._filterSub = (val == "*") and nil or val
        runSearch()
    end)
    search:SetCallback("OnTextChanged", function() runSearch() end)
    search:SetCallback("OnEnterPressed", function() runSearch() end)

    populateSub()
    runSearch()

    -- ---- Live wiring + cleanup -----------------------------------------
    -- After a Purge/Rebuild the filter lists and results are stale; rebuild
    -- the type list, reset filters, and re-run the (now empty) search.
    self._afterMutate = function()
        self2:_renderProgress()
        if self2._runSearch then self2._runSearch() end
    end

    -- Engine pushes progress here while scanning; guard against firing into a
    -- released label after the user navigates away.
    ItemDB.onProgress = function()
        if addon.MainWindow and addon.MainWindow.activeTab == "itemdb" then
            self2:_renderProgress()
        end
    end

    self:_renderProgress()

    body:SetCallback("OnRelease", function()
        rl:Detach()
        ItemDB.onProgress  = nil
        self2._progressLabel = nil
        self2._rl           = nil
        self2._runSearch    = nil
        self2._afterMutate  = nil
    end)
end

function Tab:Refresh(container)
    container:ReleaseChildren()
    self:Draw(container)
end
