require "ISUI/ISSkillProgressBar"
require "API/DST_API"
require "API/DST_Unlocks"
require "API/DST_Helpers"
require "API/DST_Options"

DST = DST or {}
ST = DST.SkillTooltips or {}

-- Use the shared/global resolver from DST_API
-- Safe alias: falls back to a simple string if API isn't ready yet.
local RESOLVE = function(perkOrName)
    local ST = DST and DST.SkillTooltips
    if ST and ST.resolveSkillKey then
        return ST.resolveSkillKey(perkOrName)
    end
    -- Minimal fallback: string-capitalize, avoids a hard crash if API loads later.
    if type(perkOrName) == "string" then
        return perkOrName:gsub("^%l", string.upper)
    end
    return tostring(perkOrName or "")
end

----------------------------------------------------------------
-- auto learn cache bootstrap and low noise deferred retry
-- Purpose: ensure DST.Unlocks Auto-Learn cache is built when data
--          is actually available. Avoid log spam; try a few times.
--
-- Events used:
--   - OnGameStart       : first pass when game boots a save
--   - OnCreatePlayer    : covers cases where player object gates data
--   - EveryOneMinute    : bounded, low-noise retry loop
--
-- Notes:
--   - Zero behavior change to formatting/results; we just decide WHEN to build.
--   - No MP sync concerns in B42 (SP only). Revisit when MP returns.
-- ⚠️ Empirical — tested in Build 42.11. Requires in-game SP testing.
----------------------------------------------------------------
-- Safe local copy of `next` so other mods can't break us.
local _dst_next = type(next) == "function" and next or function() return nil end

--- Return true if t is a non-empty table, using our safe iterator.
-- @param t any
-- @return boolean
local function _dst_hasEntries(t)
    return type(t) == "table" and _dst_next(t) ~= nil
end

-- Shared state for bootstrap cycle
DST._AutoLearnBoot = DST._AutoLearnBoot or {
    maxRetries     = 5,     -- change if your pack load is slower
    tries          = 0,
    success        = false,
    printedFail    = false,
    isLoopActive   = false,
}

--- Heuristics: decide if the world is "ready enough" to build.
-- Uses only defensive checks so it won't explode in cursed mod stacks.
-- @return boolean
local function _dst_isWorldReady()
    -- Player exists?
    local p = getSpecificPlayer and getSpecificPlayer(0) or nil

    -- Recipes loaded? (Lua-only heuristic: any recipes indexed)
    local anyRecipes = false
    local sm = getScriptManager and getScriptManager() or nil
    if sm and sm.getAllRecipes then
        -- getAllRecipes() returns an ArrayList in B42; size() > 0 indicates loaded.
        local all = sm:getAllRecipes()
        anyRecipes = (all ~= nil) and (type(all.size)=="function") and (all:size() > 0) or false
    else
        -- Fallback: if Unlocks cache already has any rows, treat as "ok"
        local C = DST.Unlocks and DST.Unlocks.Cache or nil
        local byPerk = C and C.AutoLearnByPerk or nil
        anyRecipes = _dst_hasEntries(byPerk)
    end

    return (anyRecipes and p ~= nil) or false
end

--- True when the Auto-Learn cache has at least one entry.
-- @return boolean
local function _dst_isAutoLearnBuilt()
    local C = DST.Unlocks and DST.Unlocks.Cache or nil
    local byPerk = C and C.AutoLearnByPerk or nil
    return _dst_hasEntries(byPerk)
end

-- One-shot attempt to build the Auto-Learn cache
local function _dst_tryBuildAutoLearn()
    local boot = DST._AutoLearnBoot
    if boot.success then return true end
    if not (DST.Unlocks and DST.Unlocks.buildAutoLearnCache) then
        -- Module not loaded yet; let the retry loop pick it up after require-order settles
        return false
    end
    if not _dst_isWorldReady() then
        return false
    end

    -- Attempt build
    local ok = false
    local okStatus, err = pcall(function()
        DST.Unlocks.buildAutoLearnCache()
        ok = _dst_isAutoLearnBuilt()
    end)

    if not okStatus then
        -- Hard failure in builder: keep quiet except a single-line debug once
        if not boot.printedFail then
            print("[DST] AutoLearn build threw (will retry quietly): " .. tostring(err))
            boot.printedFail = true
        end
        return false
    end

    if ok then
        boot.success = true
        if boot.printedFail or boot.tries > 0 then
            -- Only announce if we previously failed/retired; keep noise low
            print("[DST] AutoLearn cache built.")
        end
        return true
    else
        if not boot.printedFail then
            print("[DST] AutoLearn build deferred (data not ready yet).")
            boot.printedFail = true
        end
        return false
    end
end

-- Minute-based bounded retry loop (low noise)
local function _dst_startRetryLoop()
    local boot = DST._AutoLearnBoot
    if boot.isLoopActive or boot.success then return end
    boot.isLoopActive = true

    -- Keep a reference so we can remove it
    if DST._AutoLearnRetryTick then
        Events.EveryOneMinute.Remove(DST._AutoLearnRetryTick)
        DST._AutoLearnRetryTick = nil
    end

    DST._AutoLearnRetryTick = function()
        if boot.success then
            Events.EveryOneMinute.Remove(DST._AutoLearnRetryTick)
            DST._AutoLearnRetryTick = nil
            boot.isLoopActive = false
            return
        end
        if boot.tries >= boot.maxRetries then
            -- Give up silently (we already printed once on the first miss)
            Events.EveryOneMinute.Remove(DST._AutoLearnRetryTick)
            DST._AutoLearnRetryTick = nil
            boot.isLoopActive = false
            return
        end

        boot.tries = boot.tries + 1
        local okNow = _dst_tryBuildAutoLearn()
        if okNow then
            Events.EveryOneMinute.Remove(DST._AutoLearnRetryTick)
            DST._AutoLearnRetryTick = nil
            boot.isLoopActive = false
            return
        end
        -- otherwise: keep looping; no extra prints here (low noise)
    end

    Events.EveryOneMinute.Add(DST._AutoLearnRetryTick)
end

-- Event handlers (registered once)
if not DST._AutoLearnHandlersAdded then
    DST._AutoLearnHandlersAdded = true

    -- Eager build at game start
    Events.OnGameStart.Add(function()
        if _dst_tryBuildAutoLearn() then return end
        _dst_startRetryLoop()
    end)

    -- Also try when the player object is created
    Events.OnCreatePlayer.Add(function(pid, playerObj)
        if _dst_tryBuildAutoLearn() then return end
        _dst_startRetryLoop()
    end)
end

---@diagnostic disable-next-line: duplicate-set-field
function ISSkillProgressBar:render()
    self:renderPerkRect()
    if self.message ~= nil then
        if self.tooltip == nil then
            self.tooltip = ISToolTip:new()
            self.tooltip:initialise()
            self.tooltip:addToUIManager()
            self.tooltip:setOwner(self)
            self.tooltip.maxLineWidth = 250
        end

        ----------------------------------------------------------------
        -- Keep skill tooltip above other UI (Build 42.x)
        -- Tries setAlwaysOnTop(true) if present, and always bringToTop().
        -- If the class lacks bringToTop(), re-add to UIManager as a last resort.
        ----------------------------------------------------------------
        do
            local t = self.tooltip
            if t then
                -- Prefer an API flag if it exists
                if type(t.setAlwaysOnTop) == "function" then
                    -- Some ISUIElements support this; harmless if already true
                    t:setAlwaysOnTop(true)
                end

                -- Bring to front in the z-order each render while visible
                if type(t.bringToTop) == "function" then
                    t:bringToTop()
                else
                    -- Fallback: remove and re-add to push to the top
                    if type(t.removeFromUIManager) == "function" and type(t.addToUIManager) == "function" then
                        -- Temporary Hack (Build 42.x) — z-order refresh via re-add; may cause minor flicker if called too often
                        t:removeFromUIManager()
                        t:addToUIManager()
                    end
                end
            end
        end

        -- Update text
        self.tooltip.description = self.message
		self.tooltip:setDesiredPosition(self:getAbsoluteX(), self:getAbsoluteY() + self:getHeight() + 8)
    end
end

----------------------------------------------------------------
-- Close tooltip when a pip is clicked (Build 42.x)
-- Purpose: If the user clicks a level pip while its tooltip is visible,
--          hide the tooltip immediately so it doesn't linger behind UI.
----------------------------------------------------------------

--- Hide and dispose the current tooltip (if any) for this pip.
-- @return void
function ISSkillProgressBar:DST_hideTooltip()
    -- Clear message so render() won't recreate the tooltip right away
    self.message = nil
    if self.tooltip then
        -- Remove from UI manager and drop ref
        self.tooltip:removeFromUIManager()
        self.tooltip = nil
    end
end

-- Preserve vanilla/other-mod behavior
DST._old_ISSPB_onMouseDown = DST._old_ISSPB_onMouseDown or ISSkillProgressBar.onMouseDown

--- Wrapper: hide tooltip first, then run original click logic.
-- @param x number Local mouse X
-- @param y number Local mouse Y
-- @return boolean handled
function ISSkillProgressBar:onMouseDown(x, y)
    -- Close our tooltip pre-click so it can't end up behind the character sheet
    -- if self.DST_hideTooltip then self:DST_hideTooltip() end

    -- Chain to original behavior (vanilla or another mod)
    if type(DST._old_ISSPB_onMouseDown) == "function" then
        return DST._old_ISSPB_onMouseDown(self, x, y)
    end
    return true
end

---------------------------------------------------------------------
-- DST tooltip spacers (public constants)
---------------------------------------------------------------------
ST.GAP       = " "          -- "<DST:GAP_TIGHT>"   -- preferred small spacer between sections
ST.GAP_WIDE  = " <LINE>"    -- "<DST:GAP_WIDE>"    -- rarely: true wide paragraph gap

---------------------------------------------------------------------
-- DST.SkillTooltips.appendLevelTooltip
-- Purpose:
--   Given a perk (id or name), a level (1..10), and an optional
--   existing tooltip string, return the concatenated string with the
--   level-specific lines appended using DST spacing rules.
--
-- Inputs:
--   perkOrName (string|enum)  -- Perk id or display key
--   level     (number)        -- Level in [1..10] (caller should convert)
--   existing  (string|nil)    -- Existing tooltip text to append to
--
-- Output:
--   (string) Concatenated tooltip text
--
-- Notes:
--   - Pure string function; no UI side-effects.
--   - Uses ST.get() to retrieve the final merged table for the perk.
--   - Uses ST.GAP_WIDE between sections, matching existing behavior.
---------------------------------------------------------------------
function ST.appendLevelTooltip(perkOrName, level, existing, cumulative)
    local msg = (type(existing) == "string") and existing or ""
    -- Resolve the key via shared resolver (safe even if API loads late)
    local key = RESOLVE(perkOrName)
    if not (key and ST and ST.get) then
        return msg
    end

    local def = ST.get(key, cumulative == true)
    if not def then
        return msg
    end

    local lvlKey = "level_" .. tostring(level or "")
    local lines = def[lvlKey]

    if lines and #lines > 0 then
        -- Start a new paragraph, then each line gets its own wide gap prefix
        local block = ST.GAP_WIDE
        for i = 1, #lines do
            local line = tostring(lines[i])
            if line ~= "" then
                block = block .. ST.GAP_WIDE .. line
            end
        end
        msg = msg .. block
    end

    return msg
end

function ISSkillProgressBar:DST_updateTooltip(lvlSelected)
    -- Convert pip index (0-based) to level (1..10)
    local level = (lvlSelected or 0) + 1
    -- Ensure self.message is a string
    if type(self.message) ~= "string" then self.message = "" end
    -- Append the level lines for this perk to the existing message
    -- DebugLog.log(DebugType.General, "[DST] Perk: " .. tostring(self.perk))
    self.message = ST.appendLevelTooltip(self.perk, level, self.message)
end


DST.updateTooltip_base = ISSkillProgressBar.updateTooltip

---@diagnostic disable-next-line: duplicate-set-field
function ISSkillProgressBar:updateTooltip(lvlSelected)
    -- Call the previously-installed method
    if DST.updateTooltip_base then
        DST.updateTooltip_base(self, lvlSelected)
    end
    -- Then apply augmentation.
    if self.DST_updateTooltip then
        self:DST_updateTooltip(lvlSelected)
    end
end

--=====================================================================
-- DST Integration — Inject tooltips for XP Boost listbox
-- Hook CharacterCreationProfession:checkXPBoost so we rebuild tooltips
-- whenever the XP boost list is rebuilt.
--=====================================================================
if DST and DST.SkillTooltips and ST and ST.appendLevelTooltip then
    local _old_checkXPBoost = CharacterCreationProfession.checkXPBoost

    ---@diagnostic disable-next-line: duplicate-set-field
    function CharacterCreationProfession:checkXPBoost(...)
        -- Run vanilla logic first
        if _old_checkXPBoost then
            _old_checkXPBoost(self, ...)
        end

        local list = self.listboxXpBoost
        if not list or not list.items or #list.items == 0 then
            return
        end

        -- Localised “ level ” (already includes spaces)
        local lvlLabel = getText("IGUI_XP_level")

        for i = 1, #list.items do
            local row = list.items[i]
            local data = row and row.item
            if data then
                local perkRef = data.perk
                local perkLevel = data.level

                -- Localised perk name
                local perkDisplayName
                ---@diagnostic disable-next-line: undefined-field
                if type(perkRef) == "userdata" and perkRef.getName then
                    ---@diagnostic disable-next-line: undefined-field
                    perkDisplayName = perkRef:getName()
                else
                    perkDisplayName = PerkFactory.getPerkName(perkRef)
                end
                if not perkDisplayName or perkDisplayName == "" then
                    perkDisplayName = tostring(perkRef or "???")
                end

                -- Header (matches ISSkillProgressBar:updateTooltip)
                local header = perkDisplayName .. lvlLabel .. tostring(perkLevel)

                -- Append DST breakdown
                local tip = ST.appendLevelTooltip(perkRef, perkLevel, header, true)

                row.tooltip = tip
            end
        end
    end
end
