--[[
    Commander 3.0 - Cast Progress System

    Tracks spell casting and shows progress feedback:
    - Progress bar in status bar during cast
    - "Cast complete: SpellName" message on success
    - "Cast interrupted: SpellName" message on failure
    - Logout/quit 20-second countdown timer
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Cast Progress Module
-------------------------------------------------------------------------------

local CastProgress = {}
C3.CastProgress = CastProgress

-- Constants
local PROGRESS_WIDTH = 20
local FILLED_CHAR = "█"
local EMPTY_CHAR = "░"
local UPDATE_INTERVAL = 0.05  -- 50ms
local LOGOUT_DURATION = 20    -- 20 seconds for logout/quit

-- Use shared utilities (lazy lookup handled by Colors module)
local Colorize = function(...) return C3.Colors.Colorize(...) end
local GetThemeColor = function(...) return C3.Colors.GetColor(...) end

---Strip WoW formatting codes to get visible text length
---@param text string
---@return string Plain text without color/texture codes
local function StripFormatting(text)
    if not text then return "" end
    return text
        :gsub("|c%x%x%x%x%x%x%x%x", "")
        :gsub("|r", "")
        :gsub("|T[^|]*|t", "")
end

---Count UTF-8 characters (not bytes) in a string
---Lua's # operator counts bytes, but Unicode chars like █ are 3 bytes each
---@param str string
---@return number Character count
local function Utf8Len(str)
    if not str then return 0 end
    local count = 0
    for _ in str:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
        count = count + 1
    end
    return count
end

---Calculate padding to center bar under the name portion of prefix
---@param prefix string Full prefix like "Summoning: Steady Flight"
---@param barText string The progress bar text (to measure width)
---@return number Padding spaces to prepend to bar
local function CalculateBarPadding(prefix, barText)
    if not prefix or prefix == "" then return 0 end

    local plainPrefix = StripFormatting(prefix)
    local barWidth = Utf8Len(barText)

    -- Find the colon separator to locate the name
    local colonPos = plainPrefix:find(": ")
    if not colonPos then
        -- No colon, center under whole prefix
        local prefixWidth = Utf8Len(plainPrefix)
        return math.max(0, math.floor((prefixWidth - barWidth) / 2))
    end

    -- Name starts after ": "
    local nameStart = colonPos + 2
    local nameWidth = Utf8Len(plainPrefix) - nameStart + 1
    local nameCenter = nameStart + math.floor(nameWidth / 2)

    -- Center the bar under the name
    -- barCenter is the 0-indexed offset to the center character
    -- Add 1 to shift the bar right to align visually
    local barCenter = math.floor(barWidth / 2)
    local padding = nameCenter - barCenter + 1

    return math.max(0, padding)
end

-- State
CastProgress._activeCast = nil      -- Current cast being tracked
CastProgress._ticker = nil          -- Update ticker
CastProgress._terminal = nil        -- Terminal reference for output
CastProgress._prefixLine = nil      -- Prefix line (e.g., "Summoning: Mount Name")
CastProgress._prefixLineId = nil    -- Prefix line ID
CastProgress._activeLine = nil      -- Progress bar line (centered under name)
CastProgress._activeLineId = nil    -- Line ID for triggering re-renders

-- Logout/Quit state
CastProgress._logoutActive = false
CastProgress._logoutStartTime = 0
CastProgress._logoutType = nil      -- "logout" or "quit"
CastProgress._logoutTicker = nil
CastProgress._logoutPopupSeen = false
CastProgress._logoutLastBlocks = 0  -- Track filled blocks to avoid unnecessary updates
CastProgress._logoutPrefixLine = nil  -- Prefix line (e.g., "Logging out...")
CastProgress._logoutPrefixLineId = nil
CastProgress._logoutLine = nil      -- Progress bar line (centered)
CastProgress._logoutLineId = nil    -- Line ID for re-renders

-------------------------------------------------------------------------------
-- Progress Bar Rendering
-------------------------------------------------------------------------------

---Build a progress bar string
---@param percent number 0-100
---@param color string Hex color (without |cff prefix)
---@return string
local function BuildProgressBar(percent, color)
    local filledBlocks = math.floor((percent / 100) * PROGRESS_WIDTH)
    filledBlocks = math.min(PROGRESS_WIDTH, math.max(0, filledBlocks))

    local bar = string.rep(FILLED_CHAR, filledBlocks) .. string.rep(EMPTY_CHAR, PROGRESS_WIDTH - filledBlocks)
    -- Pad percentage to 3 digits to prevent width changes (e.g., "  5%" vs "100%")
    local dimColor = GetThemeColor("status.dim") or "4C566A"
    return string.format("|cff%s[%s]|r |cff%s%3d%%|r", color, bar, dimColor, percent)
end

---Build a progress bar string with countdown timer (for logout/quit)
---@param percent number 0-100
---@param remaining number Seconds remaining
---@param color string Hex color (without |cff prefix)
---@return string
local function BuildLogoutBar(percent, remaining, color)
    local filledBlocks = math.floor((percent / 100) * PROGRESS_WIDTH)
    filledBlocks = math.min(PROGRESS_WIDTH, math.max(0, filledBlocks))

    local bar = string.rep(FILLED_CHAR, filledBlocks) .. string.rep(EMPTY_CHAR, PROGRESS_WIDTH - filledBlocks)
    -- Pad seconds to 2 digits to prevent width changes
    local dimColor = GetThemeColor("status.dim") or "4C566A"
    return string.format("|cff%s[%s]|r |cff%s%2ds|r", color, bar, dimColor, remaining)
end

-------------------------------------------------------------------------------
-- Cast Tracking
-------------------------------------------------------------------------------

---Start tracking a spell cast
---@param spellName string
---@param spellID number
---@param terminal table Terminal window reference
---@param completeMessage? string Custom completion message (optional)
---@param prefix? string Text to show before progress bar (e.g., "Summoning: Mount Name")
function CastProgress:StartTracking(spellName, spellID, terminal, completeMessage, prefix)
    -- Store references
    self._terminal = terminal

    -- We'll wait for UNIT_SPELLCAST_START to actually start the progress bar
    -- Just store what we're expecting
    self._pendingSpell = {
        name = spellName,
        id = spellID,
        completeMessage = completeMessage,  -- Custom message like "Switched to: Steady Flight"
        prefix = prefix,                    -- Text to show before progress bar
    }

    C3:Debug("CastProgress: Pending spell " .. spellName)
end

---Actually start the progress bar (called when UNIT_SPELLCAST_START fires)
---@param spellID number
---@param startTime number
---@param endTime number
---@param isChannel boolean Whether this is a channeled spell
function CastProgress:_startProgressBar(spellID, startTime, endTime, isChannel)
    local color = isChannel
        and (GetThemeColor("accent.special") or "B48EAD")   -- Nord purple for channels
        or (GetThemeColor("accent.primary") or "88C0D0")    -- Nord light blue for casts

    -- Get prefix from pending spell, convert "Casting:" to "Channeling:" for channels
    local prefix = self._pendingSpell and self._pendingSpell.prefix or ""
    if isChannel then
        prefix = prefix:gsub("Casting:", "Channeling:")
    end

    self._activeCast = {
        spellID = spellID,
        spellName = self._pendingSpell and self._pendingSpell.name or "Unknown",
        completeMessage = self._pendingSpell and self._pendingSpell.completeMessage,
        prefix = prefix,
        startTime = startTime,
        endTime = endTime,
        lastBlocks = isChannel and PROGRESS_WIDTH or 0,
        isChannel = isChannel,
        color = color,
    }

    if not self._terminal or not self._terminal.output then
        self:_startTicker()
        return
    end

    -- Line 1: Static prefix text
    if prefix ~= "" then
        self._prefixLine = C3.BufferLine.Text(prefix)
        self._terminal.output.buffer:AddLine(self._prefixLine)
        self._prefixLineId = self._prefixLine.id
    end

    -- Line 2: Progress bar (centered under name portion)
    local initialPercent = isChannel and 100 or 5
    local initialBar = BuildProgressBar(initialPercent, color)
    local barPlain = StripFormatting(initialBar)
    local padding = CalculateBarPadding(prefix, barPlain)

    self._activeLine = C3.BufferLine.Text(string.rep(" ", padding) .. initialBar)
    self._terminal.output.buffer:AddLine(self._activeLine)
    self._activeLineId = self._activeLine.id
    self._terminal.output:ScrollToBottom()

    self:_startTicker()
    C3:Debug("CastProgress: Started " .. (isChannel and "channel" or "cast") .. " for " .. self._activeCast.spellName)
end

---Start the update ticker
function CastProgress:_startTicker()
    if self._ticker then
        self._ticker:Cancel()
    end

    self._ticker = C_Timer.NewTicker(UPDATE_INTERVAL, function()
        self:_updateProgress()
    end)
end

---Stop the update ticker
function CastProgress:_stopTicker()
    if self._ticker then
        self._ticker:Cancel()
        self._ticker = nil
    end
end

---Update the progress bar
function CastProgress:_updateProgress()
    if not self._activeCast or not self._activeLine then
        self:_stopTicker()
        return
    end

    local now = GetTime() * 1000
    local total = self._activeCast.endTime - self._activeCast.startTime
    local elapsed = now - self._activeCast.startTime

    if total <= 0 then
        self:_stopTicker()
        return
    end

    local realPercent = math.min(100, (elapsed / total) * 100)
    local displayPercent, filledBlocks

    if self._activeCast.isChannel then
        -- Channels drain from 100% → 0%
        -- Offset by -5% so bar empties before channel completes
        displayPercent = math.max(0, 100 - realPercent - 5)
        filledBlocks = math.ceil(displayPercent / 5)
        filledBlocks = math.min(PROGRESS_WIDTH, math.max(0, filledBlocks))
    else
        -- Regular casts fill from 0% → 100%
        -- Offset by +5% so bar fills before cast completes
        displayPercent = math.min(100, realPercent + 5)
        filledBlocks = math.floor(displayPercent / 5)
        filledBlocks = math.min(PROGRESS_WIDTH, math.max(1, filledBlocks))
    end

    -- Only update when blocks change (reduces flickering)
    if filledBlocks == self._activeCast.lastBlocks then
        return
    end
    self._activeCast.lastBlocks = filledBlocks

    -- Update line content (centered under name)
    local pct = filledBlocks * 5
    local bar = BuildProgressBar(pct, self._activeCast.color)
    local barPlain = StripFormatting(bar)
    local prefix = self._activeCast.prefix or ""
    local padding = CalculateBarPadding(prefix, barPlain)
    self._activeLine:SetContent(string.rep(" ", padding) .. bar)

    -- Trigger renderer to update the line visually
    if self._terminal and self._terminal.output and self._terminal.output.renderer then
        self._terminal.output.renderer:UpdateLine(self._activeLineId)
    end

    -- Check if cast should be complete
    if now >= self._activeCast.endTime then
        self:_stopTicker()
    end
end

---Handle cast completion
---@param spellID number
function CastProgress:_onCastComplete(spellID)
    local spellName = self._activeCast and self._activeCast.spellName or "Unknown"
    local isChannel = self._activeCast and self._activeCast.isChannel
    -- Ignore customMessage for channels - secure.lua doesn't know it's a channel when created
    local customMessage = not isChannel and self._activeCast and self._activeCast.completeMessage

    self:_stopTicker()

    -- Remove the prefix line (we'll collapse to single completion message)
    if self._prefixLine and self._terminal and self._terminal.output then
        self._terminal.output.buffer:RemoveLine(self._prefixLineId)
        self._prefixLine = nil
        self._prefixLineId = nil
    end

    -- Update the bar line to show completion message
    if self._activeLine then
        local verb = isChannel and "Channeled:" or "Cast:"
        local nameColor = isChannel and "accent.special" or "accent.primary"
        local message = customMessage or (Colorize(verb, "accent.success") .. " " .. Colorize(spellName, nameColor))
        self._activeLine:SetContent(message)

        if self._terminal and self._terminal.output and self._terminal.output.renderer then
            self._terminal.output.renderer:UpdateLine(self._activeLineId)
            self._terminal.output.renderer:Render()
        end

        self._activeLine = nil
        self._activeLineId = nil
    end

    self._activeCast = nil
    self._pendingSpell = nil

    C3:Debug("CastProgress: Completed " .. (isChannel and "channel" or "cast") .. " " .. spellName)
end

---Handle cast interruption/failure
---@param spellID number
---@param reason string
function CastProgress:_onCastFailed(spellID, reason)
    local spellName = self._activeCast and self._activeCast.spellName
        or (self._pendingSpell and self._pendingSpell.name)
        or "Unknown"

    self:_stopTicker()

    -- Remove the prefix line (we'll collapse to single failure message)
    if self._prefixLine and self._terminal and self._terminal.output then
        self._terminal.output.buffer:RemoveLine(self._prefixLineId)
        self._prefixLine = nil
        self._prefixLineId = nil
    end

    -- Update the bar line to show failure message
    if self._activeLine then
        local msg = reason == "interrupted" and "Interrupted:" or "Failed:"
        self._activeLine:SetContent(
            Colorize(msg, "accent.error") .. " " .. Colorize(spellName, "accent.primary")
        )

        -- Notify renderer of line update, then full render since we removed a line
        if self._terminal and self._terminal.output and self._terminal.output.renderer then
            self._terminal.output.renderer:UpdateLine(self._activeLineId)
            self._terminal.output.renderer:Render()
        end

        self._activeLine = nil
        self._activeLineId = nil
    end

    -- Clear state
    self._activeCast = nil
    self._pendingSpell = nil

    C3:Debug("CastProgress: Failed " .. spellName .. " (" .. reason .. ")")
end

---Handle instant cast (no progress bar, just completion message)
---@param spellName string
function CastProgress:_onInstantCast(spellName)
    local customMessage = self._pendingSpell and self._pendingSpell.completeMessage

    if self._terminal and self._terminal.output then
        local message = customMessage or (Colorize("Cast:", "accent.success") .. " " .. Colorize(spellName, "accent.primary"))
        local line = C3.BufferLine.Text(message)
        self._terminal.output.buffer:AddLine(line)
        self._terminal.output:ScrollToBottom()
    end

    self._pendingSpell = nil
    C3:Debug("CastProgress: Instant cast " .. spellName)
end

-------------------------------------------------------------------------------
-- Logout/Quit Progress
-------------------------------------------------------------------------------

---Start logout/quit countdown timer
---@param logoutType string "logout" or "quit"
---@param terminal table Terminal reference
function CastProgress:StartLogoutProgress(logoutType, terminal)
    -- Store references
    self._terminal = terminal

    self._logoutActive = true
    self._logoutStartTime = GetTime()
    self._logoutType = logoutType
    self._logoutPopupSeen = false
    self._logoutLastBlocks = 0

    -- Store prefix for centering calculation
    self._logoutPrefix = Colorize(logoutType == "quit" and "Quitting..." or "Logging out...", "status.dim")

    -- Create two lines: prefix line (static) + bar line (centered)
    if self._terminal and self._terminal.output then
        -- Line 1: Prefix text
        self._logoutPrefixLine = C3.BufferLine.Text(self._logoutPrefix)
        self._terminal.output.buffer:AddLine(self._logoutPrefixLine)
        self._logoutPrefixLineId = self._logoutPrefixLine.id

        -- Line 2: Progress bar (centered under prefix)
        local logoutColor = GetThemeColor("accent.warning") or "EBCB8B"
        local initialBar = BuildLogoutBar(0, LOGOUT_DURATION, logoutColor)
        local barPlain = StripFormatting(initialBar)
        local padding = CalculateBarPadding(self._logoutPrefix, barPlain)

        self._logoutLine = C3.BufferLine.Text(string.rep(" ", padding) .. initialBar)
        self._terminal.output.buffer:AddLine(self._logoutLine)
        self._logoutLineId = self._logoutLine.id
        self._terminal.output:ScrollToBottom()
    end

    -- Start update ticker
    if self._logoutTicker then
        self._logoutTicker:Cancel()
    end
    self._logoutTicker = C_Timer.NewTicker(0.1, function()
        self:_updateLogoutProgress()
    end)

    C3:Debug("CastProgress: Started " .. logoutType .. " countdown")
end

---Update logout progress bar
function CastProgress:_updateLogoutProgress()
    if not self._logoutActive or not self._logoutLine then
        self:_stopLogoutProgress()
        return
    end

    -- Check for WoW's logout/quit confirmation popup
    local campPopup = StaticPopup_Visible("CAMP")
    local quitPopup = StaticPopup_Visible("QUIT")
    local popupVisible = campPopup or quitPopup

    if popupVisible then
        self._logoutPopupSeen = true
    elseif self._logoutPopupSeen then
        -- User dismissed the popup - cancel
        local msg = self._logoutType == "quit" and "Quit cancelled." or "Logout cancelled."

        -- Remove prefix line
        if self._logoutPrefixLine and self._terminal and self._terminal.output then
            self._terminal.output.buffer:RemoveLine(self._logoutPrefixLineId)
            self._logoutPrefixLine = nil
            self._logoutPrefixLineId = nil
        end

        -- Update the bar line to show cancellation message
        self._logoutLine:SetContent(Colorize(msg, "accent.error"))

        -- Full render since we removed a line
        if self._terminal and self._terminal.output and self._terminal.output.renderer then
            self._terminal.output.renderer:Render()
        end

        self:_stopLogoutProgress()
        return
    end

    local elapsed = GetTime() - self._logoutStartTime
    local percent = math.min(100, (elapsed / LOGOUT_DURATION) * 100)
    local remaining = math.max(0, math.ceil(LOGOUT_DURATION - elapsed))

    -- Calculate filled blocks (same logic as BuildLogoutBar)
    local filledBlocks = math.floor((percent / 100) * PROGRESS_WIDTH)
    filledBlocks = math.min(PROGRESS_WIDTH, math.max(0, filledBlocks))

    -- Only update UI when blocks change or remaining seconds change
    local lastRemaining = self._logoutLastRemaining or (LOGOUT_DURATION + 1)
    if filledBlocks ~= self._logoutLastBlocks or remaining ~= lastRemaining then
        self._logoutLastBlocks = filledBlocks
        self._logoutLastRemaining = remaining

        -- Update bar line content (centered under prefix)
        local logoutColor = GetThemeColor("accent.warning") or "EBCB8B"
        local bar = BuildLogoutBar(percent, remaining, logoutColor)
        local barPlain = StripFormatting(bar)
        local padding = CalculateBarPadding(self._logoutPrefix, barPlain)
        self._logoutLine:SetContent(string.rep(" ", padding) .. bar)

        -- Trigger renderer to update the line visually
        if self._terminal and self._terminal.output and self._terminal.output.renderer then
            self._terminal.output.renderer:UpdateLine(self._logoutLineId)
        end
    end

    if elapsed >= LOGOUT_DURATION then
        self:_stopLogoutProgress()
    end
end

---Stop logout progress
function CastProgress:_stopLogoutProgress()
    if self._logoutTicker then
        self._logoutTicker:Cancel()
        self._logoutTicker = nil
    end

    -- Clear line references (line content already updated by caller if needed)
    self._logoutPrefixLine = nil
    self._logoutPrefixLineId = nil
    self._logoutLine = nil
    self._logoutLineId = nil

    self._logoutActive = false
    self._logoutType = nil
    self._logoutPopupSeen = false
    self._logoutLastBlocks = 0
    self._logoutLastRemaining = nil
    self._logoutPrefix = nil
end

---Check if logout progress is active
---@return boolean
function CastProgress:IsLogoutActive()
    return self._logoutActive
end

-------------------------------------------------------------------------------
-- Event Handling
-------------------------------------------------------------------------------

function CastProgress:Init()
    local eventFrame = CreateFrame("Frame")

    eventFrame:RegisterEvent("UNIT_SPELLCAST_START")
    eventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
    eventFrame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
    eventFrame:RegisterEvent("UNIT_SPELLCAST_FAILED")
    -- Note: UNIT_SPELLCAST_STOP not registered - it fires on ALL endings including success
    eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
    eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")

    -- Helper: get cast/channel timing info (handles Secrets API for WoW 12.0+)
    local function getCastTiming(isChannel)
        local fn = isChannel
            and (C3.Secrets and C3.Secrets.UnitChannelInfo or UnitChannelInfo)
            or (C3.Secrets and C3.Secrets.UnitCastingInfo or UnitCastingInfo)
        local _, _, _, startTimeMS, endTimeMS = fn("player")
        return startTimeMS, endTimeMS
    end

    -- Helper: check if player is currently casting or channeling
    local function isStillCasting()
        local castName = C3.Secrets and C3.Secrets.UnitCastingInfo("player") or UnitCastingInfo("player")
        if castName then return true end
        local channelName = C3.Secrets and C3.Secrets.UnitChannelInfo("player") or UnitChannelInfo("player")
        return channelName ~= nil
    end

    eventFrame:SetScript("OnEvent", function(_, event, unit, castGUID, spellID)
        if unit ~= "player" then return end
        if not self._pendingSpell and not self._activeCast then return end

        -- Cast or channel started
        if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
            if not self._pendingSpell or self._activeCast then return end

            local isChannel = event == "UNIT_SPELLCAST_CHANNEL_START"
            local startTimeMS, endTimeMS = getCastTiming(isChannel)
            if not startTimeMS or not endTimeMS then return end

            self:_startProgressBar(spellID, startTimeMS, endTimeMS, isChannel)
            return
        end

        -- Cast succeeded (channels complete via CHANNEL_STOP, not here)
        if event == "UNIT_SPELLCAST_SUCCEEDED" then
            if self._activeCast then
                if self._activeCast.isChannel then return end
                if spellID ~= self._activeCast.spellID and isStillCasting() then return end
                self:_onCastComplete(spellID)
                return
            end

            if not self._pendingSpell then return end
            local pendingName = self._pendingSpell.name
            C_Timer.After(0, function()
                if self._activeCast then return end
                if not self._pendingSpell then return end
                self:_onInstantCast(pendingName)
            end)
            return
        end

        -- Cast interrupted
        if event == "UNIT_SPELLCAST_INTERRUPTED" then
            if self._activeCast then
                self:_onCastFailed(spellID, "interrupted")
            else
                self._pendingSpell = nil
            end
            return
        end

        -- Cast failed (UNIT_SPELLCAST_STOP fires on ALL endings, so we use FAILED specifically)
        if event == "UNIT_SPELLCAST_FAILED" then
            if self._activeCast then
                self:_onCastFailed(spellID, "failed")
            else
                self._pendingSpell = nil
            end
            return
        end

        -- Channel stopped
        if event == "UNIT_SPELLCAST_CHANNEL_STOP" and self._activeCast then
            self:_onCastComplete(spellID)
        end
    end)

    C3:Debug("CastProgress: Initialized")
end

-------------------------------------------------------------------------------
-- Initialize on Load
-------------------------------------------------------------------------------

C3:OnInit(function()
    CastProgress:Init()
end)

return CastProgress
