-- KiraUI-Plugin_Recuperate.lua
-- A clickable Recuperate reminder (spellID 1231411). The icon fades to opacity 0 at
-- full health and to opacity 1 when you are missing health; click it to cast.
--
-- Secret-health-safe: your health is a SECRET value addons may not read or compare.
-- Instead we use UnitHealthPercent(unit, usePredicted, curve) - a C function that maps
-- your (secret) health % through a CURVE and returns the resulting alpha. We feed that
-- straight to SetAlpha, so the engine does the "am I full?" decision and we never touch
-- the raw value.
--   curve: 0% -> 1 (show) ... 99.9% -> 1 (show) ... 100% -> 0 (hide)
--
-- Taint-safe: SecureActionButton (type="spell"); a SECURE visibility state driver
-- handles combat / dead; the full-HP fade is pure opacity.

local addonName, ns = ...
local _G = _G

local CreateFrame = CreateFrame
local C_Timer = C_Timer
local InCombatLockdown = InCombatLockdown
local RegisterStateDriver = RegisterStateDriver
local UnitHealthPercent = UnitHealthPercent
local UnitIsDeadOrGhost = UnitIsDeadOrGhost
local pairs = pairs

local SPELL_ID = 1231411  -- Recuperate
local FALLBACK_ICON = 132290

ns.hasRecup = true

----------------------------------------------------------------------
-- Settings
----------------------------------------------------------------------
local DEFAULTS = {
    enable = false,
    size = 48,
    x = 0,
    y = -150,
    showTooltip = false,  -- spell tooltip on hover (off = clean icon)
}

local function GetCfg() return ns.ModuleCfg('recuperate', DEFAULTS) end
ns.RecupGetCfg = GetCfg

----------------------------------------------------------------------
-- Health -> alpha curve (built once)
----------------------------------------------------------------------
local healthCurve
local function GetCurve()
    if healthCurve ~= nil then return healthCurve or nil end
    local CU, En = _G.C_CurveUtil, _G.Enum
    if CU and CU.CreateCurve and En and En.LuaCurveType then
        local c = CU.CreateCurve()
        c:SetType(En.LuaCurveType.Step)
        c:AddPoint(0, 1)        -- empty -> fully visible
        c:AddPoint(0.999, 1)    -- anything below full -> visible
        c:AddPoint(1, 0)        -- exactly full -> invisible
        healthCurve = c
    else
        healthCurve = false     -- API missing; mark so we don't retry every call
    end
    return healthCurve or nil
end

----------------------------------------------------------------------
-- Button
----------------------------------------------------------------------
local button, lastDriver, pending, previewActive

local function SpellTexture()
    local info = _G.C_Spell and _G.C_Spell.GetSpellInfo and _G.C_Spell.GetSpellInfo(SPELL_ID)
    return (info and info.iconID) or FALLBACK_ICON
end

local function SpellKnown()
    if _G.IsSpellKnownOrOverridesKnown then return _G.IsSpellKnownOrOverridesKnown(SPELL_ID) end
    if _G.IsPlayerSpell then return _G.IsPlayerSpell(SPELL_ID) end
    return true
end

-- Resolve the spell name and write it onto the secure button. Never in combat,
-- and never with a nil name: an unresolved spell leaves the button inert rather
-- than armed with something that cannot fire.
local function ArmSpell()
    if not button or _G.InCombatLockdown() then return end
    local nm
    local CS = _G.C_Spell
    if CS and CS.GetSpellName then
        local ok, v = pcall(CS.GetSpellName, SPELL_ID)
        if ok and type(v) == 'string' and v ~= '' then nm = v end
    end
    if nm then
        button:SetAttribute('spell', nm)
    else
        button:SetAttribute('spell', nil)
        if CS and CS.RequestLoadSpellData then pcall(CS.RequestLoadSpellData, SPELL_ID) end
    end
end

local function EnsureButton()
    if button then return end
    button = CreateFrame('Button', 'KiraUIRecuperate', _G.UIParent,
        'SecureActionButtonTemplate, SecureHandlerStateTemplate')
    button:SetSize(48, 48)
    button:EnableMouse(true)
    button:RegisterForClicks('AnyUp', 'AnyDown')
    button:SetAttribute('type', 'spell')
    button:SetAttribute('unit', 'player')
    -- The spell attribute takes a NAME, not an ID.
    --
    -- A numeric ID makes the secure handler route through CastSpellByID, which
    -- on 12.x is protected - clicking the button raised ADDON_ACTION_FORBIDDEN
    -- instead of casting. A name goes through CastSpellByName, which is not.
    ArmSpell()

    button.border = button:CreateTexture(nil, 'BACKGROUND')
    button.border:SetColorTexture(0, 0, 0, 1)
    button.border:SetAllPoints()

    button.icon = button:CreateTexture(nil, 'ARTWORK')
    button.icon:SetPoint('TOPLEFT', 1, -1)
    button.icon:SetPoint('BOTTOMRIGHT', -1, 1)
    button.icon:SetTexture(SpellTexture())
    button.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    button.hl = button:CreateTexture(nil, 'HIGHLIGHT')
    button.hl:SetAllPoints()
    button.hl:SetColorTexture(1, 1, 1, 0.2)
    button.hl:SetBlendMode('ADD')

    button:SetScript('OnEnter', function(self)
        if not GetCfg().showTooltip then return end
        if _G.GameTooltip then
            _G.GameTooltip:SetOwner(self, 'ANCHOR_RIGHT')
            if _G.GameTooltip.SetSpellByID then _G.GameTooltip:SetSpellByID(SPELL_ID) end
            _G.GameTooltip:Show()
        end
    end)
    button:SetScript('OnLeave', function() if _G.GameTooltip then _G.GameTooltip:Hide() end end)
end

-- Opacity from health: 0 at full, 1 when missing. Computed entirely by the engine
-- via UnitHealthPercent + curve, so the secret health value is never read in Lua.
local function UpdateAlpha()
    if not button then return end
    if previewActive then button:SetAlpha(1) return end
    if UnitIsDeadOrGhost and UnitIsDeadOrGhost('player') then button:SetAlpha(0) return end
    local c = GetCurve()
    if c and UnitHealthPercent then
        button:SetAlpha(UnitHealthPercent('player', true, c))
    else
        button:SetAlpha(1)  -- fallback if the curve API is unavailable
    end
end

local function Reposition()
    if not button or InCombatLockdown() then return end
    local cfg = GetCfg()
    button:SetSize(cfg.size or 48, cfg.size or 48)
    button:ClearAllPoints()
    button:SetPoint('CENTER', _G.UIParent, 'CENTER', cfg.x or 0, cfg.y or -150)
    button.icon:SetTexture(SpellTexture())
end

-- Secure visibility driver handles combat / dead / the master enable. Opacity (above)
-- handles the full-HP fade.
local function ApplyDriver()
    EnsureButton()
    if InCombatLockdown() then pending = true return end
    pending = false
    local cfg = GetCfg()
    local drv
    if previewActive then
        drv = 'show'
    elseif not cfg.enable or not SpellKnown() then
        drv = 'hide'
    else
        drv = '[combat] hide; [dead] hide; show'
    end
    if drv ~= lastDriver then
        lastDriver = drv
        RegisterStateDriver(button, 'visibility', drv)
    end
    UpdateAlpha()
end

ns.RecupApply = function()
    lastDriver = nil
    C_Timer.After(0, function() EnsureButton() Reposition() ApplyDriver() end)
end

ns.RecupIsPreview = function() return previewActive and true or false end
ns.RecupSetPreview = function(state)
    previewActive = state and true or false
    lastDriver = nil
    EnsureButton()
    Reposition()
    ApplyDriver()
end

----------------------------------------------------------------------
-- Events
----------------------------------------------------------------------
local evt = CreateFrame('Frame')
evt:RegisterEvent('PLAYER_ENTERING_WORLD')
evt:RegisterEvent('PLAYER_REGEN_ENABLED')
evt:RegisterEvent('PLAYER_REGEN_DISABLED')
evt:RegisterUnitEvent('UNIT_HEALTH', 'player')
evt:RegisterUnitEvent('UNIT_MAXHEALTH', 'player')
evt:RegisterEvent('PLAYER_DEAD')
evt:RegisterEvent('PLAYER_UNGHOST')
evt:RegisterEvent('SPELLS_CHANGED')
evt:SetScript('OnEvent', function(_, event)
    if event == 'UNIT_HEALTH' or event == 'UNIT_MAXHEALTH' then
        UpdateAlpha()                     -- opacity only; safe any time
        return
    end
    if event == 'PLAYER_REGEN_DISABLED' then
        return                            -- secure driver hides in combat
    elseif event == 'PLAYER_ENTERING_WORLD' then
        C_Timer.After(0.5, function() Reposition() ApplyDriver() end)
        return
    elseif event == 'PLAYER_REGEN_ENABLED' then
        if pending then Reposition() end
    end
    ApplyDriver()
end)

-- The name may not be cached when the button is built, and attributes cannot be
-- written in combat, so both moments get a second attempt.
local armEvt = CreateFrame('Frame')
for _, e in ipairs({ 'SPELL_DATA_LOAD_RESULT', 'SPELLS_CHANGED', 'PLAYER_REGEN_ENABLED', 'PLAYER_ENTERING_WORLD' }) do
    pcall(armEvt.RegisterEvent, armEvt, e)
end
armEvt:SetScript('OnEvent', function() ArmSpell() end)
