--[[
    Commander 3.0 - Watch System

    Live-updating queries that re-execute periodically and update in-place.
    Uses the existing Line.Live() infrastructure.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Watch Manager
-------------------------------------------------------------------------------

local WatchManager = {}
C3.WatchManager = WatchManager

-- State
WatchManager._watches = {}
WatchManager._nextId = 1
WatchManager._eventFrame = nil
WatchManager._pollingTicker = nil
WatchManager._lastEventTime = {}

-- Constraints
WatchManager.MIN_INTERVAL = 0.1   -- Allow 10 updates/second for real-time feel
WatchManager.MAX_INTERVAL = 60
WatchManager.DEFAULT_INTERVAL = 0.5  -- Faster default for better responsiveness

-- Event Mappings: query keyword -> WoW events
local EVENT_MAP = {
    ["$health"] = { "UNIT_HEALTH" },
    ["health"]  = { "UNIT_HEALTH" },
    ["$mana"]   = { "UNIT_POWER_UPDATE" },
    ["$power"]  = { "UNIT_POWER_UPDATE" },
    ["$energy"] = { "UNIT_POWER_UPDATE" },
    ["$rage"]   = { "UNIT_POWER_UPDATE" },
    ["$target"] = { "PLAYER_TARGET_CHANGED" },
    ["target"]  = { "PLAYER_TARGET_CHANGED" },
    ["$focus"]  = { "PLAYER_FOCUS_CHANGED" },
    ["focus"]   = { "PLAYER_FOCUS_CHANGED" },
    ["bags"]    = { "BAG_UPDATE_DELAYED" },
    ["currencies"] = { "CURRENCY_DISPLAY_UPDATE" },
    ["gold"]    = { "CURRENCY_DISPLAY_UPDATE", "PLAYER_MONEY" },
    ["$gold"]   = { "CURRENCY_DISPLAY_UPDATE", "PLAYER_MONEY" },
    ["group"]   = { "GROUP_ROSTER_UPDATE" },
    ["party"]   = { "GROUP_ROSTER_UPDATE" },
    ["raid"]    = { "GROUP_ROSTER_UPDATE" },
    ["$zone"]   = { "ZONE_CHANGED_NEW_AREA", "ZONE_CHANGED" },
    ["$subzone"] = { "ZONE_CHANGED_NEW_AREA", "ZONE_CHANGED" },
}

-------------------------------------------------------------------------------
-- Event System Initialization
-------------------------------------------------------------------------------

---Initialize event frame for event-driven watches
function WatchManager:_initEventFrame()
    if self._eventFrame then return end

    self._eventFrame = CreateFrame("Frame")
    self._eventFrame:SetScript("OnEvent", function(_, event, arg1)
        self:_onEvent(event, arg1)
    end)
end

---Handle WoW events and update relevant watches
function WatchManager:_onEvent(event, arg1)
    local now = GetTime()

    -- Debounce: skip if same event fired within 50ms
    if self._lastEventTime[event] and (now - self._lastEventTime[event] < 0.05) then
        return
    end
    self._lastEventTime[event] = now

    -- Find watches triggered by this event
    for _, watch in pairs(self._watches) do
        if watch.isEventDriven and watch.events then
            for _, e in ipairs(watch.events) do
                if e == event then
                    -- For UNIT_ events, check if it's for player
                    if event:sub(1, 5) == "UNIT_" and arg1 ~= "player" then
                        -- Skip non-player unit events
                    else
                        self:_updateWatch(watch)
                    end
                    break
                end
            end
        end
    end
end

---Update a single watch
function WatchManager:_updateWatch(watch)
    if not watch.line then return end

    local function normalizeResult(result)
        -- Materialize iterators so formatting doesn't print function: 0x...
        if type(result) == "function" or (getmetatable(result) and getmetatable(result).__call) then
            return C3.Pipeline.Collect(result)
        end
        return result
    end

    if watch._task and watch._task:IsRunning() then
        watch._pendingUpdate = true
        return
    end

    if not C3.Executor or not C3.Executor.Execute then
        local ok, result = pcall(function()
            return C3:Query(watch.query, watch.session)
        end)

        local content
        if not ok then
            content = C3.Formatter:Colorize("Error: " .. tostring(result), "FF4444")
        else
            result = normalizeResult(result)
            content = self:_formatResult(result, watch.query)
            -- Release pooled objects after formatting (watches re-execute frequently)
            if type(result) == "table" then
                C3.Types.ReleaseAll(result)
            end
        end

        watch.line:SetContent(content)
        watch.lastUpdate = GetTime()
        return
    end

    local self_ref = self
    local task = C3.Executor.Execute(watch.query, watch.session)
    watch._task = task

    local function finalize(content)
        if not self_ref._watches[watch.id] or not watch.line then
            return
        end

        watch.line:SetContent(content)
        watch.lastUpdate = GetTime()
        watch._task = nil

        if watch._pendingUpdate then
            watch._pendingUpdate = nil
            self_ref:_updateWatch(watch)
        end
    end

    task:OnComplete(function(_, result)
        result = normalizeResult(result)
        local content = self_ref:_formatResult(result, watch.query)
        -- Release pooled objects after formatting (watches re-execute frequently)
        if type(result) == "table" then
            C3.Types.ReleaseAll(result)
        end
        finalize(content)
    end)

    task:OnError(function(_, err)
        local content = C3.Formatter:Colorize("Error: " .. tostring(err), "FF4444")
        finalize(content)
    end)
end

---Polling tick for non-event-driven watches
function WatchManager:_onPollingTick()
    local now = GetTime()
    for _, watch in pairs(self._watches) do
        if not watch.isEventDriven then
            if (now - (watch.lastUpdate or 0)) >= watch.interval then
                self:_updateWatch(watch)
            end
        end
    end
end

---Ensure coalesced polling ticker is running
function WatchManager:_ensurePollingTicker()
    if self._pollingTicker then return end

    self._pollingTicker = C_Timer.NewTicker(0.5, function()
        self:_onPollingTick()
    end)
end

---Extract first keyword from query for event matching
function WatchManager:_extractKeyword(query)
    -- Handle variable watches like "$health"
    local var = query:match("^(%$%w+)")
    if var then return var end

    -- Handle command watches like "bags | where..."
    local cmd = query:match("^(%w+)")
    return cmd
end

-------------------------------------------------------------------------------
-- Watch Creation
-------------------------------------------------------------------------------

---Create a new watch
---@param query string The query to execute
---@param options? table { interval, session }
---@return number watchId
---@return table line The live BufferLine
function WatchManager:Create(query, options)
    options = options or {}

    -- Clamp interval
    local interval = tonumber(options.interval) or self.DEFAULT_INTERVAL
    interval = math.max(self.MIN_INTERVAL, math.min(self.MAX_INTERVAL, interval))

    local session = options.session
    local watchId = self._nextId
    self._nextId = self._nextId + 1

    -- Detect event dependencies
    local keyword = self:_extractKeyword(query)
    local events = EVENT_MAP[keyword]
    local isEventDriven = (events ~= nil)

    -- Initialize event frame if needed
    if isEventDriven then
        self:_initEventFrame()
        for _, event in ipairs(events) do
            self._eventFrame:RegisterEvent(event)
        end
    else
        -- Use coalesced polling for non-event-driven watches
        self:_ensurePollingTicker()
    end

    -- Create the update function (only used for initial display and line integration)
    local self_ref = self
    local updateFn = function(line)
        -- Return nil to prevent Line's internal timer; we manage updates externally
        return nil
    end

    -- Create line WITHOUT internal timer (interval = nil) for event-driven watches
    -- For polling watches, we also manage externally via coalesced ticker
    local line = C3.BufferLine.Live(updateFn, nil, {
        meta = {
            watchId = watchId,
            query = query,
        }
    })

    -- Store watch info
    local watch = {
        id = watchId,
        query = query,
        line = line,
        interval = interval,
        created = GetTime(),
        session = session,
        isEventDriven = isEventDriven,
        events = events,
        lastUpdate = 0,
    }
    self._watches[watchId] = watch

    -- Initial update
    self:_updateWatch(watch)

    C3:Debug("WatchManager: Created watch #" .. watchId ..
        (isEventDriven and " (event-driven)" or " (polling)") ..
        " for: " .. query)

    return watchId, line
end

-------------------------------------------------------------------------------
-- Watch Control
-------------------------------------------------------------------------------

---Stop a watch by ID
---@param watchId number
---@return boolean success
function WatchManager:Stop(watchId)
    local watch = self._watches[watchId]
    if not watch then
        return false
    end

    -- Stop the live line's timer
    if watch.line then
        watch.line:StopLive()
    end

    self._watches[watchId] = nil
    C3:Debug("WatchManager: Stopped watch #" .. watchId)

    return true
end

---Stop all watches
function WatchManager:StopAll()
    local count = 0
    for id in pairs(self._watches) do
        self:Stop(id)
        count = count + 1
    end
    C3:Debug("WatchManager: Stopped " .. count .. " watches")
    return count
end

---Get a watch by ID
---@param watchId number
---@return table|nil watch
function WatchManager:Get(watchId)
    return self._watches[watchId]
end

---List all active watches
---@return table[] watches
function WatchManager:List()
    local result = {}
    for _, watch in pairs(self._watches) do
        table.insert(result, watch)
    end
    -- Sort by ID
    table.sort(result, function(a, b) return a.id < b.id end)
    return result
end

---Get count of active watches
---@return number
function WatchManager:Count()
    local count = 0
    for _ in pairs(self._watches) do
        count = count + 1
    end
    return count
end

---Pause all watches (stop timers but keep watch state)
function WatchManager:PauseAll()
    for _, watch in pairs(self._watches) do
        if watch.line and watch.line._timer then
            watch.line._timer:Cancel()
            watch.line._timer = nil
            watch._paused = true
        end
    end
    C3:Debug("WatchManager: Paused all watches")
end

---Resume all paused watches
function WatchManager:ResumeAll()
    for _, watch in pairs(self._watches) do
        if watch._paused and watch.line then
            -- Restart the timer
            watch.line._timer = C_Timer.NewTicker(watch.interval, function()
                watch.line:Update()
            end)
            watch._paused = false
            -- Immediate update to refresh content
            watch.line:Update()
        end
    end
    C3:Debug("WatchManager: Resumed all watches")
end

-------------------------------------------------------------------------------
-- Result Formatting
-------------------------------------------------------------------------------

---Format a query result for display
---@param result any
---@param query string
---@return string
function WatchManager:_formatResult(result, query)
    -- Nil result - check if this could be due to WoW 12.0 secret values
    if result == nil then
        -- For health/mana/power variables, nil means data is restricted
        local keyword = self:_extractKeyword(query)
        local isSecretAffected = (keyword == "$health" or keyword == "health"
            or keyword == "$mana" or keyword == "$power"
            or keyword == "$energy" or keyword == "$rage")

        if isSecretAffected and C3.Secrets and C3.Secrets.hasSecretSystem then
            -- Data is restricted during combat - show indicator
            return C3.Formatter:Colorize("???", "666666")
        end
        return C3.Formatter:Colorize("(no result)", "666666")
    end

    -- Not a table - just stringify
    if type(result) ~= "table" then
        return tostring(result)
    end

    -- Error object
    if result._type == "Error" then
        return C3.Formatter:Format(result)
    end

    -- Array of objects
    if result[1] ~= nil then
        if #result == 0 then
            return C3.Formatter:Colorize("(empty)", "666666")
        end
        return C3.Formatter:FormatArray(result)
    end

    -- Single typed object
    if result._type then
        return C3.Formatter:Format(result)
    end

    -- Generic table
    return C3.Formatter:FormatTable(result)
end

-------------------------------------------------------------------------------
-- Watch Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("Watch", {
    description = "Active watch/live query",
    source = "watches",
    properties = {
        { "id", "Watch ID number" },
        { "query", "Query being watched" },
        { "interval", "Update interval in seconds" },
        { "age", "Time since watch started" },
    },
})

C3.Formatter:Register("Watch", {
    format = function(obj)
        local age = obj.age or 0
        local ageStr
        if age < 60 then
            ageStr = string.format("%.0fs", age)
        elseif age < 3600 then
            ageStr = string.format("%.1fm", age / 60)
        else
            ageStr = string.format("%.1fh", age / 3600)
        end

        return string.format("|cFF00FF00#%d|r [%.1fs] %s |cFF888888(%s)|r",
            obj.id,
            obj.interval,
            obj.query,
            ageStr
        )
    end,
})

-------------------------------------------------------------------------------
-- Cleanup on Session End
-------------------------------------------------------------------------------

-- Note: Session cleanup should call WatchManager:StopAll() when appropriate
-- This is typically done in the session manager or terminal window

return WatchManager
