-- Custom item tooltip with styled OTUI widgets

local DEBUG = false
if isLocalEnv and isLocalEnv() then DEBUG = true end

local function dbg(fmt, ...)
  if not DEBUG then return end
  local ok, msg = pcall(string.format, fmt, ...)
  g_logger.info('[itemtooltip] ' .. (ok and msg or fmt))
end

-- Custom tooltip widget
local tooltipWidget
local anchoredWidget
local lastItem
local lastMeta

local ELEM_NAMES = {
  [0] = 'None', [1] = 'Fire', [2] = 'Ice', [3] = 'Earth', [4] = 'Energy'
}

local function msFmt(ms)
  return (ms and ms > 0) and string.format('%d ms', ms) or nil
end

local function ozFmt(weight)
  if not weight or weight <= 0 then return nil end
  local oz = weight / 100.0
  local formatted = string.format('%.2f', oz)
  -- Remove trailing zeros and decimal point if whole number
  formatted = formatted:gsub('%.?0+$', '')
  return formatted .. ' oz'
end

local function nameFor(item, m)
  if m and m.name and m.name ~= '' then return m.name end
  if not item then return '' end
  local ok, nm = pcall(function() if item.getName then return item:getName() end end)
  if ok and nm and nm ~= '' then return nm end
  local t = g_things.getThingType(item:getId(), ThingCategoryItem)
  return (t and t.getName and t:getName()) or ''
end

-- Use centralized rarity colors from itemmeta
local function rarityToColor(rarity)
  if not _G.itemmeta or not _G.itemmeta.getRarityColor then
    -- Fallback if itemmeta not loaded yet
    return '#dfdfdf'
  end
  return _G.itemmeta.getRarityColor(rarity)
end

-- Derive darker background color from rarity color (very dark)
local function rarityToDarkBg(rarity)
  if rarity == 'rare' then return '#0d1f33' end        -- very dark blue
  if rarity == 'epic' then return '#25103a' end        -- very dark brighter purple
  if rarity == 'legendary' then return '#332500' end   -- very dark orange-gold
  if rarity == 'masterpiece' then return '#331a00' end -- very dark orange
  return '#333333'                                     -- dark grey
end

-- Derive lighter text color from rarity color (darker tones)
local function rarityToLightText(rarity)
  if rarity == 'rare' then return '#4a7fb8' end        -- darker blue
  if rarity == 'epic' then return '#a060d9' end        -- darker brighter purple
  if rarity == 'legendary' then return '#b89627' end   -- darker orange-gold
  if rarity == 'masterpiece' then return '#b16a27' end -- darker orange
  return '#888888'                                     -- darker grey
end

-- Break long item names roughly in the middle (on a space) to fit narrow tooltips.
local function breakNameMiddle(name)
  if not name or name == '' then return name end
  local len = #name
  -- Short names stay on a single line.
  if len <= 26 then return name end
  local mid = len / 2
  local best
  for pos in name:gmatch('() ') do
    if pos > 1 and pos < len then
      if not best or math.abs(pos - mid) < math.abs(best - mid) then
        best = pos
      end
    end
  end
  if not best then return name end
  return name:sub(1, best - 1) .. '\n' .. name:sub(best + 1)
end

-- Helpers for rendering base+bonus attributes consistently
local UPGRADE_COLOR = '#9f9dfd'
local BASE_COLOR = '#dfdfdf'

-- Modifier ids (must stay in sync with server/YAML)
local MOD_ID = {
  fire                = 1,
  ice                 = 2,
  earth               = 3,
  energy              = 4,
  atk_speed           = 5,
  crit_chance         = 6,
  life_leech          = 7,
  magic               = 8,
  hit_chance          = 9,
  defense             = 10,
  reflect             = 11,
  physical_resist     = 12,
  fire_resist         = 13,
  ice_resist          = 14,
  earth_resist        = 15,
  energy_resist       = 16,
  hp_regen            = 17,
  mp_regen            = 18,
  magic_level         = 19,
  skill_one_handed    = 20,
  skill_two_handed    = 21,
  skill_distance      = 22,
  skill_shield        = 23,
  cooldown_reduction  = 24,
  mana_cost_reduction = 25,
  move_speed          = 26,
  experience_rate     = 27,
  gold_rate           = 28,
  loot_rarity         = 29,
}

local function tierInfo(modName, level)
  if not _G.gameSpecs or not _G.gameSpecs.tierFor or not _G.gameSpecs.upgradesById then
    return nil, nil
  end
  local id = MOD_ID[modName]
  if not id then
    return nil, nil
  end
  local spec = _G.gameSpecs.upgradesById[id]
  if not spec or type(spec.tiers) ~= 'table' or #spec.tiers == 0 then
    return nil, nil
  end
  local lvl = tonumber(level or 0) or 0
  local maxTier = #spec.tiers
  if lvl <= 0 then
    return nil, maxTier
  end
  local tier = _G.gameSpecs.tierFor(id, lvl)
  if not tier or tier <= 0 then
    tier = 1
  end
  if tier > maxTier then
    tier = maxTier
  end
  return tier, maxTier
end

local function pushBasePlusPercent(lines, label, base, bonus)
  base = tonumber(base) or 0
  bonus = tonumber(bonus) or 0
  if base == 0 and bonus == 0 then return end
  if bonus > 0 then
    local total = base + bonus
    local valueStr
    if base == 0 then
      valueStr = string.format('{%d%%, %s}', total, UPGRADE_COLOR)
    else
      valueStr = string.format('{%d%%, %s}{ (%d+%d), %s}', total, UPGRADE_COLOR, base, bonus, BASE_COLOR)
    end
    table.insert(lines, string.format('{%s:, #888888} %s', label, valueStr))
  else
    table.insert(lines, string.format('{%s:, #888888} {%d%%, %s}', label, base, BASE_COLOR))
  end
end

local function pushBasePlusFlat(lines, label, base, bonus)
  base = tonumber(base) or 0
  bonus = tonumber(bonus) or 0
  if base == 0 and bonus == 0 then return end
  if bonus > 0 then
    local total = base + bonus
    local valueStr
    if base == 0 then
      valueStr = string.format('{%d, %s}', total, UPGRADE_COLOR)
    else
      valueStr = string.format('{%d, %s}{ (%d+%d), %s}', total, UPGRADE_COLOR, base, bonus, BASE_COLOR)
    end
    table.insert(lines, string.format('{%s:, #888888} %s', label, valueStr))
  else
    table.insert(lines, string.format('{%s:, #888888} {%d, %s}', label, base, BASE_COLOR))
  end
end
-- Base-only wrappers: show in Base section only when base is non-zero; fold bonus into the same line
local function showBasePlusPercent(lines, label, base, bonus)
  base = tonumber(base) or 0
  if base == 0 then return end
  pushBasePlusPercent(lines, label, base, tonumber(bonus) or 0)
end

local function showBasePlusFlat(lines, label, base, bonus)
  base = tonumber(base) or 0
  if base == 0 then return end
  pushBasePlusFlat(lines, label, base, tonumber(bonus) or 0)
end

local function pushBasePlusFloat(lines, label, base, bonus)
  base = tonumber(base) or 0
  bonus = tonumber(bonus) or 0
  if base == 0 and bonus == 0 then return end
  local function fmtf(x)
    local s = string.format('%.2f', x)
    s = s:gsub('%.?0+$', '')
    return s
  end
  if bonus > 0 then
    local total = base + bonus
    local valueStr
    if base == 0 then
      valueStr = string.format('{%s, %s}', fmtf(total), UPGRADE_COLOR)
    else
      valueStr = string.format('{%s, %s}{ (%s+%s), %s}', fmtf(total), UPGRADE_COLOR, fmtf(base), fmtf(bonus),
        BASE_COLOR)
    end
    table.insert(lines, string.format('{%s:, #888888} %s', label, valueStr))
  else
    table.insert(lines, string.format('{%s:, #888888} {%s, %s}', label, fmtf(base), BASE_COLOR))
  end
end
local function showBasePlusFloat(lines, label, base, bonus)
  base = tonumber(base) or 0
  if base == 0 then return end
  pushBasePlusFloat(lines, label, base, tonumber(bonus) or 0)
end

-- Position tooltip above the anchored widget
local function positionTooltip()
  if not tooltipWidget or not anchoredWidget then return end

  local windowSize = g_window.getSize()
  local tooltipSize = tooltipWidget:getSize()
  local widgetRect = anchoredWidget:getRect()

  -- Position tooltip above the item, centered horizontally
  local x = widgetRect.x + (widgetRect.width / 2) - (tooltipSize.width / 2)
  local y = widgetRect.y - tooltipSize.height - 2 -- 2px gap

  -- Keep tooltip on screen (horizontal)
  if x < 5 then
    x = 5
  elseif x + tooltipSize.width > windowSize.width - 5 then
    x = windowSize.width - tooltipSize.width - 5
  end

  -- If tooltip would go above screen, show it below instead
  if y < 5 then
    y = widgetRect.y + widgetRect.height + 2 -- below the item
  end

  tooltipWidget:setPosition({ x = x, y = y })
end

-- Show tooltip
local function showTooltip()
  if not tooltipWidget then return end
  positionTooltip()
  tooltipWidget:show()
  tooltipWidget:raise()
  tooltipWidget:enable()
  g_effects.fadeIn(tooltipWidget, 100)
end

-- Hide tooltip
local function hideTooltip()
  if not tooltipWidget then return end
  g_effects.fadeOut(tooltipWidget, 100)
  anchoredWidget = nil
  lastItem = nil
  lastMeta = nil
end

-- Public API
_G.itemtooltip = _G.itemtooltip or {}

-- Populate custom tooltip widget from item + meta
local function populateTooltip(item, meta)
  if not tooltipWidget or not item then return end

  -- Ensure scaling functions are available
  if not _G.itemmeta or not _G.itemmeta.scaling then
    error('[itemtooltip] CRITICAL: itemmeta.scaling not available')
  end
  local scaling = _G.itemmeta.scaling

  -- Clear existing content
  tooltipWidget:destroyChildren()

  local baseName = nameFor(item, meta)
  local rarity = meta and meta.r or ''
  local displayName = baseName or ''
  if rarity ~= '' and rarity ~= 'common' then
    if displayName ~= '' then
      displayName = string.format('%s %s', rarity, displayName)
    else
      displayName = rarity
    end
  end
  if meta and meta.identified == false then
    displayName = 'unidentified ' .. displayName
  end

  -- Create header with icon + name
  local header = g_ui.createWidget('ItemTooltipHeader', tooltipWidget)
  header:getChildById('icon'):setItemId(item:getId())
  local nameLabel = header:getChildById('name')
  local brokenName = breakNameMiddle(displayName)
  nameLabel:setText(brokenName)
  nameLabel:resizeToText()
  -- Ensure header is tall enough for one or two lines.
  local headerHeight = math.max(nameLabel:getHeight(), 20)
  header:setHeight(headerHeight)

  -- Apply rarity color to name
  if rarity ~= '' then
    nameLabel:setColor(rarityToColor(rarity))
  else
    nameLabel:setColor('#dfdfdf')
  end

  local totalHeight = header:getHeight() + 1 -- header height + margin-bottom approximation

  local descText
  -- Lookup descriptions via itemdb (ItemDB v1) keyed by serverId.
  if _G.itemdb and _G.itemdb.desc then
    local sid = meta and meta.serverId
    if sid then
      local d = _G.itemdb.desc(sid)
      if d and d ~= '' then
        descText = d
      elseif DEBUG then
        g_logger.info(string.format('[itemtooltip] no itemdb description for sid=%s', tostring(sid)))
      end
    elseif DEBUG then
      g_logger.info('[itemtooltip] meta.serverId missing; cannot lookup itemdb description')
    end
  end

  -- Helper to add separator
  local function addSeparator()
    local sep = g_ui.createWidget('ItemTooltipSeparator', tooltipWidget)
    totalHeight = totalHeight + 1 + 3 + 3 -- separator height + top margin + bottom margin
  end

  -- Helper to add section (plain colored-text lines)
  local function addSection(lines)
    if #lines == 0 then return end

    -- Create section widget
    local section = g_ui.createWidget('ItemTooltipSection', tooltipWidget)
    local contentLabel = section:getChildById('content')
    contentLabel:setColoredText(table.concat(lines, '\n'))
    contentLabel:setTextAlign(AlignLeft)

    -- Calculate section height
    contentLabel:resizeToText()
    local sectionHeight = contentLabel:getHeight()
    section:setHeight(sectionHeight)
    totalHeight = totalHeight + sectionHeight + 4 + 4 -- section + top margin + bottom margin
  end

  -- Helper to add upgrades section with tier boxes
  local function addUpgradeSection(entries)
    if #entries == 0 then return end

    local section = g_ui.createWidget('ItemTooltipSection', tooltipWidget)
    local contentLabel = section:getChildById('content')
    contentLabel:setText('')
    contentLabel:setHeight(0)

    local rowHeight = 17
    local spacing = 0

    for index, entry in ipairs(entries) do
      local row = g_ui.createWidget('ItemTooltipUpgradeRow', section)
      local textLabel = row:getChildById('text')
      textLabel:setTextAlign(AlignLeft)
      if entry.color then
        textLabel:setColoredText(string.format('{%s, %s}', entry.text, entry.color))
      else
        textLabel:setText(entry.text or '')
      end

      local tiersWidget = row:getChildById('tiers')
      local tierLabel = row:getChildById('tierText')
      -- Tier text intentionally hidden to avoid overlay artifacts.
      if tierLabel then
        tierLabel:setVisible(false)
        tierLabel:setText('')
      end
      local showTiers = Modifiers and Modifiers.isShiftPressed and Modifiers.isShiftPressed() or false
      local tier, maxTier = nil, nil
      if entry.modName and entry.level then
        tier, maxTier = tierInfo(entry.modName, entry.level)
      end
      if tiersWidget then
        tiersWidget:destroyChildren()
        if maxTier and maxTier > 0 then
          for i = 1, maxTier do
            local boxType = (tier and i <= tier) and 'ItemTooltipTierBoxFilled' or 'ItemTooltipTierBox'
            local box = g_ui.createWidget(boxType, tiersWidget)
            if i > 1 then box:setMarginLeft(2) end
          end
        end
        tiersWidget:setVisible(showTiers and maxTier and maxTier > 0)
      end
    end

    local sectionHeight = (#entries * rowHeight) + math.max(0, (#entries - 1) * spacing)
    section:setHeight(sectionHeight)
    totalHeight = totalHeight + sectionHeight + 4 + 4
  end

  if descText then
    addSeparator()
    local descLabel = g_ui.createWidget('ItemTooltipDesc', tooltipWidget)
    descLabel:setText(descText)
    descLabel:resizeToText()
    totalHeight = totalHeight + descLabel:getHeight() + 4 + 4
    addSeparator()
  else
    addSeparator()
  end

  -- Base Stats
  local baseStats = {}
  -- Derived weapon label (first line in base stats)
  do
    local sid = meta and meta.serverId
    local label
    if sid and _G.itemdb and _G.itemdb.weaponType and _G.itemdb.ammoType and _G.itemdb.slotType then
      local wt = _G.itemdb.weaponType(sid) or 0
      local at = _G.itemdb.ammoType(sid) or 0
      local st = _G.itemdb.slotType(sid) or 0
      local function handed()
        if st == 9 then return 'Two-Handed' end
        return 'One-Handed'
      end
      if wt == 1 then       label = handed() .. ' Sword'
      elseif wt == 2 then   label = handed() .. ' Club'
      elseif wt == 3 then   label = handed() .. ' Axe'
      elseif wt == 6 then   label = handed() .. ' Wand'
      elseif wt == 5 then
        if at == 2 then
          label = handed() .. ' Bow'
        elseif at == 1 then
          label = handed() .. ' Crossbow'
        else
          label = 'Throwable'
        end
      end
    end
    if label then
      table.insert(baseStats, string.format('{%s, #888888}', label))
    end
  end
  if meta then
    -- 1. Attack/Damage related
    if meta.baseAtk and meta.baseAtk > 0 then
      table.insert(baseStats, string.format('{Attack:, #888888} {%d, #dfdfdf}', meta.baseAtk))
    end
    -- Base elemental attack: show as "<Elem> Attack: X"
    if meta.baseElem and meta.baseElem > 0 then
      local elemName = ELEM_NAMES[meta.baseElem] or 'Element'
      local baseElemAtk = meta.baseElemAtk or 0
      local upgradeFlat = 0
      local combine = false
      if meta.elemKey and meta.elemKey > 0 and meta.elemLvl and meta.elemLvl > 0 then
        -- if upgrade matches base element, combine in base section as "XY (X+Y)"
        if meta.elemKey == meta.baseElem then
          local elem = scaling.elem(meta.elemLvl)
          upgradeFlat = elem.flat
          combine = true
        end
      end
      if baseElemAtk > 0 or combine then
        if combine then
          local total = (baseElemAtk or 0) + (upgradeFlat or 0)
          -- Color the combined total like an upgrade value, keep parentheses in base color
          local valueStr = string.format('{%d, #9f9dfd}{ (%d+%d), #dfdfdf}', total, baseElemAtk or 0,
            upgradeFlat or 0)
          table.insert(baseStats, string.format('{%s Attack:, #888888} %s', elemName, valueStr))
        else
          table.insert(baseStats, string.format('{%s Attack:, #888888} {%d, #dfdfdf}', elemName, baseElemAtk))
        end
      end
    end
    if meta.baseAtkSpd and meta.baseAtkSpd > 0 then
      local s = msFmt(meta.baseAtkSpd)
      if meta.asLvl and meta.asLvl > 0 then
        local as = scaling.atkSpeed(meta.asLvl)
        local pct = as and as.pct or 0
        local final = math.max(1, math.floor(meta.baseAtkSpd * 100 / (100 + pct)))
        s = string.format('%s -> %s', msFmt(meta.baseAtkSpd), msFmt(final))
      end
      table.insert(baseStats, string.format('{Attack Speed:, #888888} {%s, #dfdfdf}', s))
    end
    -- Hit Chance headline (percent)
    showBasePlusPercent(baseStats, 'Hit Chance', meta.baseHitChance or 0, (meta.hitChanceLvl or 0) * 1)
    -- Range and Max Hit Chance (display-only caps)
    if meta.baseMaxHitChance and meta.baseMaxHitChance > 0 then
      table.insert(baseStats, string.format('{Max Hit Chance:, #888888} {%d%%, #dfdfdf}', meta.baseMaxHitChance))
    end
    if meta.baseRange and meta.baseRange > 1 then
      table.insert(baseStats, string.format('{Range:, #888888} {%d, #dfdfdf}', meta.baseRange))
    end

    -- 2. Defense/Resistance related
    -- Defense headline (flat)
    do
      local base = meta.baseDef or 0
      local bonus = 0
      if meta.defLvl and meta.defLvl > 0 and scaling.defense then
        local defScale = scaling.defense(meta.defLvl)
        bonus = defScale.defense or defScale.def or 0
      end
      showBasePlusFlat(baseStats, 'Defense', base, bonus)
    end
    if meta.baseArm and meta.baseArm > 0 then
      table.insert(baseStats, string.format('{Armor:, #888888} {%d, #dfdfdf}', meta.baseArm))
    end
    -- Resists headlines (percent)
    showBasePlusPercent(baseStats, 'Physical Resist', meta.baseResistPhysical or 0, (meta.resistPhysicalLvl or 0) * 1)
    showBasePlusPercent(baseStats, 'Fire Resist', meta.baseResistFire or 0, (meta.resistFireLvl or 0) * 1)
    showBasePlusPercent(baseStats, 'Ice Resist', meta.baseResistIce or 0, (meta.resistIceLvl or 0) * 1)
    showBasePlusPercent(baseStats, 'Earth Resist', meta.baseResistEarth or 0, (meta.resistEarthLvl or 0) * 1)
    showBasePlusPercent(baseStats, 'Energy Resist', meta.baseResistEnergy or 0, (meta.resistEnergyLvl or 0) * 1)

    -- 3. Mana/Health Regen
    -- Base regeneration normalized to our 2s tick, combined with upgrade when present
    do
      local hpBase = tonumber(meta.baseHpRegenPer2s) or 0
      local mpBase = tonumber(meta.baseMpRegenPer2s) or 0
      local hpBonus = 0
      local mpBonus = 0
      if meta.hpRegenLvl and meta.hpRegenLvl > 0 and scaling.hpRegen then
        -- Our regen applies every 2s; upgrade value is per 2s tick already
        hpBonus = (scaling.hpRegen(meta.hpRegenLvl) or {}).perSec or 0
      end
      if meta.mpRegenLvl and meta.mpRegenLvl > 0 and scaling.mpRegen then
        mpBonus = (scaling.mpRegen(meta.mpRegenLvl) or {}).perSec or 0
      end
      showBasePlusFloat(baseStats, 'Health Regen', hpBase, hpBonus)
      showBasePlusFloat(baseStats, 'Mana Regen', mpBase, mpBonus)
    end

    -- 4. Skills
    -- Magic Level (base + upgrade)
    do
      local base = tonumber(meta.baseMagicLevel) or 0
      local bonus = 0
      if meta.magicLevelLvl and meta.magicLevelLvl > 0 and scaling.magicLevel then
        bonus = (scaling.magicLevel(meta.magicLevelLvl) or {}).amount or 0
      end
      showBasePlusFlat(baseStats, 'Magic Level', base, bonus)
    end
    -- Base skill attributes (items.xml): render in base section and combine with upgrades if present
    do
      local oneHandBase = tonumber(meta.baseSkillOneHanded) or 0
      local twoHandBase = tonumber(meta.baseSkillTwoHanded) or 0
      local distBase    = tonumber(meta.baseSkillDist) or 0
      local shieldBase  = tonumber(meta.baseSkillShield) or 0
      local oneHandBonus  = 0
      local twoHandBonus  = 0
      local distBonus     = 0
      local shieldBonus   = 0
      if meta.skillOneHandedLvl and meta.skillOneHandedLvl > 0 and scaling.skillOneHanded then
        oneHandBonus = (scaling.skillOneHanded(meta.skillOneHandedLvl) or {}).amount or 0
      end
      if meta.skillTwoHandedLvl and meta.skillTwoHandedLvl > 0 and scaling.skillTwoHanded then
        twoHandBonus = (scaling.skillTwoHanded(meta.skillTwoHandedLvl) or {}).amount or 0
      end
      if meta.skillDistLvl and meta.skillDistLvl > 0 and scaling.skillDistance then
        distBonus = (scaling.skillDistance(meta.skillDistLvl) or {}).amount or 0
      end
      if meta.skillShieldLvl and meta.skillShieldLvl > 0 and scaling.skillShield then
        shieldBonus = (scaling.skillShield(meta.skillShieldLvl) or {}).amount or 0
      end

      if oneHandBase ~= 0 or oneHandBonus ~= 0 then
        showBasePlusFlat(baseStats, 'One-Handed Skill', oneHandBase, oneHandBonus)
      end
      if twoHandBase ~= 0 or twoHandBonus ~= 0 then
        showBasePlusFlat(baseStats, 'Two-Handed Skill', twoHandBase, twoHandBonus)
      end
      if distBase ~= 0 or distBonus ~= 0 then
        showBasePlusFlat(baseStats, 'Distance Skill', distBase, distBonus)
      end
      if shieldBase ~= 0 or shieldBonus ~= 0 then
        showBasePlusFlat(baseStats, 'Shielding Skill', shieldBase, shieldBonus)
      end
    end

    -- 5. Misc/Other
    -- Movement speed (boots): show base and combined with upgrade if present
    if meta.baseSpd and meta.baseSpd > 0 then
      local base = meta.baseSpd
      if meta.spdLvl and meta.spdLvl > 0 and scaling.moveSpeed then
        local inc = scaling.moveSpeed(meta.spdLvl).units
        -- Color the combined total like an upgrade value, keep parentheses in base color, and omit '+' inside parens
        local valueStr = string.format('{+%d, #9f9dfd}{ (%d+%d), #dfdfdf}', base + inc, base, inc)
        table.insert(baseStats, string.format('{Speed:, #888888} %s', valueStr))
      else
        table.insert(baseStats, string.format('{Speed:, #888888} {+%d, #dfdfdf}', base))
      end
    end
    if meta.power and meta.power > 0 then
      table.insert(baseStats, string.format('{Power:, #888888} {%d, #dfdfdf}', meta.power))
    end
    if meta.weight and meta.weight > 0 then
      table.insert(baseStats, string.format('{Weight:, #888888} {%s, #dfdfdf}', ozFmt(meta.weight)))
    end
  end
  -- Add Charges and Duration to base stats if they exist
  if item then
    if item.getCharges and item:getCharges() > 0 then
      table.insert(baseStats, string.format('{Charges:, #888888} {%d, #dfdfdf}', item:getCharges()))
    end
    if item.getDurationTime and item:getDurationTime() > 0 then
      local s = string.format('%dm%02d', math.floor(item:getDurationTime() / 60), item:getDurationTime() % 60)
      table.insert(baseStats, string.format('{Duration:, #888888} {%s, #dfdfdf}', s))
    end
  end
  addSection(baseStats)

  -- Upgrades (use scaling functions from itemmeta)
  local upgrades = {}
  if meta then
    -- 1. Attack/Damage related
    -- Elemental upgrade damage (always show upgrade; prefix '+' if it adds to a base elem of the same type)
    if meta.elemKey and meta.elemKey > 0 and meta.elemLvl and meta.elemLvl > 0 then
      local elemName = ELEM_NAMES[meta.elemKey] or 'element'
      local elem = scaling.elem(meta.elemLvl)
      local modName
      if meta.elemKey == 1 then modName = 'fire'
      elseif meta.elemKey == 2 then modName = 'ice'
      elseif meta.elemKey == 3 then modName = 'earth'
      elseif meta.elemKey == 4 then modName = 'energy'
      end
      table.insert(upgrades, {
        text = string.format('+%d %s Attack', elem.flat, elemName),
        color = UPGRADE_COLOR,
        modName = modName,
        level = meta.elemLvl,
      })
    end
    -- Attack speed
    if meta.asLvl and meta.asLvl > 0 then
      local as = scaling.atkSpeed(meta.asLvl)
      table.insert(upgrades, {
        text = string.format('+%d%% Attack Speed', as.pct),
        color = UPGRADE_COLOR,
        modName = 'atk_speed',
        level = meta.asLvl,
      })
    end
    -- Critical chance
    if meta.critLvl and meta.critLvl > 0 then
      local crit = scaling.crit(meta.critLvl)
      table.insert(upgrades, {
        text = string.format('+%d%% Critical Chance', crit.chance),
        color = UPGRADE_COLOR,
        modName = 'crit_chance',
        level = meta.critLvl,
      })
    end
    -- Life leech
    if meta.leechLvl and meta.leechLvl > 0 then
      local leech = scaling.leech(meta.leechLvl)
      table.insert(upgrades, {
        text = string.format('+%d%% Life Leech', leech.amount),
        color = UPGRADE_COLOR,
        modName = 'life_leech',
        level = meta.leechLvl,
      })
    end
    -- Magic damage (wands)
    if meta.magicLvl and meta.magicLvl > 0 then
      local magic = scaling.magic(meta.magicLvl)
      table.insert(upgrades, {
        text = string.format('+%d%% Magic Damage', magic.pct),
        color = UPGRADE_COLOR,
        modName = 'magic',
        level = meta.magicLvl,
      })
    end
    -- Hit Chance modifier line (always list when present)
    if meta.hitChanceLvl and meta.hitChanceLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Hit Chance', meta.hitChanceLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'hit_chance',
        level = meta.hitChanceLvl,
      })
    end

    -- 2. Defense/Resistance related
    if meta.defLvl and meta.defLvl > 0 and scaling.defense then
      local defScale = scaling.defense(meta.defLvl)
      local bonus = defScale.defense or defScale.def or 0
      if bonus > 0 then
        table.insert(upgrades, {
          text = string.format('+%d Defense', bonus),
          color = UPGRADE_COLOR,
          modName = 'defense',
          level = meta.defLvl,
        })
      end
    end
    if meta.reflectLvl and meta.reflectLvl > 0 and scaling.reflect then
      local refl = scaling.reflect(meta.reflectLvl)
      local percent = refl.percent or refl.pct or 0
      if percent > 0 then
        table.insert(upgrades, {
          text = string.format('+%d%% Reflect', percent),
          color = UPGRADE_COLOR,
          modName = 'reflect',
          level = meta.reflectLvl,
        })
      end
    end
    -- Resists (display-only in Phase A)
    local function addResLine(label, lvl, modName)
      if lvl and lvl > 0 then
        local pct = lvl * 1
        table.insert(upgrades, {
          text = string.format('+%d%% %s', pct, label),
          color = UPGRADE_COLOR,
          modName = modName,
          level = lvl,
        })
      end
    end
    addResLine('Physical Resist', meta.resistPhysicalLvl, 'physical_resist')
    addResLine('Fire Resist', meta.resistFireLvl, 'fire_resist')
    addResLine('Ice Resist', meta.resistIceLvl, 'ice_resist')
    addResLine('Earth Resist', meta.resistEarthLvl, 'earth_resist')
    addResLine('Energy Resist', meta.resistEnergyLvl, 'energy_resist')

    -- 3. Mana/Health Regen
    -- Regeneration (show +X; server applies every 2s)
    if meta.hpRegenLvl and meta.hpRegenLvl > 0 and scaling.hpRegen then
      local h = scaling.hpRegen(meta.hpRegenLvl)
      table.insert(upgrades, {
        text = string.format('+%d Health Regen', h.perSec),
        color = UPGRADE_COLOR,
        modName = 'hp_regen',
        level = meta.hpRegenLvl,
      })
    end
    if meta.mpRegenLvl and meta.mpRegenLvl > 0 and scaling.mpRegen then
      local m = scaling.mpRegen(meta.mpRegenLvl)
      table.insert(upgrades, {
        text = string.format('+%d Mana Regen', m.perSec),
        color = UPGRADE_COLOR,
        modName = 'mp_regen',
        level = meta.mpRegenLvl,
      })
    end

    -- 4. Skills
    if meta.magicLevelLvl and meta.magicLevelLvl > 0 and scaling.magicLevel then
      local v = scaling.magicLevel(meta.magicLevelLvl).amount
      table.insert(upgrades, {
        text = string.format('+%d Magic Level', v),
        color = UPGRADE_COLOR,
        modName = 'magic_level',
        level = meta.magicLevelLvl,
      })
    end
    -- Skill bonuses
    if meta.skillOneHandedLvl and meta.skillOneHandedLvl > 0 and scaling.skillOneHanded then
      local v = scaling.skillOneHanded(meta.skillOneHandedLvl).amount
      table.insert(upgrades, {
        text = string.format('+%d One-Handed Skill', v),
        color = UPGRADE_COLOR,
        modName = 'skill_one_handed',
        level = meta.skillOneHandedLvl,
      })
    end
    if meta.skillTwoHandedLvl and meta.skillTwoHandedLvl > 0 and scaling.skillTwoHanded then
      local v = scaling.skillTwoHanded(meta.skillTwoHandedLvl).amount
      table.insert(upgrades, {
        text = string.format('+%d Two-Handed Skill', v),
        color = UPGRADE_COLOR,
        modName = 'skill_two_handed',
        level = meta.skillTwoHandedLvl,
      })
    end
    if meta.skillDistLvl and meta.skillDistLvl > 0 and scaling.skillDistance then
      local v = scaling.skillDistance(meta.skillDistLvl).amount
      table.insert(upgrades, {
        text = string.format('+%d Distance Skill', v),
        color = UPGRADE_COLOR,
        modName = 'skill_distance',
        level = meta.skillDistLvl,
      })
    end
    if meta.skillShieldLvl and meta.skillShieldLvl > 0 and scaling.skillShield then
      local v = scaling.skillShield(meta.skillShieldLvl).amount
      table.insert(upgrades, {
        text = string.format('+%d Shielding Skill', v),
        color = UPGRADE_COLOR,
        modName = 'skill_shield',
        level = meta.skillShieldLvl,
      })
    end

    -- 5. Spell Cost reduction / Cooldown reduction
    if meta.cooldownReductionLvl and meta.cooldownReductionLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Cooldown Reduction', meta.cooldownReductionLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'cooldown_reduction',
        level = meta.cooldownReductionLvl,
      })
    end
    if meta.manaCostReductionLvl and meta.manaCostReductionLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Spell Cost Reduction', meta.manaCostReductionLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'mana_cost_reduction',
        level = meta.manaCostReductionLvl,
      })
    end

    -- 6. Gold/Exp/Other misc bonuses
    -- Movement speed (boots) — prefix '+' (it always adds to character speed)
    if meta.spdLvl and meta.spdLvl > 0 and scaling.moveSpeed then
      local ms = scaling.moveSpeed(meta.spdLvl)
      table.insert(upgrades, {
        text = string.format('+%d Speed', ms.units),
        color = UPGRADE_COLOR,
        modName = 'move_speed',
        level = meta.spdLvl,
      })
    end
    if meta.experienceRateLvl and meta.experienceRateLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Experience Gain', meta.experienceRateLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'experience_rate',
        level = meta.experienceRateLvl,
      })
    end
    if meta.goldRateLvl and meta.goldRateLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Gold Find', meta.goldRateLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'gold_rate',
        level = meta.goldRateLvl,
      })
    end
    if meta.lootRarityLvl and meta.lootRarityLvl > 0 then
      table.insert(upgrades, {
        text = string.format('+%d%% Rare Find', meta.lootRarityLvl * 1),
        color = UPGRADE_COLOR,
        modName = 'loot_rarity',
        level = meta.lootRarityLvl,
      })
    end
  end

  -- Only add separator and upgrades section if there are upgrades
  if #upgrades > 0 then
    addSeparator()
    addUpgradeSection(upgrades)
  end

  -- Sell value from itemdb (at the very bottom)
  local sellGold = meta and meta.serverId and _G.itemdb and _G.itemdb.sellGold and _G.itemdb.sellGold(meta.serverId) or 0
  if sellGold and sellGold > 0 then
    addSeparator()
    local valueLabel = g_ui.createWidget('ItemTooltipSellValue', tooltipWidget)

    -- Format gold value: show as "Xk" if >= 1000, otherwise show raw number
    local formattedValue
    if sellGold >= 1000 then
      local kValue = sellGold / 1000.0
      formattedValue = string.format('%.2f', kValue)
      -- Remove trailing zeros and decimal point if whole number
      formattedValue = formattedValue:gsub('%.?0+$', '')
      formattedValue = formattedValue .. 'k'
    else
      formattedValue = tostring(sellGold)
    end

    valueLabel:setText(string.format('Sells for %s gold', formattedValue))
    valueLabel:resizeToText()
    totalHeight = totalHeight + valueLabel:getHeight() + 4
  end

  -- Update tooltip height
  tooltipWidget:setHeight(totalHeight + 15) -- +15 for padding (9 top + 6 bottom)
  dbg('Tooltip built: %d sections, height=%d', tooltipWidget:getChildCount() - 1, totalHeight)
end

local function refreshTooltipTierVisibility()
  if not tooltipWidget then return end

  local show = false
  if Modifiers and Modifiers.isShiftPressed then
    show = Modifiers.isShiftPressed()
  end

  local function visit(w)
    if not w or w:isDestroyed() then return end

    local id = nil
    if w.getId then
      id = w:getId()
    end
    if id == 'tiers' then
      -- Toggle tier visibility; text stays left-aligned.
      local hasBoxes = w.getChildCount and w:getChildCount() > 0
      w:setVisible(show and hasBoxes)
    end

    local children = {}
    if w.getChildren then
      children = w:getChildren() or {}
    end
    for i = 1, #children do
      visit(children[i])
    end
  end

  visit(tooltipWidget)
end

-- Display tooltip for item (public API for UIItem)
function _G.itemtooltip.display(item, meta, widget)
  if not item or not widget then
    hideTooltip()
    return
  end
  anchoredWidget = widget
  lastItem = item
  lastMeta = meta
  populateTooltip(item, meta)
  showTooltip()
  refreshTooltipTierVisibility()
end

-- Hide tooltip (public API)
function _G.itemtooltip.hide()
  hideTooltip()
end

-- Module lifecycle
local shiftListener

function init()
  -- Ensure item database module is loaded early
  if g_modules and g_modules.ensureModuleLoaded then
    pcall(function() g_modules.ensureModuleLoaded('game_itemdb') end)
  end
  g_ui.importStyle('itemtooltip')
  tooltipWidget = g_ui.createWidget('ItemTooltip', rootWidget)
  tooltipWidget:setVisible(false)
  if Modifiers and Modifiers.onShiftChanged then
    shiftListener = function()
      if tooltipWidget and tooltipWidget:isVisible() then
        refreshTooltipTierVisibility()
      end
    end
    Modifiers.onShiftChanged(shiftListener)
  end
  dbg('Custom tooltip initialized')
end

function terminate()
  if Modifiers and Modifiers.removeListener and shiftListener then
    Modifiers.removeListener(shiftListener)
    shiftListener = nil
  end
  if tooltipWidget then
    hideTooltip()
    tooltipWidget:destroy()
    tooltipWidget = nil
  end
end
