-- ============================================================
-- 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

-- ---------- 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)
    local guid = UnitGUID(unit)
    if not guid or tracked[guid] then return end
    local name, realm = UnitFullName(unit)
    if not name then return end
    realm = (realm and realm ~= "" and realm)
        or (GetNormalizedRealmName and GetNormalizedRealmName()) or ""
    local key = (realm ~= "") and (name .. "-" .. realm) or name

    -- 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)
    tracked[guid] = { key = key, name = name, class = classToken,
        readyAt = 0, lastResult = nil, castAt = 0, gen = 0 }
    byKey[key] = guid
    order[#order + 1] = guid
end

local RefreshOverlayRows -- déclarée plus bas

function Kicks.RefreshRoster()
    wipe(tracked)
    wipe(byKey)
    wipe(order)
    myGUID = UnitGUID("player")
    AddUnit("player", true)
    if IsInRaid() then
        for i = 1, 40 do
            if #order >= MAX_ROWS then break end
            AddUnit("raid" .. i)
        end
    elseif IsInGroup() then
        for i = 1, 4 do
            AddUnit("party" .. i)
        end
    end
    if RefreshOverlayRows then RefreshOverlayRows() 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()

    overlayBg = overlay:CreateTexture(nil, "BACKGROUND")
    overlayBg:SetTexture(WHITE)
    overlayBg:SetAllPoints()

    -- bordure fine 1 px (style WeakAuras : sobre, lisible sur fond
    -- transparent grâce à l'ombre portée des polices)
    overlayBorders = {}
    for _, side in ipairs({ "TOP", "BOTTOM", "LEFT", "RIGHT" }) do
        local t = overlay:CreateTexture(nil, "BORDER")
        t:SetTexture(WHITE)
        if side == "TOP" then
            t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT"); t:SetHeight(1)
        elseif side == "BOTTOM" then
            t:SetPoint("BOTTOMLEFT"); t:SetPoint("BOTTOMRIGHT"); t:SetHeight(1)
        elseif side == "LEFT" then
            t:SetPoint("TOPLEFT"); t:SetPoint("BOTTOMLEFT"); t:SetWidth(1)
        else
            t:SetPoint("TOPRIGHT"); t:SetPoint("BOTTOMRIGHT"); t:SetWidth(1)
        end
        overlayBorders[#overlayBorders + 1] = t
    end

    local accent = overlay:CreateTexture(nil, "BORDER")
    accent:SetTexture(WHITE)
    accent:SetPoint("TOPLEFT")
    accent:SetPoint("BOTTOMLEFT")
    accent:SetWidth(2)
    accent:SetVertexColor(0.655, 0.545, 0.98, 0.9)

    local title = overlay:CreateFontString(nil, "OVERLAY")
    title:SetFont(FONT, 10, "OUTLINE")
    title:SetTextColor(0.65, 0.66, 0.72, 1)
    title:SetPoint("TOPLEFT", 10, -7)
    title:SetText("KICKS")

    overlay:SetScript("OnMouseDown", function(self) 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)

    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

-- 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)
    for _, guid in ipairs(order) do
        local st = tracked[guid]
        if st 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
    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", 8, -(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), mark .. (st.name or "?"),
                    ("%d"):format(math.ceil(remaining)), remaining / total, { 0.95, 0.65, 0.2 })
            else
                bar:Set(icon, NS.ClassRGB(st.class), mark .. (st.name or "?"),
                    NS.L("PRÊT"), 1, { 0.15, 0.80, 0.40 })
            end
            bar:Show()
        elseif bar then
            bar:Hide()
        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
    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.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 ("pet"→"player", "partypetN"→"partyN", "raidpetN"→"raidN")
local function OwnerUnit(unit)
    if unit == "pet" then return "player" end
    local p = unit:match("^partypet(%d+)$")
    if p then return "party" .. p end
    local r = unit:match("^raidpet(%d+)$")
    if r then return "raid" .. r end
    return unit
end

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 and NS.Sync and NS.Sync.BroadcastKickResult then
                NS.Sync.BroadcastKickResult("hit")
            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

-- ---------- API ----------
function Kicks.SetEnabled(on)
    if NS.db then NS.db.kickOverlay = on and true or false end
    if not overlay then return end
    overlay:SetShown(on and true or false)
    if on 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

function Kicks.ResetPosition()
    if NS.db then
        NS.db.kickPos = nil
        NS.db.kickScale = nil
    end
    if overlay then overlay:SetScale(1) end
    ApplyPosition()
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
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:RegisterEvent("GROUP_ROSTER_UPDATE")
ev:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_ENTERING_WORLD" then
        if not overlay then
            C_Timer.After(3, function()
                BuildOverlay()
                ApplyPosition()
                Kicks.ApplyStyle()
                InitCastEvents()
                Kicks.RefreshRoster()
                Kicks.SetEnabled(Kicks.IsEnabled())
            end)
        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)
