-- ============================================================
-- Asylum Compagnon — Kicks.lua
-- Tracker d'interruptions compatible Midnight (12.0+), sans CLEU.
--
-- Détection LOCALE (fiable pour soi) :
--   · UNIT_SPELLCAST_SUCCEEDED (joueur OU pet → rattaché au
--     propriétaire) avec un sort de NS.INTERRUPTS (Core.lua)
--     → cooldown + fenêtre de 0,6 s ;
--   · UNIT_SPELLCAST_INTERRUPTED d'un ennemi dans la fenêtre
--     → kick réussi ; sinon → « kick dans le vent ».
--
-- SYNCHRO (fiable pour les amis) : depuis Midnight, les casts des
-- autres joueurs sont masqués/« secrets » côté client. Chaque addon
-- annonce donc SES kicks au groupe via le canal AsylumSync ("K" puis
-- "KR" réussi/raté) — même principe que KickSync. Aucune dépendance.
-- ============================================================

local ADDON_NAME, NS = ...

local Kicks = {}
NS.Kicks = Kicks
Kicks.available = false

local OVERLAY_W    = 190
local ROW_H        = 22
local HEADER_H     = 24
local MAX_ROWS     = 8
local MISS_WINDOW  = 0.6
local PEER_TIMEOUT = 2.5 -- sans résultat reçu, on efface le « pending »

local WHITE = "Interface\\Buttons\\WHITE8x8"
local FONT  = "Fonts\\FRIZQT__.TTF"
local ICON_HIT  = "Interface\\RaidFrame\\ReadyCheck-Ready"
local ICON_MISS = "Interface\\RaidFrame\\ReadyCheck-NotReady"

-- indexer une table avec une clé « secrète » (Midnight) lève une
-- erreur → toujours passer par ce pcall
local function LookupInterrupt(spellID)
    local ok, cd = pcall(function() return NS.INTERRUPTS and NS.INTERRUPTS[spellID] end)
    if ok then return cd end
end

-- état : [guid] = { key, name, class, readyAt, lastResult, castAt, gen }
local tracked = {}
local byKey   = {} -- "Nom-Royaume" → guid
local order   = {}
local overlay, rows, ticker
local myGUID

-- Rotation : un joueur y est inclus si override MANUEL (menu Kicks) ; sinon
-- AUTO = il possède une interruption (détecté localement et partagé via « IC »).
-- Un prêtre sans le talent / un druide heal diffuse « pas de kick » → exclu
-- automatiquement, réinclus s'il prend le talent. Sans info encore → inclus.
local function InRotation(st)
    if not st then return false end
    local m = NS.db and NS.db.kickManual and NS.db.kickManual[st.key]
    if m ~= nil then return m and true or false end
    return st.hasKick ~= false
end

-- ---------- Roster ----------
local function ClassColor(token)
    local c = token and RAID_CLASS_COLORS and RAID_CLASS_COLORS[token]
    if c then return c.r, c.g, c.b end
    return 1, 1, 1
end

local function AddUnit(unit, forceInclude, old)
    local guid = UnitGUID(unit)
    if not guid or tracked[guid] then return end
    local key = NS.UnitKey(unit)
    if not key then return end
    local name = key:match("^([^%-]+)") or key

    -- seuls les amis qui ont l'addon sont suivis
    if not forceInclude then
        if not (NS.db and NS.db.peers and NS.db.peers[key]) then return end
    end

    local _, classToken = UnitClass(unit)
    -- On RÉUTILISE l'état existant du joueur (kick en recharge, résultat,
    -- CD partagé, rotation) au lieu de repartir de zéro. Avant : chaque
    -- GROUP_ROSTER_UPDATE (join/leave, même en plein combat) remettait tous
    -- les kicks à « PRÊT » alors qu'ils étaient encore en cooldown, et la
    -- rotation était fausse jusqu'aux prochains messages IC.
    local st = old and old[guid]
    if st then
        st.key, st.name, st.class = key, name, classToken
    else
        st = { key = key, name = name, class = classToken,
            readyAt = 0, lastResult = nil, castAt = 0, gen = 0 }
    end
    st.unit = unit -- token d'unité (résolu à chaque refresh : mort/déco lisibles en direct)
    tracked[guid] = st
    byKey[key] = guid
    order[#order + 1] = guid
end

local RefreshOverlayRows -- déclarée plus bas

function Kicks.RefreshRoster()
    local old = {}
    for g, st in pairs(tracked) do old[g] = st end
    wipe(tracked)
    wipe(byKey)
    wipe(order)
    myGUID = UnitGUID("player")
    AddUnit("player", true, old)
    -- coéquipiers à suivre : EN RAID, seulement notre sous-groupe (NS.MyGroupUnits)
    for _, u in ipairs(NS.MyGroupUnits and NS.MyGroupUnits() or {}) do
        if #order >= MAX_ROWS then break end
        AddUnit(u, nil, old)
    end
    if RefreshOverlayRows then RefreshOverlayRows() end
    if Kicks.AnnounceInterruptCD then Kicks.AnnounceInterruptCD() end  -- CD pour la rotation
    if Kicks.UpdateVisibility then Kicks.UpdateVisibility() end
end

function Kicks.GetRosterNames()
    local names = {}
    for _, guid in ipairs(order) do
        local st = tracked[guid]
        local c = st.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[st.class]
        names[#names + 1] = ("|c%s%s|r"):format((c and c.colorStr) or "ffffffff", st.name)
    end
    return (#names > 0) and table.concat(names, ", ") or NS.L("personne")
end

-- ---------- Encadré (overlay) ----------
local overlayBg, overlayBorders

local function ApplyPosition()
    if not overlay then return end
    overlay:ClearAllPoints()
    local pos = NS.db and NS.db.kickPos
    if pos and pos.p then
        overlay:SetPoint(pos.p, UIParent, pos.rp or pos.p, pos.x or 0, pos.y or 0)
    else
        overlay:SetPoint("CENTER", UIParent, "CENTER", 320, 140)
    end
end

-- opacité du fond + échelle, depuis les réglages sauvegardés
function Kicks.ApplyStyle()
    if not overlay then return end
    local a = (NS.db and NS.db.kickBgAlpha) or 0.6
    overlayBg:SetVertexColor(0, 0, 0, a)
    for _, t in ipairs(overlayBorders) do
        t:SetVertexColor(1, 1, 1, math.min(0.25, 0.05 + a * 0.1))
    end
    overlay:SetScale((NS.db and NS.db.kickScale) or 1)
end

function Kicks.SetBgAlpha(a)
    if NS.db then NS.db.kickBgAlpha = a end
    Kicks.ApplyStyle()
end

local function SavePosition()
    if not (overlay and NS.db) then return end
    local p, _, rp, x, y = overlay:GetPoint(1)
    NS.db.kickPos = { p = p, rp = rp, x = x, y = y }
end

local function BuildOverlay()
    if overlay then return end
    overlay = CreateFrame("Frame", "AsylumCompagnonKickFrame", UIParent)
    overlay:SetSize(OVERLAY_W, HEADER_H + ROW_H + 6)
    overlay:SetFrameStrata("MEDIUM")
    overlay:SetClampedToScreen(true)
    overlay:SetMovable(true)
    overlay:EnableMouse(true)
    overlay:Hide()

    -- chrome commun (fond, bordure 1 px, liseré accent, titre) : Bars.lua
    local chrome = NS.MakeOverlayChrome(overlay, "KICKS")
    overlayBg, overlayBorders = chrome.bg, chrome.borders

    overlay:SetScript("OnMouseDown", function(self)
        if NS.db and NS.db.kickLocked then return end  -- verrou : pas de déplacement
        self:StartMoving()
    end)
    overlay:SetScript("OnMouseUp", function(self)
        self:StopMovingOrSizing()
        SavePosition()
    end)

    -- poignée de redimensionnement (échelle, sauvegardée)
    local grip = CreateFrame("Button", nil, overlay)
    grip:SetSize(12, 12)
    grip:SetPoint("BOTTOMRIGHT", -1, 1)
    grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
    grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
    grip:SetScript("OnMouseDown", function(self)
        local pScale = UIParent:GetEffectiveScale()
        local sx = overlay:GetLeft() * overlay:GetEffectiveScale()
        local sy = overlay:GetTop() * overlay:GetEffectiveScale()
        self:SetScript("OnUpdate", function()
            local cx = GetCursorPosition()
            local newScale = (cx - sx) / (OVERLAY_W * pScale)
            if newScale < 0.6 then newScale = 0.6 end
            if newScale > 2.0 then newScale = 2.0 end
            overlay:SetScale(newScale)
            overlay:ClearAllPoints()
            overlay:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT",
                sx / (pScale * newScale), sy / (pScale * newScale))
        end)
    end)
    grip:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
        if NS.db then NS.db.kickScale = overlay:GetScale() end
        SavePosition()
    end)
    overlay.grip = grip
    grip:SetShown(not (NS.db and NS.db.kickLocked))  -- verrou de taille

    rows = {}
end

local ICON_HIT_T  = "|TInterface\\RaidFrame\\ReadyCheck-Ready:0|t "
local ICON_MISS_T = "|TInterface\\RaidFrame\\ReadyCheck-NotReady:0|t "

local function GetBar(i)
    if rows[i] then return rows[i] end
    local bar = NS.CreateBar(overlay, OVERLAY_W - 16, ROW_H - 3)
    rows[i] = bar
    return bar
end

-- ---------- Rotation de kick : badge « 1 » (vert) / « 2 » (orange) ----------
-- Ordre calculé LOCALEMENT (donc identique sur tous les écrans) à partir des CD
-- réels et des « prêt à » déjà connus : prochain = joueur PRÊT au CD le plus
-- court ; suivant = le 2e. Un joueur qui kick part en recharge → sort du
-- classement → le « 2 » passe « 1 » automatiquement. Badge uniquement sur des
-- kicks PRÊTS — sauf si TOUT le groupe est en recharge, alors on prend ceux qui
-- reviennent le plus vite. (v0.26 : petits badges numérotés à gauche des barres
-- au lieu des bordures pleines — plus discret et lisible en combat.)
local ROTA_GREEN  = { 0.10, 0.48, 0.24 }
local ROTA_ORANGE = { 0.58, 0.35, 0.07 }
local rowBadges = {}
local function GetRowBadge(i)
    if rowBadges[i] then return rowBadges[i] end
    local f = CreateFrame("Frame", nil, overlay)
    f:SetSize(14, 14)
    f:SetFrameLevel((overlay:GetFrameLevel() or 1) + 6)
    local disc = f:CreateTexture(nil, "ARTWORK")
    disc:SetTexture(WHITE)
    disc:SetAllPoints()
    local mask = f:CreateMaskTexture()
    mask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    mask:SetAllPoints(disc)
    disc:AddMaskTexture(mask)
    f.disc = disc
    local fs = f:CreateFontString(nil, "OVERLAY")
    fs:SetFont(FONT, 9, "OUTLINE")
    fs:SetPoint("CENTER", 0, 0)
    f.fs = fs
    f:Hide()
    rowBadges[i] = f
    return f
end
local function SetRowBadge(i, bar, rank)
    local f = GetRowBadge(i)
    if not rank or not bar then f:Hide(); return end
    f:ClearAllPoints()
    f:SetPoint("RIGHT", bar, "LEFT", -4, 0)
    local c = (rank == 1) and ROTA_GREEN or ROTA_ORANGE
    f.disc:SetVertexColor(c[1], c[2], c[3], 1)
    f.fs:SetText(tostring(rank))
    f.fs:SetTextColor(1, 1, 1, 0.95)
    f:Show()
end
-- Le joueur peut-il kicker MAINTENANT ? Un joueur MORT (ou déconnecté) sort
-- de la rotation : s'il était « 2 », le badge glisse sur le suivant, et il
-- revient automatiquement une fois rez (le démoniste, lui, ne revient que
-- quand son pet est ressorti : mort → UNIT_PET → « pas de kick » → hasKick).
-- Tout est lu en direct (état d'unité, synchronisé par le serveur) → tous
-- les écrans voient la même rotation. Tout sous pcall : en cas de valeur
-- « secrète » Midnight ou d'API absente, on le laisse dedans (fail-open).
local function CanKickNow(st)
    local unit = st and st.unit
    if not unit then return true end
    local excluded = false
    pcall(function()
        if UnitIsDeadOrGhost and UnitIsDeadOrGhost(unit) then excluded = true end
        if UnitIsConnected and UnitIsConnected(unit) == false then excluded = true end
    end)
    return not excluded
end

local function ComputeRotation(now)
    local ready, waiting = {}, {}
    for _, guid in ipairs(order) do
        local st = tracked[guid]
        if st and InRotation(st) and CanKickNow(st) then
            local cd = st.cd or 15
            if (st.readyAt or 0) - now <= 0 then
                ready[#ready + 1] = { st = st, cd = cd }
            else
                waiting[#waiting + 1] = { st = st, cd = cd, ra = st.readyAt or 0 }
            end
        end
    end
    table.sort(ready, function(a, b)
        if a.cd ~= b.cd then return a.cd < b.cd end
        return (a.st.name or "") < (b.st.name or "")
    end)
    table.sort(waiting, function(a, b)
        if a.ra ~= b.ra then return a.ra < b.ra end
        if a.cd ~= b.cd then return a.cd < b.cd end
        return (a.st.name or "") < (b.st.name or "")
    end)
    local g = ready[1] and ready[1].st or (waiting[1] and waiting[1].st)
    local o
    if #ready >= 2 then o = ready[2].st
    elseif #ready == 1 then o = waiting[1] and waiting[1].st
    else o = waiting[2] and waiting[2].st end
    return g, o
end

-- Tri priorité (esprit Portal Authority) : PRÊT en haut, puis recharge
-- croissante ; un joueur qui vient de kick retombe en bas et remonte à
-- mesure que son interruption revient. Barres draining + couleur de classe.
local sortedKicks = {}
local function UpdateRows()
    if not overlay or not overlay:IsShown() then return end
    local now = GetTime()
    wipe(sortedKicks)
    -- COHÉRENCE : pas de barre pour un joueur SANS interruption (hasKick=false :
    -- démoniste sans le bon pet sorti, prêtre sans le talent, druide heal…) —
    -- une barre « PRÊT » mentirait. Réapparaît dès que le kick revient (pet
    -- ressorti → IC, ou kick réellement lancé). hasKick=nil (pas encore d'info,
    -- vieille version d'addon) → on affiche (bénéfice du doute). Le forçage
    -- manuel du menu Kicks (kickManual=true) force aussi l'affichage.
    local manual = NS.db and NS.db.kickManual
    for _, guid in ipairs(order) do
        local st = tracked[guid]
        if st and (st.hasKick ~= false or (manual and manual[st.key] == true)) then
            sortedKicks[#sortedKicks + 1] = st
        end
    end
    table.sort(sortedKicks, function(a, b)
        local ra = math.max(0, (a.readyAt or 0) - now)
        local rb = math.max(0, (b.readyAt or 0) - now)
        if ra ~= rb then return ra < rb end
        return (a.name or "") < (b.name or "")
    end)
    local n = #sortedKicks
    local greenSt, orangeSt
    if n >= 2 and not (NS.db and NS.db.kickRotation == false) then
        greenSt, orangeSt = ComputeRotation(now)
    end
    -- gouttière à gauche pour les badges 1/2 quand la rotation est active
    local left = (greenSt or orangeSt) and 24 or 8
    for i = 1, math.max(n, #rows) do
        local bar = rows[i]
        if i <= n then
            bar = bar or GetBar(i)
            bar:ClearAllPoints()
            bar:SetPoint("TOPLEFT", left, -(HEADER_H + (i - 1) * ROW_H))
            bar:SetPoint("TOPRIGHT", -8, -(HEADER_H + (i - 1) * ROW_H))
            local st = sortedKicks[i]
            local remaining = (st.readyAt or 0) - now
            local total = st.cd or 15
            local icon = st.lastSpell and NS.SpellIcon(st.lastSpell) or nil
            local mark = ""
            if st.lastResult == "hit" then mark = ICON_HIT_T
            elseif st.lastResult == "miss" then mark = ICON_MISS_T end
            if remaining > 0 then
                bar:Set(icon, NS.ClassRGB(st.class), (st.name or "?"),
                    ("%d"):format(math.ceil(remaining)), remaining / total, { 0.95, 0.65, 0.2 }, mark)
            else
                bar:Set(icon, NS.ClassRGB(st.class), (st.name or "?"),
                    NS.L("PRÊT"), 1, { 0.15, 0.80, 0.40 }, mark)
            end
            bar:Show()
            if st == greenSt then SetRowBadge(i, bar, 1)
            elseif st == orangeSt then SetRowBadge(i, bar, 2)
            else SetRowBadge(i, bar, nil) end
        elseif bar then
            bar:Hide()
            if rowBadges[i] then rowBadges[i]:Hide() end
        end
    end
    overlay:SetHeight(HEADER_H + math.max(n, 1) * ROW_H + 6)
end
RefreshOverlayRows = UpdateRows

-- ---------- Application d'un kick à un état ----------
local function StartKick(st, cd, isMine, spellID)
    local now = GetTime()
    if (now - (st.castAt or 0)) < 0.1 then return end -- doublon
    -- un kick réellement lancé PROUVE que le joueur a une interruption :
    -- on le (ré)inclut même si l'auto-détection l'avait marqué « pas de kick »
    st.hasKick = true
    st.castAt = now
    st.readyAt = now + cd
    st.cd = cd
    if spellID then st.lastSpell = spellID end
    st.lastResult = "pending"
    st.gen = st.gen + 1
    local g = st.gen
    C_Timer.After(MISS_WINDOW, function()
        if st.gen == g and st.lastResult == "pending" and isMine then
            st.lastResult = "miss" -- kick dans le vent
            if NS.Stats then NS.Stats.RecordSelfKick("miss") end   -- stats M+
            if NS.Sync and NS.Sync.BroadcastKickResult then
                NS.Sync.BroadcastKickResult("miss")
            end
        end
    end)
    if not isMine then
        -- pair : si aucun résultat n'arrive, on efface le sablier
        C_Timer.After(PEER_TIMEOUT, function()
            if st.gen == g and st.lastResult == "pending" then
                st.lastResult = nil
            end
        end)
    end
end

-- ---------- Détection locale (événements d'unité, sans CLEU) ----------
-- pet → propriétaire : utilitaire partagé (Core.lua)
local OwnerUnit = NS.OwnerUnit

local function OnCastSucceeded(unit, spellID)
    local cd = LookupInterrupt(spellID)
    if not cd then return end

    local guid = UnitGUID(OwnerUnit(unit))
    local st = guid and tracked[guid]
    if not st then return end

    local isMine = (guid == myGUID)
    -- pour MON kick, on lit le vrai CD (talents/charges) et on le diffuse ;
    -- les amis reçoivent donc mon cooldown réel, pas la valeur de base
    if isMine and NS.RealCooldown then cd = NS.RealCooldown(spellID, cd) end
    StartKick(st, cd, isMine, spellID)
    if isMine and NS.Sync and NS.Sync.BroadcastKick then
        NS.Sync.BroadcastKick(cd, spellID)
    end
end

local function OnCastInterrupted(unit)
    -- un ennemi vient de se faire couper : crédite le(s) kick(s) locaux
    -- en attente (approximation si plusieurs kicks simultanés)
    local guid = UnitGUID(unit)
    if guid and tracked[guid] then return end -- c'est un des nôtres qui est coupé
    local now = GetTime()
    for _, g in ipairs(order) do
        local st = tracked[g]
        if st and st.lastResult == "pending" and (now - (st.castAt or 0)) <= MISS_WINDOW then
            st.lastResult = "hit"
            st.gen = st.gen + 1
            if g == myGUID then
                if NS.Stats then NS.Stats.RecordSelfKick("hit") end   -- stats M+
                if NS.Sync and NS.Sync.BroadcastKickResult then
                    NS.Sync.BroadcastKickResult("hit")
                end
            end
        end
    end
end

-- ---------- Synchro reçue des amis (via Sync.lua) ----------
function Kicks.OnPeerKick(senderKey, cd, spellID)
    local guid = byKey[senderKey]
    local st = guid and tracked[guid]
    if not st or not cd or cd <= 0 then return end
    StartKick(st, cd, false, spellID)
end

function Kicks.OnPeerKickResult(senderKey, res)
    local guid = byKey[senderKey]
    local st = guid and tracked[guid]
    if not st then return end
    if res == "hit" or res == "miss" then
        st.lastResult = res
        st.gen = st.gen + 1
    end
end

-- CD du kick d'un pair (rotation) : cd>0 → il a un kick ; cd=0 → pas de kick
function Kicks.OnPeerKickCD(senderKey, cd)
    local guid = byKey[senderKey]
    local st = guid and tracked[guid]
    if not st then return end
    if cd and cd > 0 then st.cd = cd; st.hasKick = true
    else st.hasKick = false end
    if NS.RefreshKickRotationList then NS.RefreshKickRotationList() end
end
-- Un sort d'interruption est-il connu ? On regarde le grimoire du JOUEUR
-- **ET** celui du FAMILIER (IsSpellKnown(sid, true)) : les kicks du démoniste
-- (Verrou de sort du chasseur corrompu, Lancer de hache du gangregarde…) sont
-- des sorts de PET — IsPlayerSpell répondait « non » et un démono se
-- retrouvait « pas de kick » alors que son gangregarde kick très bien.
local function KnowsInterrupt(sid)
    if IsPlayerSpell and IsPlayerSpell(sid) then return true end
    if IsSpellKnown then
        if IsSpellKnown(sid) then return true end
        if IsSpellKnown(sid, true) then return true end -- grimoire du familier
    end
    return false
end

-- mon interruption (le CD le plus court parmi celles que je connais)
local function FindMyInterrupt()
    if not NS.INTERRUPTS then return nil end
    local best, bestcd
    pcall(function()
        for sid in pairs(NS.INTERRUPTS) do
            if KnowsInterrupt(sid) then
                local base = LookupInterrupt(sid) or 15
                local cd = (NS.RealCooldown and NS.RealCooldown(sid, base)) or base
                if not bestcd or cd < bestcd then best, bestcd = sid, cd end
            end
        end
    end)
    return best, bestcd
end
-- diffuse TOUJOURS : cd réel si j'ai une interruption, sinon 0 (= pas de kick)
function Kicks.AnnounceInterruptCD()
    local sid, cd = FindMyInterrupt()
    cd = (sid and cd and cd > 0) and math.floor(cd) or 0
    if myGUID and tracked[myGUID] then
        if cd > 0 then tracked[myGUID].cd = cd end
        tracked[myGUID].hasKick = (cd > 0)
    end
    if NS.Sync and NS.Sync.BroadcastKickCD then NS.Sync.BroadcastKickCD(cd) end
    if NS.RefreshKickRotationList then NS.RefreshKickRotationList() end
end

-- liste pour le menu Kicks : joueurs (avec l'addon) + état de rotation
function Kicks.GetRotationList()
    local out = {}
    for _, guid in ipairs(order) do
        local st = tracked[guid]
        if st then
            out[#out + 1] = { key = st.key, name = st.name, class = st.class,
                hasKick = st.hasKick, inRotation = InRotation(st), me = (guid == myGUID) }
        end
    end
    return out
end
function Kicks.SetInRotation(key, on)
    if not key or not NS.db then return end
    NS.db.kickManual = NS.db.kickManual or {}
    NS.db.kickManual[key] = on and true or false
end

-- ---------- API ----------
function Kicks.SetEnabled(on)
    if NS.db then NS.db.kickOverlay = on and true or false end
    Kicks.UpdateVisibility()
end

-- L'overlay ne s'affiche QUE si : module activé (master) + en groupe avec un
-- VRAI joueur + contexte courant coché (Open World / Donjon / Raid). Le ticker
-- est coupé quand c'est masqué (zéro CPU hors des cas utiles).
function Kicks.UpdateVisibility()
    if not overlay then return end
    local show = Kicks.IsEnabled()
        and (not NS.HasRealGroup or NS.HasRealGroup())
        and (not NS.ModuleEnabledHere or NS.ModuleEnabledHere("kick"))
    overlay:SetShown(show and true or false)
    if show then
        if not ticker then ticker = C_Timer.NewTicker(0.2, UpdateRows) end
    elseif ticker then
        ticker:Cancel()
        ticker = nil
    end
end

function Kicks.IsEnabled()
    return not (NS.db and NS.db.kickOverlay == false)
end

-- verrou du cadre : masque la poignée (plus de redimensionnement) ET bloque le
-- déplacement (voir OnMouseDown) — pour ne rien bouger par erreur.
function Kicks.SetLocked(on)
    if NS.db then NS.db.kickLocked = on and true or false end
    if overlay and overlay.grip then
        overlay.grip:SetShown(not (NS.db and NS.db.kickLocked))
    end
end
function Kicks.IsLocked()
    return NS.db and NS.db.kickLocked and true or false
end

function Kicks.ResetPosition()
    if NS.db then
        NS.db.kickPos = nil
        NS.db.kickScale = nil
    end
    if overlay then overlay:SetScale(1) end
    if overlay and overlay.grip then overlay.grip:SetShown(not (NS.db and NS.db.kickLocked)) end
    ApplyPosition()
end

-- ---------- Interruption RÉUSSIE (kick réussi) : journal de combat ----------
-- BUG 0.27 : en Midnight, UNIT_SPELLCAST_INTERRUPTED ne se déclenche pas de
-- façon fiable sur les ennemis → aucun kick n'était crédité « réussi » et TOUS
-- finissaient « ratés » via le minuteur de StartKick. Le JOURNAL DE COMBAT, lui,
-- expose MES PROPRES interruptions réussies de façon fiable (SPELL_INTERRUPT
-- avec moi comme source — pas une valeur « secrète »). Chaque client détecte
-- donc SON hit et le diffuse (« KR hit ») ; les ratés restent gérés par le
-- minuteur. On garde aussi l'ancien repli UNIT_SPELLCAST_INTERRUPTED (inoffensif).
local function CreditMyKickHit()
    local st = myGUID and tracked[myGUID]
    if not st or st.lastResult ~= "pending" then return end
    st.lastResult = "hit"
    st.gen = st.gen + 1
    if NS.Stats then NS.Stats.RecordSelfKick("hit") end
    if NS.Sync and NS.Sync.BroadcastKickResult then NS.Sync.BroadcastKickResult("hit") end
end

local clog = CreateFrame("Frame")
local function InitCombatLog()
    if not (CombatLogGetCurrentEventInfo and clog.RegisterEvent) then return end
    if not pcall(clog.RegisterEvent, clog, "COMBAT_LOG_EVENT_UNFILTERED") then return end
    clog:SetScript("OnEvent", function()
        local _, sub, _, sourceGUID = CombatLogGetCurrentEventInfo()
        if sub == "SPELL_INTERRUPT" and sourceGUID and sourceGUID == myGUID then
            pcall(CreditMyKickHit)
        end
    end)
end

-- ---------- Initialisation / événements ----------
-- Enregistrement différé (pile propre) et sous pcall : requis depuis
-- les restrictions Midnight. Le handler est lui aussi protégé.
local caster = CreateFrame("Frame")
local function InitCastEvents()
    local ok1 = pcall(caster.RegisterEvent, caster, "UNIT_SPELLCAST_SUCCEEDED")
    local ok2 = pcall(caster.RegisterEvent, caster, "UNIT_SPELLCAST_INTERRUPTED")
    Kicks.available = (ok1 and ok2) and true or false
    if Kicks.available then
        caster:SetScript("OnEvent", function(_, event, unit, _, spellID)
            if type(unit) ~= "string" then return end
            if event == "UNIT_SPELLCAST_SUCCEEDED" then
                pcall(OnCastSucceeded, unit, spellID)
            else
                pcall(OnCastInterrupted, unit)
            end
        end)
    end
end

local groupTimer, petTimer
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("GROUP_ROSTER_UPDATE")
ev:RegisterEvent("ZONE_CHANGED_NEW_AREA")   -- changement de contexte (monde/donjon/raid)
ev:RegisterEvent("PLAYER_REGEN_DISABLED")   -- début de combat → CD à jour pour la rotation
ev:RegisterEvent("UNIT_PET")                -- pet invoqué/changé → le kick peut (dis)apparaître
ev:SetScript("OnEvent", function(_, event, arg1)
    if event == "ZONE_CHANGED_NEW_AREA" then
        if Kicks.UpdateVisibility then Kicks.UpdateVisibility() end
    elseif event == "PLAYER_REGEN_DISABLED" then
        if Kicks.AnnounceInterruptCD then Kicks.AnnounceInterruptCD() end
    elseif event == "UNIT_PET" then
        -- démoniste/chasseur : le kick vit dans le grimoire du familier →
        -- on ré-annonce quand MON pet change (invocation, sacrifice, swap)
        if arg1 == "player" and not petTimer then
            petTimer = C_Timer.NewTimer(2, function()
                petTimer = nil
                if Kicks.AnnounceInterruptCD then Kicks.AnnounceInterruptCD() end
            end)
        end
    elseif event == "PLAYER_ENTERING_WORLD" then
        if not overlay then
            C_Timer.After(3, function()
                BuildOverlay()
                ApplyPosition()
                Kicks.ApplyStyle()
                InitCastEvents()
                InitCombatLog()
                Kicks.RefreshRoster()
                Kicks.SetEnabled(Kicks.IsEnabled())
            end)
        else
            -- re-zone : le contexte (monde/donjon/raid) et le sous-groupe
            -- peuvent changer → on reconstruit + réévalue la visibilité
            Kicks.RefreshRoster()
        end
    elseif event == "GROUP_ROSTER_UPDATE" then
        if overlay and not groupTimer then
            groupTimer = C_Timer.NewTimer(2, function()
                groupTimer = nil
                Kicks.RefreshRoster()
            end)
        end
    end
end)
