-- DST_API.lua
-- Build 42.x — Core registry + hook API for modular skill tooltips
-- Purpose:
--   • Allow base mod + other mods to register skill tooltip content per level
--   • Provide additive hooks so external mods can append lines or support new skills
-- Inputs: registration calls from per-skill files and third-party mods
-- Outputs: merged level tables (level_1..level_10) for UI rendering

local DST = rawget(_G, "DST") or {}
DST.SkillTooltips = DST.SkillTooltips or {}
local ST = DST.SkillTooltips

-- Internal storage
ST._base = ST._base or {}           -- skillKey -> base builder | static table
ST._contributors = ST._contributors or {}  -- skillKey -> {fn, fn, ...}

-- Tiny event bus (global)
ST.onBuild = ST.onBuild or {
    _handlers = {},
    Add = function(self, fn)
        if type(fn) == "function" then table.insert(self._handlers, fn) end
    end,
    Remove = function(self, fn)
        for i=#self._handlers,1,-1 do if self._handlers[i] == fn then table.remove(self._handlers, i) end end
    end,
    _emit = function(self, ctx)
        -- ctx: { skill=string, level=number, add=function(text) end, addMany=function(list) end }
        for _, fn in ipairs(self._handlers) do
            local ok, err = pcall(fn, ctx)
            if not ok then print("[DST] onBuild handler error: "..tostring(err)) end
        end
    end
}

--- Normalize a level table: ensure array of strings, never nil
local function _norm(tbl)
    if type(tbl) ~= "table" then return {} end
    -- Copy only array part
    local out = {}
    for i=1,#tbl do
        local v = tbl[i]
        if v ~= nil then out[#out+1] = tostring(v) end
    end
    return out
end

--- Merge two arrays (append b onto a)
local function _append(a, b)
    for i=1,#b do a[#a+1] = b[i] end
end

-----------------------------------------------------------------------
-- TRANSLATION FALLBACK (mod keys → fallback to EN if missing)
-- Usage: ST.t("IGUI_DST_...", arg1, arg2, ...)
-----------------------------------------------------------------------
local ST = DST and DST.SkillTooltips or DST_SkillTooltipAPI or _G.DST.SkillTooltips

-----------------------------------------------------------------------
-- PERK FALLBACKS + Resolver (shared, Build 42.x)
-- Maps internal/legacy names to the skill definition keys you use.
-- Exported as ST.PERK_FALLBACKS and ST.resolveSkillKey(perkOrName)
-----------------------------------------------------------------------
ST.PERK_FALLBACKS = ST.PERK_FALLBACKS or {
    -- Combat - Ranged
    Aiming          = "Aiming",
    Reloading       = "Reloading",

    -- Combat - Melee
    SmallBlunt      = "ShortBlunt",
    Blunt           = "LongBlunt",
    SmallBlade      = "ShortBlade",
    LongBlade       = "LongBlade",
    Axe             = "Axe",
    Spear           = "Spear",
    Maintenance     = "Maintenance",

    -- Crafting
    Woodwork        = "Carpentry",
    Carving         = "Carving",
    Cooking         = "Cooking",
    Electricity     = "Electrical",
    Glassmaking     = "Glassmaking",
    FlintKnapping   = "Knapping",
    Masonry         = "Masonry",
    Blacksmith      = "Metalworking",
    Mechanics       = "Mechanics",
    Pottery         = "Pottery",
    Tailoring       = "Tailoring",
    MetalWelding    = "Welding",

    -- Farming
    Farming         = "Agriculture",
    Husbandry       = "AnimalCare",
    Butchering      = "Butchering",

    -- Physical
    Fitness         = "Fitness",
    Strength        = "Strength",
    Lightfoot       = "Lightfooted",
    Nimble          = "Nimble",
    Sprinting       = "Running",
    Sneak           = "Sneaking",

    -- Survival
    Doctor          = "FirstAid",
    Fishing         = "Fishing",
    PlantScavenging = "Foraging",
    Tracking        = "Tracking",
    Trapping        = "Trapping",
}

--- Resolve a Perk object or string into the definition key you use
-- @param perkOrName Perk|String
-- @return string|nil
function ST.resolveSkillKey(perkOrName)
    local name = nil
    if type(perkOrName) == "string" then
        name = perkOrName
    elseif perkOrName and perkOrName.getType then
        local ok, t = pcall(function() return perkOrName:getType() end)
        name = (ok and t ~= nil) and tostring(t) or tostring(perkOrName)
    end
    if not name or name == "" then return nil end
    local cap = name:gsub("^%l", string.upper)
    local mapped = ST.PERK_FALLBACKS[name] or ST.PERK_FALLBACKS[cap]
    return mapped or cap
end

-- Detect if a string starts with our mod's translation prefix (keeps scope tight)
local function _startsWith(str, prefix)
    return type(str) == "string" and type(prefix) == "string" and str:sub(1, #prefix) == prefix
end

-- Simple %N replacer for our fallback path (PZ getText uses %1, %2,...)
local function _subst_placeholders(s, ...)
    if type(s) ~= "string" then return s end
    local n = select("#", ...)
    for i = 1, n do
        local v = tostring(select(i, ...) or "")
        -- replace %1, %2, ... (do NOT touch literal '%' otherwise)
        s = s:gsub("%%" .. i, v)
    end
    return s
end

-- Lazy-load IGUI_EN once (your EN file defines IGUI_EN = { ... })
local _EN_LOADED = false
local function _ensureEN()
    if _EN_LOADED then return end
    _EN_LOADED = true
    -- If not already present, try to require your EN table.
    -- Path matches media/lua/shared/Translate/EN/IGUI_EN.lua
    if not rawget(_G, "IGUI_EN") then
        pcall(function() require("Translate/EN/IGUI_EN") end)
    end
end

-----------------------------------------------------------------------
-- TRANSLATION WRAPPER (safe fallback to English for our mod keys)
-- Usage: ST.getText("IGUI_DST_...", arg1, arg2, ...)
-----------------------------------------------------------------------
function ST.getText(key, ...)
    local function startsWith(str, prefix)
        return type(str) == "string" and str:sub(1, #prefix) == prefix
    end
    local function subst_placeholders(s, ...)
        if type(s) ~= "string" then return s end
        local n = select("#", ...)
        for i = 1, n do
            local v = tostring(select(i, ...) or "")
            s = s:gsub("%%" .. i, v) -- replace %1, %2, ...
        end
        return s
    end

    -- 1) Try the game’s getText first
    local ok, txt = pcall(getText, key, ...)
    if ok and type(txt) == "string" and txt ~= key then
        return txt
    end

    -- 2) Only apply fallback for our namespace
    if not startsWith(key, "IGUI_DST_") then
        return (type(txt) == "string" and txt) or tostring(key)
    end

    -- 3) Fallback to English table if present
    if not rawget(_G, "IGUI_EN") then
        pcall(function() require("Translate/EN/IGUI_EN") end)
    end
    local en = rawget(_G, "IGUI_EN")
    if type(en) == "table" and type(en[key]) == "string" then
        return subst_placeholders(en[key], ...)
    end

    -- 4) Last resort: return the key itself
    return tostring(key)
end

--- Public: define a skill's base content
-- You can pass:
--   • a static table: { level_1={...}, ..., level_10={...} }
--   • a builder function(skillKey) -> same table
function ST.define(skillKey, defOrBuilder)
    skillKey = tostring(skillKey or "")
    if skillKey == "" then return end
    ST._base[skillKey] = defOrBuilder
end

--- Public: add a contributor for a specific skill
-- fn(ctx) where:
--   ctx.skill  = skillKey
--   ctx.level  = 1..10
--   ctx.add(t) = append single string line
--   ctx.addMany(list) = append many lines
function ST.addContributor(skillKey, fn)
    skillKey = tostring(skillKey or "")
    if skillKey == "" or type(fn) ~= "function" then return end
    ST._contributors[skillKey] = ST._contributors[skillKey] or {}
    table.insert(ST._contributors[skillKey], fn)
end

--- Public: add a "computed" contributor (sugar)
-- fn(level, ctx) -> {lines} | nil
-- Internally wraps into ST.addContributor so you can focus on level-based output.
function ST.addContributor(skillKey, fn)
    skillKey = tostring(skillKey or "")
    if skillKey == "" or type(fn) ~= "function" then return end

    -- Canonicalize to the resolved (fallback) key
    local canon = ST.resolveSkillKey(skillKey) or skillKey

    -- Store under canonical key
    ST._contributors[canon] = ST._contributors[canon] or {}
    table.insert(ST._contributors[canon], fn)

    -- Also alias the raw key to the same table (so both lookups hit)
    if skillKey ~= canon then
        ST._contributors[skillKey] = ST._contributors[canon]  -- shared reference
    end
end

-----------------------------------------------------------------------
-- EASY MODE HELPERS (non-breaking sugar for modders)
-----------------------------------------------------------------------

--- Public: define static lines by level in one call.
-- Usage:
--   DST.SkillTooltips.addStaticLevels("Carpentry", {
--       [1] = { "Unlocks basic boards.", "Allows simple barricades." },
--       [2] = { "Improved board yield." },
--   })
function ST.addStaticLevels(skillKey, levels)
    if type(levels) ~= "table" then return end
    ST.addContributor(skillKey, function(ctx)
        local lvlLines = levels[ctx.level]
        if lvlLines and #lvlLines > 0 then
            ctx.addMany(lvlLines)
        end
    end)
end

--- Public: add simple lines for a single level (can be called many times).
-- Usage:
--   DST.SkillTooltips.addLevelLines("Carpentry", 1, "Line A", "Line B")
function ST.addLevelLines(skillKey, level, ...)
    local args = { ... }
    if type(level) ~= "number" or #args == 0 then return end
    ST.addContributor(skillKey, function(ctx)
        if ctx.level == level then ctx.addMany(args) end
    end)
end

--- Public: add a super simple computed builder without needing ctx plumbing.
-- fn(level) -> array-of-lines | string | nil
-- Usage:
--   DST.SkillTooltips.addSimpleComputed("Fishing", function(level)
--       if level >= 5 then return { "Higher catch chance.", "Access to better traps." }
--       else return "Basic fishing actions." end
--   end)
function ST.addSimpleComputed(skillKey, fn)
    if type(fn) ~= "function" then return end
    ST.addContributor(skillKey, function(ctx)
        local ok, res = pcall(fn, ctx.level)
        if not ok or res == nil then return end
        if type(res) == "string" then ctx.add(res)
        elseif type(res) == "table" then ctx.addMany(res) end
    end)
end

--- Public: one-call "register" that accepts a table spec.
-- Usage:
--   DST.SkillTooltips.register({
--       skill   = "Carpentry",
--       static  = { [1] = {"L1 line"}, [2] = {"L2 line"} },
--       compute = function(level) if level==3 then return "L3 extra" end end,
--   })
function ST.register(spec)
    if type(spec) ~= "table" then return end
    local skill = spec.skill or spec.name or spec.key
    if not skill then return end

    if type(spec.static) == "table" then
        ST.addStaticLevels(skill, spec.static)
    end
    if type(spec.compute) == "function" then
        ST.addSimpleComputed(skill, spec.compute)
    end
    -- Optional: allow extras = array-of-contributors
    if type(spec.extras) == "table" then
        for _, fn in ipairs(spec.extras) do
            if type(fn) == "function" then ST.addContributor(skill, fn) end
        end
    end
end

--- Public: one-line convenience for external mods.
-- Usage from another mod:
--   require "API/DST_API"
--   local ST = DST.SkillTooltips
--   ST.quick("Fishing", {
--       static = { [1]={"L1"}, [2]={"L2"} },
--       compute = function(level) if level>=5 then return "Tier 5 bonus" end end
--   })
function ST.quick(skillKey, spec)
    if not skillKey or type(spec) ~= "table" then return end
    spec.skill = skillKey
    ST.register(spec)
end

-- Helper for contributors (replacement)
-- Purpose: provide a unified ctx.add(text, opts) that can handle plain or colored lines,
--          plus addHeader sugar and a hasEntries() probe.
-- Inputs:
--   level (number)       - perk level for the contributor block
--   targetArr (table)    - underlying array to append tooltip lines into
--   cumulativeFlag:
--      false / nil  => show ONLY unlocks gained exactly at this level
--      true         => show ALL unlocks from level 1..this level (character creation view)
-- Outputs:
--   ctx (table) with methods:
--     ctx:add(text, opts?)         -- opts = { gap=bool, color={r,g,b} (0..1 or 0..255), reset=bool=true }
--     ctx:addMany(list)            -- append many plain lines (no color)
--     ctx:addHeader(text)          -- yellow header + conditional gap
--     ctx:hasEntries()             -- whether targetArr already has entries
local function _mkCtx(level, targetArr, cumulativeFlag)
    -- Local helpers ---------------------------------------------------------

    -- Clamp a number to [0,1]
    local function _clamp01(x)
        if x < 0 then return 0 end
        if x > 1 then return 1 end
        return x
    end

    -- Accept {r,g,b} in 0..1 or 0..255; normalize to 0..1 and clamp
    local function _normRGB(rgb)
        if type(rgb) ~= "table" then return 1, 1, 1 end
        local r = tonumber(rgb.r or rgb[1] or 1) or 1
        local g = tonumber(rgb.g or rgb[2] or 1) or 1
        local b = tonumber(rgb.b or rgb[3] or 1) or 1
        if r > 1 or g > 1 or b > 1 then
            r, g, b = r/255, g/255, b/255
        end
        return _clamp01(r), _clamp01(g), _clamp01(b)
    end

    -- Build a <RGB:...> tag with short floats
    local function _rgbTag(r, g, b)
        return string.format("<RGB:%.3f,%.3f,%.3f>", r, g, b)
    end

    -- Context object --------------------------------------------------------
    local ctx = {
        skill = skillKey,
        level = level,
        cumulative  = (cumulativeFlag == true),
    }

    --- Return true if any entries already queued for this level
    function ctx.hasEntries(self)
        return #targetArr > 0
    end

    -- Place near other locals inside _mkCtx:
    local _DEFAULT_R, _DEFAULT_G, _DEFAULT_B = _normRGB(ST.COLORS.white)
    local _pendingReset = false

    --- Add a single line (plain or colored) with optional conditional gap.
    -- @param text  (string) required; no-op if nil/empty
    -- @param opts  (table|nil)
    --        - gap   (bool)  : insert ST.GAP iff there are existing lines
    --        - color (table) : {r,g,b} in 0..1 or 0..255; wraps with <RGB:...>
    --        - reset (bool)  : default true; when color is set, request a reset
    function ctx.add(text, opts)
        if text == nil then return end
        local s = tostring(text)
        if s == "" then return end
        opts = opts or {}

        -- Conditional spacer only if something already exists
        if opts.gap and #targetArr > 0 then
            targetArr[#targetArr+1] = ST.GAP
        end

        local hasColor = opts.color ~= nil
        local prefix = ""

        -- If we owe a reset from the previous colored line, handle it inline
        if _pendingReset then
            if hasColor then
                -- New color supersedes the reset; drop the reset
                _pendingReset = false
            else
                -- Prefix the reset tag to THIS plain line (no extra line!)
                prefix = _rgbTag(_DEFAULT_R, _DEFAULT_G, _DEFAULT_B)
                _pendingReset = false
            end
        end

        if hasColor then
            local r, g, b = _normRGB(opts.color)
            prefix = prefix .. _rgbTag(r, g, b)         -- start the requested color
            targetArr[#targetArr+1] = prefix .. s       -- emit colored line

            -- Request a reset for the *next* line (not appended here)
            if opts.reset ~= false then
                _pendingReset = true
            end
            return
        end

        -- Plain line (possibly prefixed with a reset tag)
        targetArr[#targetArr+1] = prefix .. s
    end

    -- Optional: call at the end of a contributor if you insist on default color,
    -- but we STILL avoid ending with a tag by putting a safe char after it.
    function ctx.finish()
        if _pendingReset then
            targetArr[#targetArr+1] = _rgbTag(_DEFAULT_R, _DEFAULT_G, _DEFAULT_B) .. "."
            -- Remove the dot again if your joiner will add another line after this.
            _pendingReset = false
        end
    end

    --- Add multiple lines, delegating to ctx.add so color/reset/gap rules apply.
    -- @param list (table) array of strings OR tables:
    --        { text=string, color={r,g,b}|nil, gap=bool|nil, reset=bool|nil }
    -- @param opts (table|nil) default opts applied to each string item (ignored for table items)
    function ctx.addMany(list, opts)
        if type(list) ~= "table" then return end
        local arr = _norm(list) or list  -- keep your existing normalizer if present

        for i = 1, #arr do
            local v = arr[i]
            local vt = type(v)

            if vt == "string" then
                -- Use defaults passed via opts (e.g., a section color for all lines)
                ctx.add(v, opts)

            elseif vt == "table" then
                -- Structured entry; per-item options override any defaults
                local t = v.text or v[1]
                if t and t ~= "" then
                    ctx.add(t, {
                        gap   = v.gap   or false,
                        color = v.color or (opts and opts.color) or nil,
                        reset = (v.reset ~= nil) and v.reset or (opts and opts.reset)
                    })
                end
            end
            -- nil/other types are ignored
        end
    end

    --- Add a section header (yellow) with a conditional gap if entries already exist.
    -- You can swap ST.COLORS.yellow if you maintain a palette table.
    function ctx.addHeader(text)
        if text == nil then return end
        local needGap = (ctx.hasEntries() == true)
        -- Default to bright yellow; override here if you adopt ST.COLORS.yellow
        ctx.add(text, { gap = needGap, color = ST.COLORS.yellow, reset = true })
    end

    -- Separator
    function ctx.addSeparator()
        ctx.add(ST.GAP)
    end

    function ctx.getLevel()
        return DST.Helpers.clampLevel(ctx.level)
    end

    return ctx
end

--- Build the final level table for a skill (base + contributors + global hooks)
-- Returns a table with keys level_1..level_10, each an array of strings.
function ST.get(skillKey, cumulative)
    skillKey = tostring(skillKey or "")
    if skillKey == "" then return nil end

    skillKey = ST.resolveSkillKey(skillKey) or skillKey

    -- 1) Resolve base
    local base = ST._base[skillKey]
    local built = nil
    if type(base) == "function" then
        local ok, res = pcall(base, skillKey)
        if ok and type(res) == "table" then built = res end
    elseif type(base) == "table" then
        built = base
    end
    if not built then
        -- No base exists yet; start empty scaffold
        built = {}
        for i=1,10 do built["level_"..i] = {} end
    end

    -- 2) Normalize base levels
    for i=1,10 do
        built["level_"..i] = _norm(built["level_"..i])
    end

    -- 3) Apply skill-specific contributors
    local list = ST._contributors[skillKey]
    if type(list) == "table" then
        for lvl=1,10 do
            local arr = built["level_"..lvl]
            local ctx = _mkCtx(lvl, arr, cumulative)
            for _, fn in ipairs(list) do
                local ok, err = pcall(fn, ctx)
                if not ok then print("[DST] contributor error for "..skillKey..": "..tostring(err)) end
            end
        end
    end

    -- 4) Emit global onBuild hooks (for mods that want to target multiple skills)
    for lvl=1,10 do
        local arr = built["level_"..lvl]
        local ctx = _mkCtx(lvl, arr, false)
        ST.onBuild:_emit(ctx)
    end

    return built
end

_G.DST = DST  -- expose back to global
return DST
