---@diagnostic disable: undefined-field, undefined-global
local _, namespace = ...

local WTUI = namespace.WildThingsUI
local Theme = namespace.Theme

-- A table (never a numeric spellID) so it can never collide with a real candidate value --
-- the dropdown's index-1 "Add cooldown..." placeholder entry.
local ADD_SENTINEL = {}

local ROW_GAP = 4

-- Live-equipped on-use trinket resolution (trinket support): returns an array of
-- { itemID, slot, name, icon } for the currently-equipped trinkets (slots 13/14) that
-- have a real on-use spell -- a passive/proc trinket (nil second return from
-- C_Item.GetItemSpell) is never offered. Named globals (INVSLOT_TRINKET1/2), never bare
-- slot literals. Called fresh from BuildInfoTable() on every picker build/rebuild -- the
-- Settings screen rebuilds per open, so equipment changes are picked up with no event
-- listener (a PLAYER_EQUIPMENT_CHANGED listener is deliberately not added here).
-- RequestLoadItemDataByID covers the login-uncached case; the name/icon calls below still
-- degrade gracefully (item:<id> / nil icon) if data isn't cached yet.
local function GetEquippedOnUseTrinkets()
    local trinkets = {}

    local slots = { INVSLOT_TRINKET1, INVSLOT_TRINKET2 }
    for _, slot in ipairs(slots) do
        local itemID = GetInventoryItemID("player", slot)
        if itemID then
            C_Item.RequestLoadItemDataByID(itemID)
            local _, spellID = C_Item.GetItemSpell(itemID)
            if type(spellID) == "number" then
                local name = C_Item.GetItemNameByID(itemID) or ("item:" .. tostring(itemID))
                local icon = C_Item.GetItemIconByID(itemID)
                trinkets[#trinkets + 1] = { itemID = itemID, slot = slot, name = name, icon = icon }
            end
        end
    end

    return trinkets
end

-- True iff itemID currently occupies slot 13 or 14. Used only to decide whether a
-- SEEDED item pill should render greyed -- it does not gate what BuildInfoTable offers
-- (GetEquippedOnUseTrinkets above already only offers currently-equipped trinkets).
local function IsTrinketEquipped(itemID)
    return GetInventoryItemID("player", INVSLOT_TRINKET1) == itemID
        or GetInventoryItemID("player", INVSLOT_TRINKET2) == itemID
end

-- namespace:CreateSpecCooldownPicker(parent, specID, specName, candidates, onHeightChanged)
-- Self-managing per-spec cooldown picker (READY-05 Settings redesign): a spec header, an
-- "Add spell..." dropdown offering only not-yet-selected candidates, and a wrapping flow of
-- removable pills for the selected spells. Owns its own layout/height via picker:Layout();
-- calls onHeightChanged() after every mutation so the caller (SettingsScreen) can recompute
-- the scroll content height. Reads/writes WildThingsSavedVars.readycheck.cooldown.per_spec
-- (sparse: true = on, nil = off) -- the SAME data model the old per-spec toggle wall used.
function namespace:CreateSpecCooldownPicker(parent, specID, specName, candidates, onHeightChanged)
    local picker = CreateFrame("Frame", nil, parent)

    -- Spec header: re-anchor to sit at the picker's own top (CreateSectionHeader defaults
    -- to anchoring LEFT/RIGHT of its parent, not TOPLEFT/TOPRIGHT).
    local header = namespace:CreateSectionHeader(picker, specName)
    header:ClearAllPoints()
    header:SetPoint("TOPLEFT", picker, "TOPLEFT", 0, 0)
    header:SetPoint("TOPRIGHT", picker, "TOPRIGHT", 0, 0)
    local headerH = header:GetHeight()

    -- Zero-candidate branch: graceful fallback text, no dropdown.
    if not candidates or #candidates == 0 then
        local fallback = picker:CreateFontString(nil, "ARTWORK")
        fallback:SetFontObject(WildThingsFont13)
        fallback:SetTextColor(Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, Theme.TEXT_DIM.a)
        fallback:SetText("No eligible spells for this spec.")
        fallback:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -Theme.GAP)

        function picker:Layout()
            picker:SetHeight(headerH + Theme.GAP + fallback:GetStringHeight())
        end

        return picker
    end

    -- Normal branch: dropdown adder + wrapping pill flow.
    local per = WildThingsSavedVars.readycheck.cooldown.per_spec
    local existing = per[specID]

    -- Trinket sibling of `per`/`existing` above (trinket support) -- reads/writes ONLY
    -- per_spec_items, never per_spec, so an itemID key can never reach the spell API.
    local perItems = WildThingsSavedVars.readycheck.cooldown.per_spec_items
    local existingItems = perItems[specID]

    local selected = {}
    if existing then
        for spellID in pairs(existing) do
            selected[spellID] = true
        end
    end

    local selectedItems = {}
    if existingItems then
        for itemID in pairs(existingItems) do
            selectedItems[itemID] = true
        end
    end

    local pills = {}
    local dd

    -- BuildInfoTable(): fresh table, first entry is the non-actionable ADD_SENTINEL
    -- placeholder, followed by one entry per spell candidate NOT already selected, then
    -- one entry per live equipped on-use trinket NOT already selected (trinket support).
    -- A trinket entry's `value` is a fresh table carrying the itemID -- it MUST be a
    -- table, never a bare itemID number, because the existing spell values are raw
    -- numeric spellIDs and a numeric itemID could collide with a real spellID.
    local function BuildInfoTable()
        local info = { { text = "Add cooldown...", value = ADD_SENTINEL } }

        for _, cand in ipairs(candidates) do
            if not selected[cand.spellID] then
                local candInfo = WTUI.GetSpellInfo(cand.spellID)
                local iconID = candInfo and candInfo.iconID

                info[#info + 1] = {
                    text = ("%s (%ds)"):format(cand.name, cand.cooldown),
                    icon = iconID,
                    value = cand.spellID,
                }
            end
        end

        for _, trinket in ipairs(GetEquippedOnUseTrinkets()) do
            if not selectedItems[trinket.itemID] then
                local suffix = (trinket.slot == INVSLOT_TRINKET1) and "Trinket 1" or "Trinket 2"
                info[#info + 1] = {
                    text = ("%s (%s)"):format(trinket.name, suffix),
                    icon = trinket.icon,
                    value = { itemID = trinket.itemID },
                }
            end
        end

        return info
    end

    local function RemainingCount()
        local count = 0
        for _, cand in ipairs(candidates) do
            if not selected[cand.spellID] then count = count + 1 end
        end
        for _, trinket in ipairs(GetEquippedOnUseTrinkets()) do
            if not selectedItems[trinket.itemID] then count = count + 1 end
        end
        return count
    end

    local function Placeholder()
        if RemainingCount() > 0 then
            return "Add cooldown..."
        end
        return "All cooldowns added"
    end

    local function RemovePillFromList(spellID)
        for i, p in ipairs(pills) do
            if p.spellID == spellID then
                table.remove(pills, i)
                break
            end
        end
    end

    local function RemoveItemPillFromList(itemID)
        for i, p in ipairs(pills) do
            if p.itemID == itemID then
                table.remove(pills, i)
                break
            end
        end
    end

    -- Forward-declared: OnPick/removeHandler and picker:Layout reference each other
    -- (OnPick creates a pill whose removeHandler is defined below; Layout is defined
    -- after both since it needs `pills`).
    local OnPick

    local function MakeRemoveHandler(removedSpellID)
        return function()
            selected[removedSpellID] = nil
            per[specID][removedSpellID] = nil -- sparse: never write false

            for _, p in ipairs(pills) do
                if p.spellID == removedSpellID then
                    p:Hide()
                    p:SetParent(nil)
                    break
                end
            end
            RemovePillFromList(removedSpellID)

            dd:SetInfoTable(BuildInfoTable())
            dd:OverrideText(Placeholder())

            picker:Layout()
            onHeightChanged()
        end
    end

    -- Sibling of MakeRemoveHandler above (trinket support). A gear swap must never
    -- silently erase the user's configuration -- this handler only fires from the pill's
    -- own X button, never from an equip/unequip event, so the stored key is removed
    -- exclusively by explicit user action.
    local function MakeItemRemoveHandler(removedItemID)
        return function()
            selectedItems[removedItemID] = nil
            if perItems[specID] then
                perItems[specID][removedItemID] = nil -- sparse: never write false
            end

            for _, p in ipairs(pills) do
                if p.itemID == removedItemID then
                    p:Hide()
                    p:SetParent(nil)
                    break
                end
            end
            RemoveItemPillFromList(removedItemID)

            dd:SetInfoTable(BuildInfoTable())
            dd:OverrideText(Placeholder())

            picker:Layout()
            onHeightChanged()
        end
    end

    local function AddPill(spellID)
        local pill = namespace:CreateCooldownPill(picker, spellID, MakeRemoveHandler(spellID), specID)
        pills[#pills + 1] = pill
        return pill
    end

    -- Sibling of AddPill above (trinket support). CreateItemPill already exists
    -- (UI/Widgets/Pill.lua) and exposes pill.name/pill.itemID.
    -- The slot list is what lets the pill's hover tooltip show the EQUIPPED item level
    -- instead of the base variant (ilvl 28 for a trinket actually worn at 298) -- the
    -- upgrade level lives in the link's bonusIDs, which a bare itemID does not carry.
    -- Built inline, like GetEquippedOnUseTrinkets above, rather than held in a file-level
    -- constant that would read INVSLOT_TRINKET1/2 at load time.
    local function AddItemPill(itemID)
        local pill = namespace:CreateItemPill(picker, itemID, MakeItemRemoveHandler(itemID),
            { INVSLOT_TRINKET1, INVSLOT_TRINKET2 })
        pills[#pills + 1] = pill
        return pill
    end

    OnPick = function(value)
        -- NEUTRALIZE the SetDefaultValue auto-fire gotcha: Dropdown.lua's constructor
        -- calls SetDefaultValue() when built with no initialValue, which selects index 1
        -- and FIRES OnValueChanged -- the sentinel is always index 1, so this guard makes
        -- that auto-fire (and any other sentinel selection) a no-op. MUST run before the
        -- table-branch check below -- ADD_SENTINEL is also a table.
        if value == ADD_SENTINEL then return end

        if type(value) == "table" then
            -- Trinket branch (trinket support): value carries { itemID = ... }.
            local itemID = value.itemID
            selectedItems[itemID] = true
            perItems[specID] = perItems[specID] or {}
            perItems[specID][itemID] = true

            AddItemPill(itemID)

            dd:SetInfoTable(BuildInfoTable())
            dd:OverrideText(Placeholder())

            picker:Layout()
            onHeightChanged()
            return
        end

        local spellID = value
        selected[spellID] = true
        per[specID] = per[specID] or {}
        per[specID][spellID] = true

        AddPill(spellID)

        dd:SetInfoTable(BuildInfoTable()) -- rebuild EXCLUDES the now-selected spell; does
                                           -- NOT re-fire OnValueChanged (only the
                                           -- constructor's SetDefaultValue/SetValue path does)
        dd:OverrideText(Placeholder())

        picker:Layout()
        onHeightChanged()
    end

    dd = namespace:CreateDropdown(picker, nil, BuildInfoTable(), OnPick)
    dd:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -Theme.GAP)
    dd:OverrideText(Placeholder())

    -- Seed pills for spells already enabled (existing per_spec entries), so a re-opened
    -- Settings screen reflects prior selections. The dropdown's exclusion already accounts
    -- for `selected` via BuildInfoTable, so no separate rebuild is needed here.
    if existing then
        for spellID in pairs(existing) do
            AddPill(spellID)
        end
    end

    -- Seed pills for stored trinket selections (trinket support). A stored itemID that is
    -- NOT currently in slot 13/14 still gets a pill -- greyed, not dropped -- so a gear
    -- swap never silently erases the user's configuration; re-equipping restores the
    -- check with no reconfiguration.
    if existingItems then
        for itemID in pairs(existingItems) do
            local pill = AddItemPill(itemID)
            if not IsTrinketEquipped(itemID) then
                pill.name:SetTextColor(Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, Theme.TEXT_DIM.a)
            end
        end
    end

    -- picker:Layout(): reflow pills left-to-right within the picker's bound width,
    -- wrapping to a new row when the next pill would overflow. DEFERRED to the caller
    -- (SettingsScreen) to invoke AFTER anchoring the picker -- a wrapping pill flow only
    -- measures correctly once the picker's width is bound (same width-before-measure rule
    -- as BuildWidgets' `text` builder).
    --
    -- Two-pass because the old assumption of one uniform pill height no longer
    -- holds: a charges-dropdown pill is Theme.H_DROPDOWN tall (28) while a plain
    -- pill is PILL_HEIGHT tall (22), so the row's vertical step must follow the
    -- TALLEST pill on that row, not the first one seen. Pass 1 walks pills in
    -- insertion order with the same wrap rule as before, recording each pill's
    -- row index, x offset and per-row tallest height; pass 2 converts those row
    -- heights into cumulative Y offsets and positions every pill.
    function picker:Layout()
        local availWidth = picker:GetWidth()
        local flowH = 0

        if #pills > 0 and availWidth and availWidth > 0 then
            -- Pass 1: wrap decision + per-pill bookkeeping.
            local pillRow = {}
            local pillXOff = {}
            local rowHeights = {}
            local x, row = 0, 0

            for i, pill in ipairs(pills) do
                local pillW = pill:GetWidth()

                if x > 0 and x + pillW > availWidth then
                    x = 0
                    row = row + 1
                end

                pillRow[i] = row
                pillXOff[i] = x
                rowHeights[row] = math.max(rowHeights[row] or 0, pill:GetHeight())

                x = x + pillW + ROW_GAP
            end

            -- Pass 2: convert per-row heights into cumulative Y offsets.
            -- rowY[row] = the Y offset where row `row` starts, measured from the
            -- top of the flow area (headerH + Theme.GAP + Theme.H_DROPDOWN +
            -- Theme.GAP). Pass 2's trailing + ROW_GAP after each row's own
            -- height is the gap to the next row; the final flowH trims the
            -- trailing gap.
            local rowY = {}
            local cumulative = 0
            local r = 0
            while rowHeights[r] do
                rowY[r] = cumulative
                cumulative = cumulative + rowHeights[r] + ROW_GAP
                r = r + 1
            end
            flowH = math.max(0, cumulative - ROW_GAP)

            for i, pill in ipairs(pills) do
                local pillRowIdx = pillRow[i]
                local rowYOffset = headerH + Theme.GAP + Theme.H_DROPDOWN + Theme.GAP + rowY[pillRowIdx]
                pill:ClearAllPoints()
                pill:SetPoint("TOPLEFT", picker, "TOPLEFT", pillXOff[i], -rowYOffset)
            end
        end

        picker:SetHeight(headerH + Theme.GAP + Theme.H_DROPDOWN + Theme.GAP + flowH)
    end

    return picker
end
