-- ============================================================
-- FILE: InterruptTracker.lua
-- MODULE: MidnightUI_InterruptTracker  (_G.MidnightUI_InterruptTracker, local M)
--
-- PURPOSE:
--   Party interrupt-cooldown tracker. Displays a movable row of
--   icons (one per kicker in the group) showing each player's kick
--   cooldown. Highlights the "next up" kicker with a green border
--   and shows numeric badges (1, 2, 3...) for the rotation order.
--   Designed to work inside TWW 12.x restricted combat by avoiding
--   CLEU and laundering secret-value spell IDs through tostring().
--
-- DETECTION (layered, best -> worst):
--   1. Own kick: UNIT_SPELLCAST_SUCCEEDED on player/pet, spellID
--      laundered via tonumber(tostring(rawID)).
--   2. MidnightUI teammates: addon-comm "K|<cd>" broadcast on the
--      "MUI_IT" prefix. Handshake "H|<v>" on join announces presence.
--   3. Strangers (no MidnightUI): name-match against NAME_KICK using
--      C_Spell.GetSpellName (names aren't secret); fallback to
--      mob UNIT_SPELLCAST_INTERRUPTED + nearest eligible teammate
--      cast within a 0.5s window. Eligibility-filtered.
--
-- ROTATION RANKING (Option C, identical on every client):
--   1) longest base CD first  (save short-CD kickers as insurance)
--   2) least-recently-kicked  (round-robin fairness)
--   3) Tank > DPS > Healer    (M+ kick-priority wisdom)
--   4) alphabetical by name   (final tie-break)
--
-- DEPENDS ON:
--   PlayerFrame.lua  - frame MidnightUI_PlayerFrame (icon parent)
--   MoverRegistry    - draggable container, settings panel
--   Settings.lua     - MidnightUISettings.InterruptTracker
--
-- EXPORTS:
--   _G.MidnightUI_InterruptTracker (module table M)
--   M.Refresh()  - rebuild icons, re-anchor, re-evaluate visibility
--
-- GOTCHAS:
--   - Spell IDs from UNIT_SPELLCAST_SUCCEEDED are secret values in
--     restricted combat. tonumber() alone does NOT strip taint.
--     Always route through tostring() first.
--   - This module never registers COMBAT_LOG_EVENT_UNFILTERED.
-- ============================================================

local M = _G.MidnightUI_InterruptTracker or {}
_G.MidnightUI_InterruptTracker = M

-- One-time cleanup of legacy globally-named icon frames left behind by an
-- older Phase 2 implementation that parented icons to party frames.
-- Hide them and reparent off-screen so they never re-appear.
do
    local legacy = { "MidnightUI_InterruptIcon_player",
                     "MidnightUI_InterruptIcon_party1",
                     "MidnightUI_InterruptIcon_party2",
                     "MidnightUI_InterruptIcon_party3",
                     "MidnightUI_InterruptIcon_party4" }
    for _, name in ipairs(legacy) do
        local f = _G[name]
        if f and f.Hide then
            f:Hide()
            if f.SetParent then f:SetParent(nil) end
            if f.ClearAllPoints then f:ClearAllPoints() end
        end
    end
end

-- ============================================================
-- Sync constants
-- ============================================================
local ADDON_PREFIX  = "MUI_IT"   -- max 16 chars
local SYNC_VERSION  = 1
local syncedPeers   = {}         -- syncedPeers[unitName] = true

-- Stranger fallback state
local NAME_KICK         = {}     -- lowercase spell name → { cd = N }  (built at PLAYER_LOGIN)
local NAME_KICK_BUILT   = false
local CORRELATION_WINDOW = 0.5   -- seconds
local recentPartyCasts  = {}     -- recentPartyCasts[unit] = GetTime()

-- ============================================================
-- Spell data
-- ============================================================
local CLASS_KICK = {
    WARRIOR     = { id = 6552,    cd = 15, icon = 132938 },   -- Pummel
    ROGUE       = { id = 1766,    cd = 15, icon = 132219 },   -- Kick
    MAGE        = { id = 2139,    cd = 24, icon = 135856 },   -- Counterspell
    SHAMAN      = { id = 57994,   cd = 12, icon = 136018 },   -- Wind Shear
    DRUID       = { id = 106839,  cd = 15, icon = 236946 },   -- Skull Bash
    DEATHKNIGHT = { id = 47528,   cd = 15, icon = 237527 },   -- Mind Freeze
    PALADIN     = { id = 96231,   cd = 15, icon = 523893 },   -- Rebuke
    DEMONHUNTER = { id = 183752,  cd = 15, icon = 1305153 },  -- Disrupt
    MONK        = { id = 116705,  cd = 15, icon = 608940 },   -- Spear Hand Strike
    HUNTER      = { id = 147362,  cd = 24, icon = 249170 },   -- Counter Shot
    EVOKER      = { id = 351338,  cd = 20, icon = 4622469 },  -- Quell
}

-- Pet-cast interrupts: Hunter Muzzle, Warlock Felhunter Spell Lock, Observer Optical Blast
local PET_KICK = {
    [187707] = { name = "Muzzle",        cd = 15, icon = 1376045 },
    [19647]  = { name = "Spell Lock",    cd = 24, icon = 136174  },
    [132409] = { name = "Spell Lock",    cd = 24, icon = 136174  },
    [115781] = { name = "Optical Blast", cd = 24, icon = 136223  },
}

-- Healer specs without a reliably-available kick. SHAMAN and EVOKER
-- healers retain their kicks (Wind Shear / Quell) so are NOT in this list.
local HEALER_HIDES_KICK = {
    PRIEST  = true,  -- Holy / Disc
    PALADIN = true,  -- Holy
    MONK    = true,  -- Mistweaver
    DRUID   = true,  -- Resto (Skull Bash requires Feral form)
}

-- ============================================================
-- Helpers
-- ============================================================

-- Strip secret-value taint by routing through tostring (see MEMORY.md
-- "WoW API Taint Gotchas"). tonumber() alone does NOT break the chain.
local function LaunderSpellID(rawID)
    if rawID == nil then return nil end
    local s = tostring(rawID)
    if not s then return nil end
    return tonumber(s)
end

local function GetSettings()
    if not MidnightUISettings then return nil end
    if not MidnightUISettings.InterruptTracker then
        MidnightUISettings.InterruptTracker = { enabled = true, iconSize = 28, showNonMUIUsers = true, offsetX = 0, offsetY = 0 }
    end
    return MidnightUISettings.InterruptTracker
end

-- Returns kick data for the given unit, or nil if the unit cannot kick
-- (no class, no kick spell for class, or healer with no kick).
local function GetKickDataForUnit(unit)
    if not unit or not UnitExists(unit) then return nil end
    local _, class = UnitClass(unit)
    if not class then return nil end
    local data = CLASS_KICK[class]
    if not data then return nil end
    local role = UnitGroupRolesAssigned(unit)
    if role == "HEALER" and HEALER_HIDES_KICK[class] then return nil end
    return data
end

-- Apply self-only role-based CD overrides on top of the class default.
-- Teammates broadcast their own effective CDs over the comm channel; this
-- is only used for our own kicks and for stranger-fallback estimates.
local function GetEffectiveCD(unit, baseCD)
    if not baseCD then return baseCD end
    local _, class = UnitClass(unit or "player")
    local role = UnitGroupRolesAssigned(unit or "player")
    if class == "EVOKER" and role == "HEALER" then return 40 end  -- Quell, Pres
    return baseCD
end

-- ============================================================
-- Slot registry
-- ============================================================

-- Slot order: 1 = player, 2..5 = party1..4
local SLOT_UNITS = { "player", "party1", "party2", "party3", "party4" }

local icons = {}    -- icons[unit] = frame

-- ============================================================
-- Icon construction
-- ============================================================
local DEFAULT_ICON_SIZE = 28

local function CreateIcon(parent, unit, sizePx)
    local size = sizePx or DEFAULT_ICON_SIZE
    -- No global name (was a leak across /reload via legacy named frames).
    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    f:SetSize(size, size)
    f:SetFrameStrata("MEDIUM")
    f:SetFrameLevel((parent.GetFrameLevel and parent:GetFrameLevel() or 1) + 5)

    f:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
    f:SetBackdropBorderColor(0, 0, 0, 1)

    f.tex = f:CreateTexture(nil, "ARTWORK")
    f.tex:SetPoint("TOPLEFT", 1, -1)
    f.tex:SetPoint("BOTTOMRIGHT", -1, 1)
    f.tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    f.cd = CreateFrame("Cooldown", nil, f, "CooldownFrameTemplate")
    f.cd:SetAllPoints(f.tex)
    f.cd:SetDrawEdge(false)
    f.cd:SetHideCountdownNumbers(false)

    -- Rank badge (small number "1", "2"... in top-right) shown when this icon
    -- is one of multiple ready kickers and the rotation has produced a queue.
    f.rankBadge = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.rankBadge:SetPoint("TOPRIGHT", 2, 2)
    f.rankBadge:SetTextColor(1, 1, 1, 1)
    f.rankBadge:Hide()

    -- Persistent amber border while ready
    f.readyBorder = f:CreateTexture(nil, "OVERLAY", nil, 5)
    f.readyBorder:SetPoint("TOPLEFT", -2, 2)
    f.readyBorder:SetPoint("BOTTOMRIGHT", 2, -2)
    f.readyBorder:SetTexture("Interface\\Buttons\\WHITE8X8")
    f.readyBorder:SetVertexColor(1.0, 0.65, 0.10, 0.60)
    f.readyBorder:SetBlendMode("ADD")
    f.readyBorder:Hide()

    -- One-shot pulse on transition to ready
    f.pulse = f:CreateTexture(nil, "OVERLAY", nil, 7)
    f.pulse:SetPoint("TOPLEFT", -4, 4)
    f.pulse:SetPoint("BOTTOMRIGHT", 4, -4)
    f.pulse:SetTexture("Interface\\Buttons\\WHITE8X8")
    f.pulse:SetVertexColor(1, 1, 1, 0.8)
    f.pulse:SetBlendMode("ADD")
    f.pulse:Hide()

    f.pulseAG = f.pulse:CreateAnimationGroup()
    local fade = f.pulseAG:CreateAnimation("Alpha")
    fade:SetFromAlpha(0.8); fade:SetToAlpha(0); fade:SetDuration(0.55)
    f.pulseAG:SetScript("OnPlay",     function() f.pulse:Show() end)
    f.pulseAG:SetScript("OnFinished", function() f.pulse:Hide() end)

    f.unit = unit

    -- Tooltip
    f:EnableMouse(true)
    f:SetScript("OnEnter", function(self)
        local u = self.unit
        if not u then return end
        local name = (u == "player") and (UnitName("player") or "You") or (UnitName(u) or u)
        local _, class = UnitClass(u)
        local sname = self.spellID and C_Spell and C_Spell.GetSpellName
                      and C_Spell.GetSpellName(self.spellID) or "Interrupt"
        local statusLine
        if self._endTime and self._endTime > GetTime() then
            statusLine = string.format("%.1fs remaining", self._endTime - GetTime())
        else
            statusLine = "|cFF66FF66Ready|r"
        end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(string.format("%s |cFF999999(%s)|r", name, class or "?"))
        GameTooltip:AddLine(sname, 1, 1, 1)
        GameTooltip:AddLine(statusLine, 0.8, 0.8, 0.8)
        GameTooltip:Show()
    end)
    f:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Live tooltip refresh when CD is showing
    f:SetScript("OnUpdate", function(self, elapsed)
        self._tipTick = (self._tipTick or 0) + elapsed
        if self._tipTick < 0.1 then return end
        self._tipTick = 0
        if GameTooltip:IsOwned(self) and self._endTime and self._endTime > GetTime() then
            GameTooltip:Show()  -- forces re-render with current line text
        end
    end)

    f:Hide()
    return f
end

local function ApplyClassTint(self, onCooldown)
    if onCooldown then
        local _, class = UnitClass(self.unit or "player")
        local c = RAID_CLASS_COLORS and class and RAID_CLASS_COLORS[class]
        if c then self.tex:SetVertexColor(c.r, c.g, c.b)
        else      self.tex:SetVertexColor(0.55, 0.55, 0.55) end
    else
        self.tex:SetVertexColor(1, 1, 1)
    end
end

local function SetReady(self, ready)
    if not self then return end
    if ready then
        self.readyBorder:Show()
        ApplyClassTint(self, false)
        if self.pulseAG and not self.pulseAG:IsPlaying() then self.pulseAG:Play() end
    else
        self.readyBorder:Hide()
        ApplyClassTint(self, true)
    end
end

local function StartCooldown(self, duration)
    if not self or not duration or duration <= 0 then return end
    self.cd:SetCooldown(GetTime(), duration)
    self._endTime = GetTime() + duration
    self._lastStarted = GetTime()      -- used as Option C tie-break ranking
    self._lastDuration = duration      -- used as Option C primary rank key
    SetReady(self, false)
    if self._readyTimer then self._readyTimer:Cancel() end
    self._readyTimer = C_Timer.NewTimer(duration, function()
        self._readyTimer = nil
        self._endTime = nil
        SetReady(self, true)
        if M._UpdateRotation then M._UpdateRotation() end
    end)
    self:Show()
    if M._UpdateRotation then M._UpdateRotation() end
end

-- ============================================================
-- Container (group mover) + layout
-- ============================================================
-- Single draggable container holds all icons in a row (or column).
-- User moves the container via MidnightUI Edit Mode; icons follow.
local container

local DEFAULT_POSITION = "CENTER,UIParent,CENTER,0,200"

local function EnsureContainer()
    if container then return container end
    container = CreateFrame("Frame", "MidnightUI_InterruptTrackerContainer", UIParent, "BackdropTemplate")
    container:SetSize(160, 32)
    container:SetFrameStrata("MEDIUM")
    container:SetClampedToScreen(true)
    container:Hide()

    -- Set a sane anchor up-front so the container has a position even if
    -- MR's StringToPoint or pcall-wrapped SetPoint silently fails.
    container:ClearAllPoints()
    container:SetPoint("CENTER", UIParent, "CENTER", 0, 200)

    -- Register with MoverRegistry so MidnightUI Edit Mode can drag it.
    if _G.MidnightUI_MoverRegistry and _G.MidnightUI_MoverRegistry.RegisterMover then
        _G.MidnightUI_MoverRegistry.RegisterMover("InterruptTracker", container, DEFAULT_POSITION, {
            category = "auras",
            label    = "Interrupt Tracker",
            -- When Edit Mode unlocks, force the container visible so the drag
            -- overlay can be grabbed even if the player is solo / out of group.
            onUnlock = function(frame) if frame then frame:Show() end end,
            onLock   = function() if M.Refresh then M.Refresh() end end,
        })
    end
    return container
end

-- Arrange visible icons inside the container, in slot order.
local function LayoutIcons()
    EnsureContainer()
    local s = GetSettings()
    local size    = (s and s.iconSize) or DEFAULT_ICON_SIZE
    local spacing = (s and s.spacing)  or 4
    local horiz   = (s and s.orientation or "HORIZONTAL") == "HORIZONTAL"

    -- Build the visible-slot list in fixed slot order
    local visible = {}
    for _, unit in ipairs(SLOT_UNITS) do
        local ico = icons[unit]
        if ico and ico:IsShown() then
            table.insert(visible, ico)
        end
    end

    -- Position each icon inside container along the chosen axis
    for i, ico in ipairs(visible) do
        ico:ClearAllPoints()
        if horiz then
            ico:SetPoint("LEFT", container, "LEFT", (i - 1) * (size + spacing), 0)
        else
            ico:SetPoint("TOP",  container, "TOP",  0, -(i - 1) * (size + spacing))
        end
    end

    -- Resize container to wrap its visible children
    local count = #visible
    if count == 0 then
        container:SetSize(size, size)  -- minimum size so the mover handle still works
    elseif horiz then
        container:SetSize(count * size + math.max(0, count - 1) * spacing, size)
    else
        container:SetSize(size, count * size + math.max(0, count - 1) * spacing)
    end
end

-- ============================================================
-- Visibility
-- ============================================================
local function ShouldShowSystem()
    local s = GetSettings()
    if s and s.enabled == false then return false end
    if not IsInGroup() then return false end
    if IsInRaid() then return false end
    return true
end

local function IsEditModeActive()
    local MR = _G.MidnightUI_MoverRegistry
    if not MR or not MR.GetEntry then return false end
    local entry = MR.GetEntry("InterruptTracker")
    return entry and entry.dragOverlay and entry.dragOverlay:IsShown() or false
end

local function ApplyVisibility()
    local show = ShouldShowSystem() or IsEditModeActive()
    if container then
        if show then container:Show() else container:Hide() end
    end
    local s = GetSettings()
    local hideStrangers = s and s.showNonMUIUsers == false
    for _, unit in ipairs(SLOT_UNITS) do
        local ico = icons[unit]
        if ico then
            if not show then
                ico:Hide()
            else
                local kick = GetKickDataForUnit(unit)
                if (unit == "player" or UnitExists(unit)) and kick then
                    if hideStrangers and unit ~= "player" then
                        local nm = UnitName(unit)
                        if nm and syncedPeers[nm] then ico:Show() else ico:Hide() end
                    else
                        ico:Show()
                    end
                else
                    ico:Hide()
                end
            end
        end
    end
end

-- ============================================================
-- (Re)build all icons
-- ============================================================
local function RebuildAll()
    EnsureContainer()
    local s = GetSettings()
    local size = (s and s.iconSize) or DEFAULT_ICON_SIZE

    for _, unit in ipairs(SLOT_UNITS) do
        local ico = icons[unit]
        if not ico then
            ico = CreateIcon(container, unit, size)
            icons[unit] = ico
        else
            ico:SetParent(container)
            ico:SetSize(size, size)
        end

        -- Set icon texture from this unit's class kick (if any) or test placeholder
        local kick = GetKickDataForUnit(unit)
        if kick then
            ico.tex:SetTexture(kick.icon)
            ico.spellID  = kick.id
            ico.duration = kick.cd
            if not (ico._readyTimer) then SetReady(ico, true) end
        end
    end

    ApplyVisibility()
    LayoutIcons()
    if M._UpdateRotation then M._UpdateRotation() end
end

function M.Refresh()
    RebuildAll()
end

-- Used by Settings.lua's Overlay Manager toggle.
_G.MidnightUI_RefreshInterruptTracker = function() RebuildAll() end

-- ============================================================
-- Sync (addon-comm)
-- ============================================================

-- Pick the right comm channel for the current group context.
local function PickChannel()
    if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then return "INSTANCE_CHAT" end
    if IsInInstance() then return "INSTANCE_CHAT" end
    if IsInGroup() then return "PARTY" end
    return nil
end

local function SendComm(payload)
    local ch = PickChannel()
    if not ch then return end
    pcall(C_ChatInfo.SendAddonMessage, ADDON_PREFIX, payload, ch)
end

-- Find which slot ("party1".."party4") corresponds to a given player name.
local function UnitForName(playerName)
    if not playerName or playerName == "" then return nil end
    local short = Ambiguate(playerName, "short")
    for i = 1, 4 do
        local u = "party" .. i
        if UnitExists(u) then
            local name = UnitName(u)
            if name and (name == playerName or name == short) then
                return u
            end
        end
    end
    return nil
end

local function HandleIncomingKick(senderName, durationStr)
    local cd = tonumber(durationStr)
    if not cd or cd <= 0 then return end
    local unit = UnitForName(senderName)
    if not unit then return end
    local ico = icons[unit]
    if not ico then return end
    StartCooldown(ico, cd)
end

local function HandleIncoming(prefix, message, _, sender)
    if prefix ~= ADDON_PREFIX or not message then return end
    local short = sender and Ambiguate(sender, "short") or sender
    -- Ignore our own broadcasts
    if short == UnitName("player") then return end

    local wasKnown = syncedPeers[short]
    syncedPeers[short] = true
    -- If "Show non-MidnightUI users" is OFF, a freshly-detected peer needs an
    -- icon-row refresh so their icon becomes visible.
    if not wasKnown and M.Refresh then M.Refresh() end

    local kind, rest = message:match("^([^|]+)|?(.*)$")
    if not kind then return end

    if kind == "K" then
        HandleIncomingKick(short, rest)
    end
end

-- ============================================================
-- Stranger fallback (name match + timestamp correlation)
-- ============================================================

-- Build a lowercase-name → {cd, isPet} table from CLASS_KICK + PET_KICK.
-- Names aren't secret values, so this lookup works in restricted combat.
local function BuildNameKickTable()
    if NAME_KICK_BUILT or not (C_Spell and C_Spell.GetSpellName) then return end
    for _, data in pairs(CLASS_KICK) do
        local n = C_Spell.GetSpellName(data.id)
        if n then NAME_KICK[n:lower()] = { cd = data.cd, isPet = false } end
    end
    for sid, data in pairs(PET_KICK) do
        local n = C_Spell.GetSpellName(sid)
        if n then NAME_KICK[n:lower()] = { cd = data.cd, isPet = true } end
    end
    NAME_KICK_BUILT = next(NAME_KICK) ~= nil
end

-- Map "partypetN" → "partyN"
local function OwnerOfPet(unit)
    if not unit then return nil end
    local n = unit:match("^partypet(%d)$")
    if n then return "party" .. n end
    return nil
end

-- True if a teammate at this unit slot is eligible to be credited with a kick:
-- has a kick, role isn't no-kick healer, kick currently not on CD, not synced.
local function IsEligibleStranger(unit)
    if not UnitExists(unit) then return false end
    local s = GetSettings()
    if not s or s.showNonMUIUsers == false then return false end

    local name = UnitName(unit)
    if name and syncedPeers[name] then return false end

    local kick = GetKickDataForUnit(unit)
    if not kick then return false end

    local ico = icons[unit]
    if ico and ico._readyTimer then return false end  -- already on CD
    return true
end

local function StoreRecentCast(unit)
    if IsEligibleStranger(unit) then
        recentPartyCasts[unit] = GetTime()
    end
end

-- Try name-match for a tainted spell ID. Returns kick data table or nil.
local function NameMatchKick(rawID)
    if not (C_Spell and C_Spell.GetSpellName) then return nil end
    local n = C_Spell.GetSpellName(rawID)
    if not n then return nil end
    return NAME_KICK[n:lower()]
end

-- Try to credit the most recent eligible teammate within the correlation window.
local function CorrelateMobInterrupt()
    local now = GetTime()
    local bestUnit, bestDelta = nil, 999
    for unit, t in pairs(recentPartyCasts) do
        local delta = now - t
        if delta <= CORRELATION_WINDOW and delta < bestDelta then
            if IsEligibleStranger(unit) then
                bestUnit, bestDelta = unit, delta
            end
        end
    end
    if not bestUnit then return end
    local kick = GetKickDataForUnit(bestUnit)
    if not kick then return end
    StartCooldown(icons[bestUnit], kick.cd)
    recentPartyCasts[bestUnit] = nil   -- consume so a duplicate event can't double-credit
end

-- Garbage-collect expired timestamps to keep the table tiny.
local function PurgeOldCasts()
    local cutoff = GetTime() - CORRELATION_WINDOW
    for unit, t in pairs(recentPartyCasts) do
        if t < cutoff then recentPartyCasts[unit] = nil end
    end
end

-- Handle a UNIT_SPELLCAST_SUCCEEDED on a party slot or party pet.
local function HandlePartyCast(unit, rawID)
    -- Map partypets to their owner slot for credit purposes.
    local ownerUnit = OwnerOfPet(unit) or unit
    if not ownerUnit:match("^party%d$") then return end
    if not UnitExists(ownerUnit) then return end

    local name = UnitName(ownerUnit)
    if name and syncedPeers[name] then return end  -- they broadcast their own kicks

    local s = GetSettings()
    if not s or s.showNonMUIUsers == false then return end

    local spellID = LaunderSpellID(rawID)

    -- Layer (a): name match
    if spellID then
        local match = NameMatchKick(rawID)
        if match then
            local ico = icons[ownerUnit]
            if ico and not ico._readyTimer then
                StartCooldown(ico, match.cd)
                return
            end
        end
    end

    -- Layer (b): timestamp store for later correlation
    StoreRecentCast(ownerUnit)
end

-- ============================================================
-- Event handling
-- ============================================================
local ef = CreateFrame("Frame")
ef:RegisterEvent("PLAYER_LOGIN")
ef:RegisterEvent("PLAYER_ENTERING_WORLD")
ef:RegisterEvent("GROUP_ROSTER_UPDATE")
ef:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
ef:RegisterEvent("ROLE_CHANGED_INFORM")
ef:RegisterEvent("CHAT_MSG_ADDON")
ef:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED",
    "player", "pet",
    "party1", "party2", "party3", "party4",
    "partypet1", "partypet2", "partypet3", "partypet4")
-- UNIT_SPELLCAST_INTERRUPTED registered as a normal event; fires for any
-- tracked unit (target/focus/boss/nameplate). We only need to know that
-- ANY interrupt landed to drive timestamp correlation.
ef:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")

ef:SetScript("OnEvent", function(self, event, ...)
    if event == "PLAYER_LOGIN" then
        if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
            C_ChatInfo.RegisterAddonMessagePrefix(ADDON_PREFIX)
        end
        C_Timer.After(0.2, function()
            BuildNameKickTable()
            RebuildAll()
        end)
        return
    end

    if event == "PLAYER_ENTERING_WORLD" then
        C_Timer.After(0.2, function()
            BuildNameKickTable()
            RebuildAll()
            SendComm("H|" .. SYNC_VERSION)
        end)
        return
    end

    if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_SPECIALIZATION_CHANGED" or event == "ROLE_CHANGED_INFORM" then
        C_Timer.After(0.1, function()
            RebuildAll()
            if event == "GROUP_ROSTER_UPDATE" then SendComm("H|" .. SYNC_VERSION) end
        end)
        return
    end

    if event == "CHAT_MSG_ADDON" then
        HandleIncoming(...)
        return
    end

    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        local unit = ...
        -- Only react if it hit something we can attack (i.e. a mob, not a teammate)
        if unit and UnitExists(unit) and not UnitIsFriend("player", unit) then
            PurgeOldCasts()
            CorrelateMobInterrupt()
        end
        return
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit, _, rawID = ...

        -- Self path
        if unit == "player" or unit == "pet" or unit == "playerpet" then
            local spellID = LaunderSpellID(rawID)
            if not spellID then return end
            local _, class = UnitClass("player")
            local kick = class and CLASS_KICK[class]

            if unit == "player" and kick and spellID == kick.id then
                local cd = GetEffectiveCD("player", kick.cd)
                StartCooldown(icons["player"], cd)
                SendComm("K|" .. cd)
                return
            end

            if (unit == "pet" or unit == "playerpet") and PET_KICK[spellID] then
                local p = PET_KICK[spellID]
                local ico = icons["player"]
                if ico then
                    ico.tex:SetTexture(p.icon)
                    ico.spellID  = spellID
                    ico.duration = p.cd
                    StartCooldown(ico, p.cd)
                    SendComm("K|" .. p.cd)
                end
            end
            return
        end

        -- Stranger path (party1..4 + their pets)
        HandlePartyCast(unit, rawID)
    end
end)

-- ============================================================
-- Rotation ranking (Option C: longest CD desc, then least-recently-kicked)
-- ============================================================
-- When multiple players are ready, rank them so the player with the longest
-- base CD goes first (preserves short-CD "insurance" kickers for back-to-back
-- mob casts). Ties are broken by who kicked least recently (round-robin
-- fairness). Final tie-break: slot order (player → party4) so all clients
-- agree even with no kick history.
local NEXT_TINT_R, NEXT_TINT_G, NEXT_TINT_B = 0.25, 1.00, 0.40

local function GetUnitBaseCD(unit)
    local kick = GetKickDataForUnit(unit)
    return (kick and kick.cd) or 999
end

-- Role-based priority for tie-breaking. Tanks already in melee range take
-- priority; healers go last so they can spot-heal during boss casts.
local ROLE_PRIORITY = { TANK = 1, DAMAGER = 2, HEALER = 3, NONE = 4 }
local function GetUnitRolePriority(unit)
    local role = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unit) or "NONE"
    return ROLE_PRIORITY[role] or 4
end

local function BuildReadyQueue()
    local queue = {}
    for slotIndex, unit in ipairs(SLOT_UNITS) do
        local ico = icons[unit]
        if ico and ico:IsShown() and (not ico._endTime or ico._endTime <= GetTime()) then
            table.insert(queue, {
                ico       = ico,
                unit      = unit,
                slotIndex = slotIndex,
                baseCD    = GetUnitBaseCD(unit),
                lastUsed  = ico._lastStarted or 0,
                rolePri   = GetUnitRolePriority(unit),
                name      = (UnitName(unit) or unit):lower(),  -- deterministic across all clients
            })
        end
    end
    -- Sort priority (each tier breaks ties in the next):
    --   1) longer baseCD first  (Option A: burn long CDs, save short ones)
    --   2) least-recently-kicked first  (round-robin fairness)
    --   3) role priority: Tank → DPS → Healer  (M+ kick-order wisdom)
    --   4) alphabetical by name  (final tie-break, identical on every client)
    table.sort(queue, function(a, b)
        if a.baseCD   ~= b.baseCD   then return a.baseCD   > b.baseCD   end
        if a.lastUsed ~= b.lastUsed then return a.lastUsed < b.lastUsed end
        if a.rolePri  ~= b.rolePri  then return a.rolePri  < b.rolePri  end
        return a.name < b.name
    end)
    return queue
end

local function UpdateRotation()
    -- Reset every icon's base state and clear rank badges
    for _, unit in ipairs(SLOT_UNITS) do
        local ico = icons[unit]
        if ico then
            local onCD = ico._endTime and ico._endTime > GetTime()
            ApplyClassTint(ico, onCD)
            ico.rankBadge:Hide()
            if onCD then
                ico.readyBorder:Hide()
            else
                ico.readyBorder:Show()
                ico.readyBorder:SetVertexColor(1.0, 0.65, 0.10, 0.60)  -- amber default
            end
        end
    end

    local queue = BuildReadyQueue()

    -- Rank #1 gets the green border
    if queue[1] then
        local w = queue[1].ico
        w.readyBorder:Show()
        w.readyBorder:SetVertexColor(NEXT_TINT_R, NEXT_TINT_G, NEXT_TINT_B, 0.85)
    end

    -- Show numeric badges only when there are multiple ready kickers
    if #queue >= 2 then
        for rank, entry in ipairs(queue) do
            entry.ico.rankBadge:SetText(tostring(rank))
            entry.ico.rankBadge:Show()
        end
    end
end
M._UpdateRotation = UpdateRotation

-- ============================================================
-- Right-click Overlay Settings panel
-- ============================================================
local function BuildInterruptTrackerOverlaySettings(content)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    local s = GetSettings()
    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Interrupt Tracker")

    b:Checkbox("Enabled", s and s.enabled ~= false, function(v)
        local cs = GetSettings(); if not cs then return end
        cs.enabled = v and true or false
        if M.Refresh then M.Refresh() end
    end)

    b:Slider("Icon Size", 16, 64, 1, (s and s.iconSize) or DEFAULT_ICON_SIZE, function(v)
        local cs = GetSettings(); if not cs then return end
        cs.iconSize = math.floor((tonumber(v) or DEFAULT_ICON_SIZE) + 0.5)
        if M.Refresh then M.Refresh() end
    end)

    b:Slider("Spacing", 0, 500, 1, (s and s.spacing) or 4, function(v)
        local cs = GetSettings(); if not cs then return end
        cs.spacing = math.floor((tonumber(v) or 4) + 0.5)
        if M.Refresh then M.Refresh() end
    end)

    local currentOrient = (s and s.orientation == "VERTICAL") and "Vertical" or "Horizontal"
    b:Dropdown("Orientation", { "Horizontal", "Vertical" }, currentOrient, function(v)
        local cs = GetSettings(); if not cs then return end
        cs.orientation = (v == "Vertical") and "VERTICAL" or "HORIZONTAL"
        if M.Refresh then M.Refresh() end
    end)

    b:Checkbox("Show non-MidnightUI users", s and s.showNonMUIUsers ~= false, function(v)
        local cs = GetSettings(); if not cs then return end
        cs.showNonMUIUsers = v and true or false
        if M.Refresh then M.Refresh() end
    end)

    return b:Height()
end

if _G.MidnightUI_RegisterOverlaySettings then
    _G.MidnightUI_RegisterOverlaySettings("InterruptTracker", {
        title = "Interrupt Tracker",
        build = BuildInterruptTrackerOverlaySettings,
    })
end

SLASH_MUIINTERRUPTS1 = "/muiint"
SLASH_MUIINTERRUPTS2 = "/muiinterrupts"
SlashCmdList["MUIINTERRUPTS"] = function()
    if M.Refresh then M.Refresh() end
    print("|cFF66CCFF[MUI Interrupts]|r refreshed")
end
