--[[
    KickAlert.lua - TGXG Interrupt
    -----------------------------------------------------------------------
    Two alert layers, both honoring the hard rules:
      * only when the OWN interrupt is ready (no cooldown), and
      * only when the mob is within kick range.

    1) TARGET / FOCUS ALERT
       Watches casts on "target" and "focus". When the cast is
       interruptible and both rules pass, a banner ("Ziel unterbrechbar!")
       pulses at the top of the screen until the cast ends or the player
       kicks.

    2) ASSIGNMENT ALERT ("Automatische Einteilung")
       When a nameplate mob carrying MY assigned raid marker starts an
       interruptible cast (and the rules pass), the banner shows the
       marker icon + "UNTERBRECHEN!".

    Midnight (12.x) safety notes:
      * notInterruptible on enemy casts can be a SECRET boolean → every
        read goes through pcall + issecretvalue; "unknown" counts as
        interruptible (a false positive banner beats a missed kick).
      * GetRaidTargetIndex on nameplates can be SECRET in instances.
        The marker compare runs inside pcall — when the compare itself
        is impossible (secret), the assignment alert falls back to the
        target/focus layer, which needs no marker identity.
    -----------------------------------------------------------------------
]]

TGX = TGX or {}
TGX.KickAlert = TGX.KickAlert or {}
local KA = TGX.KickAlert

local _banner
local _activeUnit          -- unit token the banner currently tracks
local _activeIsAssign      -- true when triggered by the marker assignment
local _recheck             -- ticker re-evaluating ready/range while a cast runs

local function db() return TGX.db end

------------------------------------------------------------
-- Secret-safe cast probes
------------------------------------------------------------

-- Debug print helper — enable with /tgxg debug to see exactly which gate
-- blocks an alert (cast state, interruptible flag, kick ready, range).
local function dbg(...)
    if TGX.debug then print("|cff33ff99TGXG-DBG|r", ...) end
end

-- Returns: castActive (bool), interruptible (true/false/nil=unknown).
-- Uses TGX.ExtractNotInterruptible — position-independent scan of the
-- UnitCastingInfo/UnitChannelInfo returns (Midnight's added castBarID
-- shifted the positional layout; a fixed slot read silently landed on
-- the wrong value and killed the alert+sound).
local function ReadCastInfo(unit)
    local ok, active, interruptible = pcall(function()
        local castName = UnitCastingInfo(unit)
        if castName ~= nil then
            return true, TGX.ExtractNotInterruptible(UnitCastingInfo(unit))
        end
        local chanName = UnitChannelInfo(unit)
        if chanName ~= nil then
            return true, TGX.ExtractNotInterruptible(UnitChannelInfo(unit))
        end
        return false, nil
    end)
    if not ok or not active then return false, nil end
    -- interruptible currently holds the notInterruptible flag (or nil)
    if interruptible == nil then
        -- Flag hidden → interruptibility database (seed + learned).
        if TGX.SpellDB then
            local dbHit = TGX.SpellDB:ResolveForUnit(unit)
            if dbHit ~= nil then return true, dbHit end
        end
        return true, nil
    end
    return true, (interruptible == false)
end

-- Secret-safe: does this unit carry MY assigned marker?
-- Returns true/false, or nil when the compare is impossible (secret index).
local function UnitHasMyMarker(unit)
    local mine = TGX.Assign and TGX.Assign:MyMarker()
    if not mine then return false end
    local ok, res = pcall(function()
        local idx = GetRaidTargetIndex(unit)
        if idx == nil then return false end
        return idx == mine       -- throws when idx is secret → caught by pcall
    end)
    if not ok then return nil end
    return res
end

------------------------------------------------------------
-- Banner UI
------------------------------------------------------------
function KA:Create()
    if _banner then return end

    _banner = CreateFrame("Frame", "TGXGKickAlert", UIParent)
    _banner:SetSize(320, 44)
    _banner:SetPoint("CENTER", UIParent, "CENTER", 0, db().kickAlertPosY or 200)
    _banner:SetFrameStrata("HIGH")
    _banner:Hide()

    _banner.marker = _banner:CreateTexture(nil, "ARTWORK")
    _banner.marker:SetSize(36, 36)
    _banner.marker:SetPoint("LEFT", _banner, "LEFT", 0, 0)
    _banner.marker:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    _banner.marker:Hide()

    _banner.text = _banner:CreateFontString(nil, "OVERLAY")
    _banner.text:SetFont(STANDARD_TEXT_FONT, 24, "THICKOUTLINE")
    _banner.text:SetPoint("LEFT", _banner.marker, "RIGHT", 8, 0)
    _banner.text:SetTextColor(1, 0.25, 0.25)

    -- Pulse animation
    local ag = _banner:CreateAnimationGroup()
    ag:SetLooping("BOUNCE")
    local a = ag:CreateAnimation("Alpha")
    a:SetFromAlpha(1.0)
    a:SetToAlpha(0.45)
    a:SetDuration(0.35)
    _banner.pulse = ag
end

-- Sound with a 1s throttle. Decoupled from the banner's shown-state: the
-- old "only when the banner just appeared" coupling swallowed the sound
-- whenever a new interruptible cast started while the banner was still
-- visible from the previous one — the "ton kommt nicht immer" bug.
local _lastSoundAt = 0
local function MaybeSound(enabled)
    if not enabled then dbg("sound: toggle off") return end
    local now = GetTime()
    if now - _lastSoundAt < 1.0 then dbg("sound: throttled") return end
    _lastSoundAt = now
    -- SOUNDKIT lookup kept inside the guard: if the table is ever absent
    -- the hard-coded RaidWarning id (8959) still plays.
    local id = (type(SOUNDKIT) == "table" and SOUNDKIT.RAID_WARNING) or 8959
    local okS = pcall(PlaySound, id, "Master")
    if not okS then pcall(PlaySound, 8959) end
    dbg("sound: played", id)
end

local function ShowBanner(text, markerIndex)
    if not _banner then return end
    _banner.text:SetText(text)
    if markerIndex then
        SetRaidTargetIconTexture(_banner.marker, markerIndex)
        _banner.marker:Show()
    else
        _banner.marker:Hide()
    end
    _banner:ClearAllPoints()
    _banner:SetPoint("CENTER", UIParent, "CENTER", 0, db().kickAlertPosY or 200)
    if not _banner:IsShown() then
        _banner:Show()
        _banner.pulse:Play()
    end
end

function KA:HideAll()
    if not _banner then return end
    _banner.pulse:Stop()
    _banner:Hide()
    _activeUnit, _activeIsAssign = nil, nil
    if _recheck then _recheck:Cancel() _recheck = nil end
end

------------------------------------------------------------
-- Evaluation — the two hard rules live here.
------------------------------------------------------------
local function RulesPass(unit)
    return TGX.Self:IsKickReady() and TGX.Self:IsKickInRange(unit)
end

-- Try to raise (or keep) an alert for `unit`. Returns true when shown.
local function Evaluate(unit, isAssign)
    if not db().kickAlertEnabled then dbg(unit, "gate: alert disabled") return false end
    if not TGX.Self.spellID then dbg(unit, "gate: no own interrupt found") return false end
    if not UnitExists(unit) then return false end
    if not UnitCanAttack("player", unit) then dbg(unit, "gate: not attackable") return false end

    local active, interruptible = ReadCastInfo(unit)
    dbg(unit, "cast active:", tostring(active), "interruptible:", tostring(interruptible))
    if not active then return false end
    -- STRICT interruptibility: only casts that a kick (Kick, Counter Shot,
    -- Pummel, ...) can actually stop raise the alert. WoW marks kick-immune
    -- casts (stun/CC-only, shielded bosses) with notInterruptible=true —
    -- those never alert. interruptible == nil means the flag came back as
    -- a SECRET value (instances) or was unreadable; by default that does
    -- NOT alert either. The "kickAlertUnknown" option opts back into
    -- alerting on unknown status for users who prefer a false positive
    -- over a missed kick in M+.
    if interruptible ~= true then
        if not (interruptible == nil and db().kickAlertUnknown) then
            dbg(unit, "gate: not (provably) interruptible — kein Alert")
            return false
        end
    end
    if not RulesPass(unit) then
        dbg(unit, "gate: kick ready:", tostring(TGX.Self:IsKickReady()),
            "in range:", tostring(TGX.Self:IsKickInRange(unit)))
        return false
    end

    if isAssign then
        -- e.g. "DREIECK — UNTERBRECHEN!" with the marker icon next to it.
        -- Sound follows its own toggle (assignAlertSound), independent of
        -- the target/focus sound setting.
        local marker = TGX.Assign:MyMarker()
        ShowBanner(TGX.MarkerName(marker):upper() .. " — " .. TGX.L.KICK_NOW, marker)
        MaybeSound(db().assignAlertSound)
    else
        local isFocus = (unit == "focus")
        ShowBanner(isFocus and TGX.L.KICK_FOCUS or TGX.L.KICK_TARGET, nil)
        MaybeSound(db().kickAlertSound)
    end
    _activeUnit     = unit
    _activeIsAssign = isAssign or nil

    -- While the cast runs, keep re-checking: the mob may leave range, our
    -- kick may go on CD from another target, or the cast may finish.
    if _recheck then _recheck:Cancel() end
    _recheck = C_Timer.NewTicker(0.2, function()
        if not _activeUnit then return end
        local stillActive = select(1, ReadCastInfo(_activeUnit))
        if not stillActive or not RulesPass(_activeUnit) then
            KA:HideAll()
        end
    end)
    return true
end

------------------------------------------------------------
-- Events
------------------------------------------------------------
local ev = CreateFrame("Frame")
ev:RegisterEvent("UNIT_SPELLCAST_START")
ev:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
ev:RegisterEvent("UNIT_SPELLCAST_STOP")
ev:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
ev:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
ev:RegisterEvent("PLAYER_FOCUS_CHANGED")

ev:SetScript("OnEvent", function(_, event, unit)
    if event == "PLAYER_TARGET_CHANGED" then
        if _activeUnit == "target" then KA:HideAll() end
        Evaluate("target", false)
        return
    end
    if event == "PLAYER_FOCUS_CHANGED" then
        if _activeUnit == "focus" then KA:HideAll() end
        if db().kickAlertFocus then Evaluate("focus", false) end
        return
    end

    if not unit then return end

    if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
        -- Assignment layer: nameplate mob with MY marker starts casting.
        if unit:find("^nameplate") and TGX.Assign and TGX.Assign.active then
            local hasMine = UnitHasMyMarker(unit)
            if hasMine == true then
                if Evaluate(unit, true) then return end
            end
            -- hasMine == nil (secret marker) → no marker identity available;
            -- the target/focus layer below still covers the mob when targeted.
        end
        if unit == "target" then Evaluate("target", false) end
        if unit == "focus" and db().kickAlertFocus then Evaluate("focus", false) end

    else -- STOP / CHANNEL_STOP / INTERRUPTED
        if _activeUnit and unit == _activeUnit then KA:HideAll() end
    end
end)
