local _, ns = ...

local Castbar = {}
ns.Castbar = Castbar

Castbar.frames  = {}
Castbar.anchors = {}

-- ============================================================
-- Helpers
-- ============================================================
local function CreateBorder(frame, size)
    frame:CreateBeautyBorder(size or 11)
    frame:SetBeautyBorderPadding(2.66)
end

local function ResolveTexture(config)
    local texName = config and config.statusbarTexture
    if texName == "smooth_v2" then
        return "Interface\\AddOns\\nCastbar\\Media\\smooth_v2"
    end
    if texName then
        local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
        if LSM then
            local path = LSM:Fetch("statusbar", texName)
            if path then return path end
        end
    end
    return "Interface\\AddOns\\nCastbar\\Media\\smooth_v2"
end

-- ============================================================
-- Empower stage tick helpers
-- Tick marks are thin vertical lines drawn at each stage boundary.
-- Positions come from UnitEmpoweredStagePercentages (cumulative).
-- ============================================================
local function hide_empower_stages(castbar)
    if castbar.empTickPool then
        for _, t in ipairs(castbar.empTickPool) do
            t:Hide()
        end
    end
    castbar.numActiveTicks = 0
end

local function setup_empower_stages(castbar)
    local unit = castbar.unit
    if not UnitEmpoweredStagePercentages then return end

    local rawStages = UnitEmpoweredStagePercentages(unit, true)
    if not rawStages or #rawStages == 0 then return end

    -- Build cumulative boundary positions
    local positions = {}
    local cumSum = 0
    for i = 1, #rawStages do
        cumSum = cumSum + rawStages[i]
        positions[i] = math.min(cumSum, 1.0)
    end

    local numStages = #positions
    local numTicks  = numStages - 1  -- no tick at the very end (bar edge)
    if numTicks < 1 then return end

    if not castbar.empTickPool then
        castbar.empTickPool = {}
    end

    hide_empower_stages(castbar)

    local barWidth  = castbar:GetWidth()
    local barHeight = castbar:GetHeight()

    for i = 1, numTicks do
        local tick = castbar.empTickPool[i]
        if not tick then
            tick = castbar:CreateTexture(nil, "OVERLAY")
            castbar.empTickPool[i] = tick
        end
        local xOffset = positions[i] * barWidth
        tick:SetColorTexture(1, 1, 1, 0.85)
        tick:SetSize(2, barHeight + 6)
        tick:ClearAllPoints()
        tick:SetPoint("LEFT", castbar, "LEFT", xOffset - 1, 0)
        tick:Show()
    end

    castbar.numActiveTicks = numTicks
end

-- ============================================================
-- Anchor (draggable positioning frame)
-- ============================================================
function Castbar:CreateAnchor(unit, label)
    local config = nCastbarDB.profile[unit]
    if not config then return end
    local anchor = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    anchor:SetSize((config.width + config.height) * config.scale, config.height * config.scale)
    anchor:SetScale(1.193)
    anchor:SetPoint(unpack(config.position or {"CENTER"}))
    anchor:SetFrameStrata("HIGH")
    anchor:SetMovable(true)
    anchor:SetClampedToScreen(true)
    anchor:SetUserPlaced(true)
    anchor:SetBackdrop({ bgFile = "Interface\\MINIMAP\\TooltipBackdrop-Background" })
    CreateBorder(anchor, 11)
    anchor:EnableMouse(true)
    anchor:RegisterForDrag("LeftButton")
    anchor:Hide()
    anchor.text = anchor:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    anchor.text:SetAllPoints(anchor)
    anchor.text:SetText(label .. " Castbar")
    anchor:SetScript("OnDragStart", function(self) self:StartMoving() end)
    anchor:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint()
        config.position = {point, "UIParent", relPoint, x, y}
    end)
    self.anchors[unit] = anchor
    return anchor
end

-- ============================================================
-- Create
-- ============================================================
function Castbar:Create(unit)
    local config = nCastbarDB.profile[unit]
    if not config or not config.enabled then return end

    -- Alten Frame aufraumen falls vorhanden (z.B. nach Reload)
    if self.frames[unit] then
        local old = self.frames[unit]
        old:UnregisterAllEvents()
        old:SetScript("OnEvent", nil)
        old:Hide()
        if old.fadeOut then old.fadeOut:Stop() end
        self.frames[unit] = nil
    end

    if not self.anchors[unit] then
        self:CreateAnchor(unit, unit:gsub("^%l", string.upper))
    end

    local texturePath = ResolveTexture(config)

    -- StatusBar frame: benannter Frame, einmalig erstellt und dann wiederverwendet.
    -- Verhindert Zombie-Frames mit aktiven Event-Listenern nach Reload.
    local frameName = "nCastbar_Bar_" .. unit
    local isNew = (_G[frameName] == nil)
    local castbar = _G[frameName] or CreateFrame("StatusBar", frameName, UIParent)

    -- Events immer zuruecksetzen (egal ob neu oder recycelt)
    castbar:UnregisterAllEvents()
    castbar:SetScript("OnEvent", nil)

    -- Groesse, Textur und Position immer aktualisieren
    castbar:SetStatusBarTexture(texturePath)
    local tex = castbar:GetStatusBarTexture()
    if tex then tex:SetHorizTile(false) end
    castbar:SetSize(config.width, config.height)
    castbar:SetScale(config.scale)
    castbar:SetMinMaxValues(0, 1)
    castbar:SetValue(0)
    castbar:ClearAllPoints()
    if self.anchors[unit] then
        castbar:SetPoint("CENTER", self.anchors[unit])
    elseif config.position then
        castbar:SetPoint(unpack(config.position))
    end

    -- Child-Objekte nur beim ersten Erstellen anlegen (nicht bei Reload recycling)
    if isNew then
        -- Background
        castbar.bg = castbar:CreateTexture(nil, "BACKGROUND")
        castbar.bg:SetAllPoints(castbar)
        CreateBorder(castbar, 11)

        -- notInterruptible shield overlay
        castbar.shieldOverlay = castbar:CreateTexture(nil, "ARTWORK")
        castbar.shieldOverlay:SetAllPoints(castbar)
        castbar.shieldOverlay:SetColorTexture(unpack(config.colors.nonInterruptible or {0.7, 0.7, 0.7}))

        -- Spell name font
        local fontObjName = "nCastbar_Font_" .. unit
        if not _G[fontObjName] then _G[fontObjName] = CreateFont(fontObjName) end
        local spellFont = _G[fontObjName]
        castbar.text = castbar:CreateFontString(nil, "OVERLAY")
        castbar.text:SetFontObject(spellFont)
        castbar.text:SetWordWrap(false)
        castbar.time = castbar:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        castbar.time:SetJustifyH("RIGHT")

        -- Icon
        if config.icon and config.icon.show then
            castbar.iconFrame = CreateFrame("Frame", nil, castbar)
            castbar.iconFrame:SetSize(config.height, config.height)
            castbar.icon = castbar.iconFrame:CreateTexture(nil, "ARTWORK")
            castbar.icon:SetAllPoints(castbar.iconFrame)
            castbar.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            if config.icon.positionOutside then
                castbar.iconFrame:CreateBeautyBorder(10)
                castbar.iconFrame:SetBeautyBorderPadding(2)
            end
        end

        -- Player-specific extras
        if unit == "player" then
            if config.showSafezone then
                castbar.safezone = castbar:CreateTexture(nil, "OVERLAY")
                castbar.safezone:SetColorTexture(unpack(config.colors.safezone or {1,0,1,1}))
                castbar.safezone:SetPoint("TOPRIGHT")
                castbar.safezone:SetPoint("BOTTOMRIGHT")
            end
            if config.showLatency then
                castbar.latency = castbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                castbar.latency:SetPoint("RIGHT", castbar, "BOTTOMRIGHT", -2, -2)
                castbar.latency:SetTextColor(0.6, 0.6, 0.6, 1)
            end
        end

        -- Fade animation (einmalig erstellen)
        castbar.fadeOut = castbar:CreateAnimationGroup()
        local anim = castbar.fadeOut:CreateAnimation("Alpha")
        anim:SetFromAlpha(1); anim:SetToAlpha(0); anim:SetDuration(0.3); anim:SetSmoothing("IN")
        castbar.fadeOut:SetScript("OnFinished", function(ag)
            local f = ag:GetParent()
            f:Hide()
            f:SetAlpha(1)
            f:SetReverseFill(false)
            f:SetMinMaxValues(0, 1)
            f:SetValue(0)
            if f.shieldOverlay then f.shieldOverlay:SetAlpha(0) end
            hide_empower_stages(f)
        end)
    end

    -- Immer aktualisieren: Font-Einstellungen koennen sich geaendert haben
    do
        local fontObjName = "nCastbar_Font_" .. unit
        if not _G[fontObjName] then _G[fontObjName] = CreateFont(fontObjName) end
        local spellFont = _G[fontObjName]
        local LSM      = LibStub and LibStub("LibSharedMedia-3.0", true)
        local fontSize = config.spellNameFontSize or 12
        local outline  = config.spellNameOutline or "OUTLINE"
        if outline == "NONE" then outline = "" end
        local fontName = config.spellNameFont or "Friz Quadrata TT"
        local fontPath = (LSM and LSM:Fetch("font", fontName))
                      or "Interface\\AddOns\\nCastbar\\Media\\FRIZQT__.ttf"
        spellFont:SetFont(fontPath, fontSize, outline)
        spellFont:SetTextColor(1, 1, 1, 1)
        if castbar.text then castbar.text:SetFontObject(spellFont) end
    end

    -- Icon-Position immer aktualisieren (kann sich geaendert haben)
    if castbar.iconFrame then
        castbar.iconFrame:ClearAllPoints()
        if config.icon and config.icon.show then
            castbar.iconFrame:Show()
            if config.icon.position == "LEFT" then
                if config.icon.positionOutside then
                    castbar.iconFrame:SetPoint("RIGHT", castbar, "LEFT", -8, 0)
                else
                    castbar.iconFrame:SetPoint("LEFT", castbar, "LEFT", 2, 0)
                end
            else
                if config.icon.positionOutside then
                    castbar.iconFrame:SetPoint("LEFT", castbar, "RIGHT", 8, 0)
                else
                    castbar.iconFrame:SetPoint("RIGHT", castbar, "RIGHT", -2, 0)
                end
            end
        else
            castbar.iconFrame:Hide()
        end
    end

    -- TextPosition immer aktualisieren
    if castbar.text then
        self:UpdateTextPosition(castbar, config)
    end

    -- bg-Farbe aktualisieren
    if castbar.bg then
        castbar.bg:SetColorTexture(0, 0, 0, 0.5)
    end
    if castbar.shieldOverlay then
        castbar.shieldOverlay:SetAlpha(0)
    end
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
    end

    -- State
    castbar.unit           = unit
    castbar.config         = config
    castbar.casting         = false
    castbar.isEmpowered     = false
    castbar.failstart       = nil
    castbar.successstart    = nil
    castbar.duration_obj    = nil
    castbar.currentCastGUID = nil
    castbar.numActiveTicks  = 0
    castbar:Hide()

    self.frames[unit] = castbar
    self:RegisterEvents(castbar)
    return castbar
end

-- ============================================================
-- UpdateTextPosition
-- ============================================================
function Castbar:UpdateTextPosition(castbar, config)
    castbar.text:ClearAllPoints()
    castbar.time:ClearAllPoints()
    local pos        = config.textPosition or "inside"
    local showTime   = config.showTime ~= false
    local iconInside = castbar.iconFrame
                   and config.icon
                   and config.icon.show
                   and not config.icon.positionOutside

    if pos == "above" then
        castbar.text:SetPoint("BOTTOMLEFT",  castbar, "TOPLEFT",  0, 2)
        castbar.text:SetPoint("BOTTOMRIGHT", castbar, "TOPRIGHT", 0, 2)
        castbar.text:SetJustifyH("CENTER")
        if showTime then
            castbar.time:SetPoint("LEFT", castbar.text, "RIGHT", 4, 0)
            castbar.time:Show()
        else castbar.time:Hide() end

    elseif pos == "below" then
        castbar.text:SetPoint("TOPLEFT",  castbar, "BOTTOMLEFT",  0, -2)
        castbar.text:SetPoint("TOPRIGHT", castbar, "BOTTOMRIGHT", 0, -2)
        castbar.text:SetJustifyH("CENTER")
        if showTime then
            castbar.time:SetPoint("LEFT", castbar.text, "RIGHT", 4, 0)
            castbar.time:Show()
        else castbar.time:Hide() end

    else -- inside
        if config.textAlignCenter then
            castbar.text:SetPoint("LEFT",  castbar, "LEFT",  4, 0)
            castbar.text:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
            castbar.text:SetJustifyH("CENTER")
            if showTime then
                castbar.time:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
                castbar.time:Show()
            else castbar.time:Hide() end
        else
            castbar.text:SetJustifyH("LEFT")
            if iconInside and config.icon.position == "LEFT" then
                castbar.text:SetPoint("LEFT", castbar.iconFrame, "RIGHT", 4, 0)
            else
                castbar.text:SetPoint("LEFT", castbar, "LEFT", 4, 0)
            end
            if showTime then
                if iconInside and config.icon.position == "RIGHT" then
                    castbar.text:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                    castbar.time:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                else
                    castbar.text:SetPoint("RIGHT", castbar, "CENTER", -10, 0)
                    castbar.time:SetPoint("RIGHT", castbar, "RIGHT",   -4, 0)
                end
                castbar.time:Show()
            else
                if iconInside and config.icon.position == "RIGHT" then
                    castbar.text:SetPoint("RIGHT", castbar.iconFrame, "LEFT", -4, 0)
                else
                    castbar.text:SetPoint("RIGHT", castbar, "RIGHT", -4, 0)
                end
                castbar.time:Hide()
            end
        end
    end
end

-- ============================================================
-- show_success – called when a cast completes successfully
-- ============================================================
local function show_success(castbar)
    local config = castbar.config

    -- Stop any running fade
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
    end

    -- Hide stage ticks on success
    hide_empower_stages(castbar)

    if not config.castSuccess then
        castbar.casting      = false
        castbar.isEmpowered  = false
        castbar.duration_obj = nil
        castbar.failstart    = nil
        castbar.successstart = nil
        if castbar.fadeOut then
            castbar.fadeOut:Play()
        else
            castbar:SetReverseFill(false)
            castbar:SetMinMaxValues(0, 1)
            castbar:SetValue(0)
            if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
            castbar:Hide()
        end
        return
    end

    -- Fill bar to 100%
    castbar:SetReverseFill(false)
    castbar:SetMinMaxValues(0, 100)
    castbar:SetValue(100)

    -- Apply success color
    local c = config.colors.success or {0.0, 0.8, 0.0}
    castbar:SetStatusBarColor(unpack(c))
    castbar.bg:SetColorTexture(c[1]*0.3, c[2]*0.3, c[3]*0.3, 0.5)

    if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end

    -- Mark success state (OnTick will fade after hold time)
    castbar.casting      = false
    castbar.isEmpowered  = false
    castbar.duration_obj = nil
    castbar.successstart = GetTime()

    castbar:Show()
end

-- ============================================================
-- hide_castbar
-- ============================================================
local function hide_castbar(castbar)
    castbar.casting         = false
    castbar.isEmpowered     = false
    castbar.isChannel       = false
    castbar.failstart       = nil
    castbar.successstart    = nil
    castbar.duration_obj    = nil
    castbar.currentCastGUID = nil
    hide_empower_stages(castbar)

    if castbar.fadeOut then
        -- Stop laufende Animation und alpha resetten
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
        -- Nur faden wenn der Frame sichtbar ist
        if castbar:IsShown() then
            castbar.fadeOut:Play()
        else
            castbar:SetReverseFill(false)
            castbar:SetMinMaxValues(0, 1)
            castbar:SetValue(0)
            if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        end
    else
        castbar:SetReverseFill(false)
        castbar:SetMinMaxValues(0, 1)
        castbar:SetValue(0)
        if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        castbar:Hide()
    end
end

-- ============================================================
-- UpdateCastColor
-- ============================================================
local function UpdateCastColor(castbar, channeling)
    local config = castbar.config
    local color
    if config.classColorCast then
        local ok, _, class = pcall(UnitClass, castbar.unit)
        if ok and class then
            if not (issecretvalue and issecretvalue(class)) then
                if RAID_CLASS_COLORS[class] then
                    local c = RAID_CLASS_COLORS[class]
                    color = {c.r, c.g, c.b}
                end
            end
        end
    end
    if not color then
        color = channeling and config.colors.channel or config.colors.cast
    end
    castbar:SetStatusBarColor(unpack(color))
    castbar.bg:SetColorTexture(color[1]*0.3, color[2]*0.3, color[3]*0.3, 0.5)
end

-- ============================================================
-- check_casting – core cast state handler
--
-- EMPOWERED CAST NOTES:
--   • Empowered spells appear in UnitChannelInfo with valid startMs/endMs.
--   • Add GetUnitEmpowerHoldAtMaxTime() to endMs so the bar covers the
--     "hold at max" window after the charge completes.
--   • UNIT_SPELLCAST_SUCCEEDED fires at the end of the charge phase (bar full)
--     but BEFORE the hold window ends – it must be ignored for empowered casts.
--     We wait for UNIT_SPELLCAST_EMPOWER_STOP instead (mirrors Quartz behaviour).
--   • UNIT_SPELLCAST_EMPOWER_STOP args: unit, castGUID, spellID, complete
--     (complete is arg 4 in the vararg, NOT arg 5).
--   • SetReverseFill(false) – empowered fills left→right like a normal cast.
--   • The OnTick UnitChannelInfo nil-check is skipped for empowered casts.
-- ============================================================
local function check_casting(castbar, event, interruptedBy, isEmpowered)
    local unit   = castbar.unit
    local config = castbar.config

    if not UnitExists(unit) then hide_castbar(castbar); return end

    -- ── Handle interrupts ──
    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        hide_empower_stages(castbar)
        castbar:SetReverseFill(false)
        castbar:SetMinMaxValues(0, 100)
        castbar:SetValue(100)
        castbar:SetStatusBarColor(unpack(config.colors.failed))
        castbar.bg:SetColorTexture(
            config.colors.failed[1]*0.3,
            config.colors.failed[2]*0.3,
            config.colors.failed[3]*0.3, 0.5)
        castbar.time:SetText(ns.L["INTERRUPTED"] or "Interrupted")
        castbar.text:SetText("")
        castbar.failstart    = GetTime()
        castbar.casting      = false
        castbar.isEmpowered  = false
        castbar.duration_obj = nil
        castbar.shieldOverlay:SetAlpha(0)
        if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
            castbar.fadeOut:Stop(); castbar:SetAlpha(1)
        end
        castbar:Show()
        return
    end

    -- ── Try cast, then channel ──
    local isChannel = false
    local name, _, texture, startMs, endMs, isTradeSkill, castID, notInterruptible, spellId
    name, _, texture, startMs, endMs, isTradeSkill, castID, notInterruptible, spellId = UnitCastingInfo(unit)
    if not name then
        name, _, texture, startMs, endMs, isTradeSkill, notInterruptible, spellId = UnitChannelInfo(unit)
        -- Empowered spells show up in UnitChannelInfo but fill left→right (not reversed)
        isChannel = not isEmpowered
    end

    if not name then
        -- Guard: isChannel bleibt nach show_success (CHANNEL_STOP) bewusst auf true,
        -- bis CHANNEL_START eines neuen Channels check_casting erfolgreich durchläuft.
        -- Solange isChannel=true sind alle nil-name Ereignisse (SPELLCAST_START des
        -- Instant-Triggers, CHANNEL_UPDATE-Ticks) zu ignorieren – der neue CHANNEL_START
        -- oder CHANNEL_STOP regelt das echte Ende.
        if castbar.isChannel then
            ns.Debug("check_casting: nil name (isChannel=true) – ignoriert (event=%s)", event)
            return
        end
        if not castbar.failstart and not castbar.successstart then
            hide_castbar(castbar)
        end
        return
    end

    -- ── Stop any running fade ──
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then
        castbar.fadeOut:Stop()
        castbar:SetAlpha(1)
        castbar:SetReverseFill(false)
        castbar:SetMinMaxValues(0, 1)
        castbar:SetValue(0)
        if castbar.shieldOverlay then castbar.shieldOverlay:SetAlpha(0) end
        hide_empower_stages(castbar)
    end
    castbar.successstart = nil

    -- ── Duration Object (for time text only) ──
    local durationObj
    if isEmpowered then
        if UnitEmpoweredChannelDuration then durationObj = UnitEmpoweredChannelDuration(unit) end
    elseif isChannel then
        if UnitChannelDuration then durationObj = UnitChannelDuration(unit) end
    else
        if UnitCastingDuration then durationObj = UnitCastingDuration(unit) end
    end
    castbar.duration_obj = durationObj
    castbar.isEmpowered  = isEmpowered or false

    -- ── Setup bar range and initial value ──
    castbar:SetReverseFill(isChannel)

    if isEmpowered then
        -- Add the "hold at max" window so the bar stays full until EMPOWER_STOP fires.
        -- GetUnitEmpowerHoldAtMaxTime returns milliseconds (same unit as startMs/endMs),
        -- so do NOT multiply by 1000 – that would inflate the range to ~1000 seconds
        -- and make the charge fill appear invisible (0.2% of range).
        local holdMs = 0
        if GetUnitEmpowerHoldAtMaxTime then
            holdMs = GetUnitEmpowerHoldAtMaxTime(unit) or 0
        end
        castbar:SetMinMaxValues(startMs, endMs + holdMs)
        castbar:SetValue(GetTime() * 1000)
    else
        castbar:SetMinMaxValues(startMs, endMs)
        castbar:SetValue(GetTime() * 1000)
    end

    castbar.casting     = true
    castbar.isChannel   = isChannel
    castbar.failstart   = nil
    castbar.justStarted = true

    ns.Debug("  check_casting START: name=%s isChannel=%s isEmp=%s start=%.0f end=%.0f",
        name, tostring(isChannel), tostring(isEmpowered), startMs, endMs)

    -- Text and icon
    castbar.text:SetText(name)
    if castbar.icon then castbar.icon:SetTexture(texture) end

    -- Dynamic width
    if config.dynamicWidth then
        local tw = castbar.text:GetStringWidth()
        if not (issecretvalue and issecretvalue(tw)) then
            local pad = 20
            if config.showTime ~= false then pad = pad + 55 end
            if castbar.iconFrame and config.icon.show
               and not config.icon.positionOutside then
                pad = pad + config.height + 8
            end
            castbar:SetWidth(math.max(config.width, math.min(tw + pad, 500)))
        end
    end

    -- Colors
    UpdateCastColor(castbar, isChannel)

    -- Shield overlay
    -- notInterruptible kann bei Nicht-Spieler-Units (z.B. target) als tainted
    -- secret boolean aus UnitCastingInfo/UnitChannelInfo kommen. Direkter
    -- boolean-Test darauf loest einen Taint-Fehler aus. Guard mit issecretvalue.
    do
        local shieldAlpha = 0
        if not (issecretvalue and issecretvalue(notInterruptible)) then
            shieldAlpha = notInterruptible and 0.55 or 0
        end
        castbar.shieldOverlay:SetAlpha(shieldAlpha)
    end

    -- Player-only: safezone / latency (skip for empowered; timing model differs)
    if unit == "player" and not isEmpowered and durationObj then
        local ok, totalDur = pcall(durationObj.GetTotalDuration, durationObj, 0)
        if ok and totalDur and totalDur > 0 then
            if castbar.safezone then
                local _, _, lagHome = GetNetStats()
                local w = castbar:GetWidth()
                castbar.safezone:SetWidth(math.min((w / totalDur) * (lagHome / 1000), w))
            end
        end
        if castbar.latency then
            local _, _, lH, lW = GetNetStats()
            castbar.latency:SetText(string.format("%.0fms", (lH + lW) / 2))
        end
    end

    -- Stage ticks for empowered spells
    if isEmpowered then
        setup_empower_stages(castbar)
    end

    castbar:Show()
end

-- ============================================================
-- on_unit_event
-- ============================================================
local function on_unit_event(castbar, event, ...)
    -- Debug: alle Events loggen wenn Debug aktiv
    do
        local arg1, arg2, arg3, arg4 = ...
        ns.Debug("[%s] %s  casting=%s isChannel=%s isEmp=%s",
            castbar.unit, event,
            tostring(castbar.casting),
            tostring(castbar.isChannel),
            tostring(castbar.isEmpowered))
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        -- Channeled und Empowered Spells enden via CHANNEL_STOP / EMPOWER_STOP,
        -- nicht via SUCCEEDED. SUCCEEDED mitten im Channel wuerde die Bar resetten.
        if castbar.isEmpowered then
            ns.Debug("  >> SUCCEEDED ignoriert (empowered)"); return
        end
        if castbar.isChannel then
            ns.Debug("  >> SUCCEEDED ignoriert (isChannel=true, warte auf CHANNEL_STOP)"); return
        end
        if castbar.casting then
            ns.Debug("  >> show_success")
            show_success(castbar)
        end
        return
    end

    if event == "UNIT_SPELLCAST_STOP" then
        -- isChannel bleibt nach CHANNEL_STOP / show_success auf true bis CHANNEL_START
        -- eines neuen Channels eintrifft. SPELLCAST_STOP gehört dann zum Instant-Trigger
        -- des neu gestarteten Channeled Spells und darf die Bar nicht verstecken.
        if castbar.isChannel then
            ns.Debug("  >> SPELLCAST_STOP ignoriert (isChannel=true, channel-Übergang)")
            return
        end
        ns.Debug("  >> hide_castbar (STOP)")
        hide_castbar(castbar)
        return
    end

    if event == "UNIT_SPELLCAST_FAILED" then
        -- Gleiche Logik wie SPELLCAST_STOP: FAILED des Instant-Triggers nicht als
        -- echtes Fehlschlagen des Channels werten.
        if castbar.isChannel then
            ns.Debug("  >> SPELLCAST_FAILED ignoriert (isChannel=true, channel-Übergang)")
            return
        end
        ns.Debug("  >> hide_castbar (FAILED)")
        hide_castbar(castbar)
        return
    end

    local isEmpowered = (event == "UNIT_SPELLCAST_EMPOWER_START")
                     or (event == "UNIT_SPELLCAST_EMPOWER_UPDATE")
                     or (castbar.isEmpowered and event == "UNIT_SPELLCAST_EMPOWER_STOP")

    if event == "UNIT_SPELLCAST_INTERRUPTED" then
        local interruptedBy = select(4, ...)
        ns.Debug("  >> INTERRUPTED by=%s", tostring(interruptedBy))
        check_casting(castbar, event, interruptedBy, isEmpowered)
        return

    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
        local stopGUID = select(2, ...)
        -- Stale-STOP guard: if a new channel has already started (its castGUID was stored
        -- on CHANNEL_START), this STOP belongs to the OLD channel and must be ignored.
        -- This prevents the bar from vanishing when the player re-casts the same
        -- channeled spell in quick succession (e.g. key-spam off cooldown).
        if castbar.currentCastGUID and stopGUID ~= castbar.currentCastGUID then
            ns.Debug("  >> CHANNEL_STOP stale (stop=%s current=%s) – ignored",
                tostring(stopGUID), tostring(castbar.currentCastGUID))
            return
        end
        local arg4 = select(4, ...)
        ns.Debug("  >> CHANNEL_STOP arg4=%s", tostring(arg4))
        if arg4 ~= nil then
            if not (issecretvalue and issecretvalue(arg4)) then
                check_casting(castbar, "UNIT_SPELLCAST_INTERRUPTED", arg4, false)
            end
        else
            ns.Debug("  >> CHANNEL_STOP -> show_success")
            show_success(castbar)
        end
        return

    elseif event == "UNIT_SPELLCAST_EMPOWER_STOP" then
        ns.Debug("  >> EMPOWER_STOP -> show_success")
        show_success(castbar)
        return
    end

    -- Store castGUID when a new channel begins so the stale-STOP guard above works.
    if event == "UNIT_SPELLCAST_CHANNEL_START" then
        castbar.currentCastGUID = select(2, ...)
        ns.Debug("  >> CHANNEL_START castGUID=%s", tostring(castbar.currentCastGUID))
    end

    ns.Debug("  >> check_casting (isEmp=%s)", tostring(isEmpowered))
    check_casting(castbar, event, nil, isEmpowered)
end

-- ============================================================
-- RegisterEvents
-- ============================================================
function Castbar:RegisterEvents(castbar)
    local unit = castbar.unit
    local events = {
        "UNIT_SPELLCAST_START",
        "UNIT_SPELLCAST_STOP",
        "UNIT_SPELLCAST_FAILED",
        "UNIT_SPELLCAST_SUCCEEDED",
        "UNIT_SPELLCAST_INTERRUPTED",
        "UNIT_SPELLCAST_DELAYED",
        "UNIT_SPELLCAST_CHANNEL_START",
        "UNIT_SPELLCAST_CHANNEL_STOP",
        "UNIT_SPELLCAST_CHANNEL_UPDATE",
        "UNIT_SPELLCAST_EMPOWER_START",
        "UNIT_SPELLCAST_EMPOWER_UPDATE",
        "UNIT_SPELLCAST_EMPOWER_STOP",
        "UNIT_SPELLCAST_INTERRUPTIBLE",
        "UNIT_SPELLCAST_NOT_INTERRUPTIBLE",
    }
    for _, ev in ipairs(events) do
        castbar:RegisterUnitEvent(ev, unit)
    end
    castbar:SetScript("OnEvent", on_unit_event)
end

-- ============================================================
-- OnTick – shared ticker for all castbars (~50fps)
-- ============================================================
local function OnTick()
    local now   = GetTime()
    local nowMs = now * 1000

    for _, castbar in pairs(Castbar.frames) do
        if castbar:IsShown() then
            if castbar.failstart then
                if now - castbar.failstart > 1 then
                    hide_castbar(castbar)
                end

            elseif castbar.successstart then
                if now - castbar.successstart > 0.35 then
                    castbar.successstart = nil
                    hide_castbar(castbar)
                end

            elseif castbar.fadeOut and castbar.fadeOut:IsPlaying() then
                -- Let animation finish; don't touch SetValue

            else
                -- Drive bar fill for all cast types (normal, channel, empowered)
                if castbar.justStarted then
                    castbar:SetValue(nowMs)
                    castbar.justStarted = false
                elseif Enum.StatusBarInterpolation then
                    castbar:SetValue(nowMs, Enum.StatusBarInterpolation.ExponentialEaseOut)
                else
                    castbar:SetValue(nowMs)
                end

                -- Time text
                if castbar.config.showTime ~= false and castbar.duration_obj then
                    local ok_r, remaining = pcall(castbar.duration_obj.GetRemainingDuration, castbar.duration_obj, 0)
                    local ok_t, total     = pcall(castbar.duration_obj.GetTotalDuration,     castbar.duration_obj, 0)
                    if ok_r and ok_t and remaining and total then
                        castbar.time:SetText(string.format("%.1f / %.1f", remaining, total))
                    end
                end

                -- Safety hide: skip for empowered and channeled casts.
                -- UnitChannelInfo can briefly return nil between ticks causing false hides.
                if castbar.casting and not castbar.isEmpowered and not castbar.isChannel then
                    if not UnitCastingInfo(castbar.unit) then
                        hide_castbar(castbar)
                    end
                end
            end
        end
    end
end

-- ============================================================
-- check_unit – called on target/focus change
-- ============================================================
local function check_unit(unit)
    local castbar = Castbar.frames[unit]
    if not castbar then return end

    castbar.casting      = false
    castbar.isEmpowered  = false
    castbar.failstart    = nil
    castbar.successstart = nil
    castbar.duration_obj = nil
    hide_empower_stages(castbar)
    castbar:Hide()
    castbar:SetAlpha(1)
    if castbar.fadeOut and castbar.fadeOut:IsPlaying() then castbar.fadeOut:Stop() end

    if not UnitExists(unit) then return end
    check_casting(castbar, "PLAYER_TARGET_CHANGED")
end

-- ============================================================
-- ToggleAnchors
-- ============================================================
function Castbar:ToggleAnchors()
    if InCombatLockdown() then return end
    for _, anchor in pairs(self.anchors) do
        if anchor:IsShown() then anchor:Hide() else anchor:Show() end
    end
end

-- ============================================================
-- Initialize
-- ============================================================
function Castbar:Initialize()
    if self._initialized then return end
    self._initialized = true

    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then
        LSM:Register("statusbar", "smooth_v2", "Interface\\AddOns\\nCastbar\\Media\\smooth_v2")
        LSM:Register("statusbar", "nCastbar Default", "Interface\\AddOns\\nCastbar\\Media\\statusbarTexture")
    end

    self:HideBlizzardCastbars()

    if not self.mainFrame then
        self.mainFrame = CreateFrame("Frame")
        self.mainFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
        self.mainFrame:RegisterEvent("PLAYER_FOCUS_CHANGED")
        self.mainFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        self.mainFrame:SetScript("OnEvent", function(_, event)
            if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
                check_unit("target")
            end
            if event == "PLAYER_FOCUS_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
                check_unit("focus")
            end
        end)
    end

    if self.ticker then self.ticker:Cancel() end

    C_Timer.After(0.5, function()
        for _, unit in ipairs({"player", "target", "focus", "pet"}) do
            Castbar:Create(unit)
        end
        check_unit("target")
        check_unit("focus")
        Castbar.ticker = C_Timer.NewTicker(0.02, OnTick)
    end)
end

function Castbar:HideBlizzardCastbars()
    if PlayerCastingBarFrame then
        PlayerCastingBarFrame:UnregisterAllEvents()
        PlayerCastingBarFrame:Hide()
        PlayerCastingBarFrame:SetAlpha(0)
    end
    if PetCastingBarFrame then
        PetCastingBarFrame:UnregisterAllEvents()
        PetCastingBarFrame:Hide()
        PetCastingBarFrame:SetAlpha(0)
    end
    if TargetFrameSpellBar then
        TargetFrameSpellBar:UnregisterAllEvents()
        TargetFrameSpellBar:Hide()
        TargetFrameSpellBar:SetAlpha(0)
        if TargetFrameSpellBar.ShowCastBar then TargetFrameSpellBar.ShowCastBar = nop end
    end
    if TargetFrame and TargetFrame.SpellBar then
        TargetFrame.SpellBar:UnregisterAllEvents()
        TargetFrame.SpellBar:Hide()
        TargetFrame.SpellBar:SetAlpha(0)
        if TargetFrame.SpellBar.ShowCastBar then TargetFrame.SpellBar.ShowCastBar = nop end
    end
    if FocusFrameSpellBar then
        FocusFrameSpellBar:UnregisterAllEvents()
        FocusFrameSpellBar:Hide()
        FocusFrameSpellBar:SetAlpha(0)
        if FocusFrameSpellBar.ShowCastBar then FocusFrameSpellBar.ShowCastBar = nop end
    end
    if FocusFrame and FocusFrame.SpellBar then
        FocusFrame.SpellBar:UnregisterAllEvents()
        FocusFrame.SpellBar:Hide()
        FocusFrame.SpellBar:SetAlpha(0)
        if FocusFrame.SpellBar.ShowCastBar then FocusFrame.SpellBar.ShowCastBar = nop end
    end
end

function Castbar:Cleanup()
    if self.ticker then self.ticker:Cancel(); self.ticker = nil end
    for _, frame in pairs(self.frames) do
        frame:UnregisterAllEvents()
        frame:SetScript("OnEvent", nil)
        frame:Hide()
    end
    for _, anchor in pairs(self.anchors) do anchor:Hide() end
end

function Castbar:Reload()
    self:Cleanup()
    self.frames       = {}
    self.anchors      = {}
    self._initialized = false  -- Guard zuruecksetzen damit Initialize() wieder laeuft
    self:Initialize()
end


