-- ============================================================
-- FILE: Console_UI.lua
-- MODULE: MidnightUI_Console_UI  (_G.MidnightUI_Console_UI, local M)
--
-- PURPOSE:
--   Phase 2a of the Diagnostics Console rewrite. Builds the
--   Console's main window shell: dark-navy backdrop, cyan
--   accent bar, header with title / subtitle / live stats /
--   close button, and a horizontal tab strip with active-tab
--   indicator. Each tab body is currently a centered placeholder
--   card; content for individual tabs (Overview, Stream, Archive,
--   Memory, Reports, Settings) lands in later phases.
--
-- HOW IT WORKS:
--   The window is created lazily on first Open() so it costs
--   nothing until the player actually opens it. Construction
--   builds the header strip (fixed height), the tab strip
--   (fixed height, below the header), and one body frame per
--   tab that fills the remaining area. SetActiveTab() just
--   shows the selected body and hides the rest, and moves the
--   accent underline under the active tab button.
--
--   Combat deferral: Open() during combat lockdown stashes a
--   flag and the window opens automatically on PLAYER_REGEN_ENABLED.
--   If the window is already open when combat starts, it stays
--   open — the gate only applies to opening, not to staying.
--
-- DEPENDS ON:
--   MidnightUI_Console (engine) — read-only, for GetStats() used
--     by the header's live stats line. The UI works even if the
--     engine fails to load (shows "engine offline").
--
-- EXPORTS:
--   _G.MidnightUI_Console_UI.Open()
--   _G.MidnightUI_Console_UI.Close()
--   _G.MidnightUI_Console_UI.Toggle()
--   _G.MidnightUI_Console_UI.IsShown()
--   _G.MidnightUI_Console_UI.SetActiveTab(id)
--   _G.MidnightUI_Console_UI.RefreshStats()
--
-- OWNED SAVEDVARIABLES: none (Phase 2a)
--
-- EVENTS REGISTERED:
--   PLAYER_REGEN_ENABLED — drains the combat-deferred open flag.
--
-- FRAMES CREATED:
--   MidnightUI_Console_Frame — the main window (lazy, singleton).
--   Subframes: header, tab strip, one body per tab.
--
-- GOTCHAS:
--   - The window is created on demand inside BuildWindow(). Do
--     not touch `frame` before that — use M.Open() which builds
--     it if needed.
--   - UISpecialFrames registration lets ESC close the window.
--     Closing via ESC goes through the same Hide() path as the
--     close button, so no extra wiring needed.
--   - Palette values are chosen to match Welcome.lua and
--     Settings_UI.lua exactly, so the Console feels like a
--     sibling of the existing MidnightUI surfaces, not an alien.
-- ============================================================

local _G = _G
local CreateFrame = CreateFrame
local UIParent = _G.UIParent
local InCombatLockdown = _G.InCombatLockdown
local table = _G.table
local string = _G.string
local ipairs = _G.ipairs
local pairs = _G.pairs
local Mixin = _G.Mixin
local BackdropTemplateMixin = _G.BackdropTemplateMixin

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

-- ============================================================
-- SECTION: Palette + Layout Constants
-- Matches Welcome.lua and Settings_UI.lua so the Console shares
-- the existing MidnightUI visual vocabulary.
-- ============================================================
local UI = {
    bg        = {0.05, 0.06, 0.11},   -- deep navy, darkest layer
    panel     = {0.075, 0.09, 0.15},  -- one shade up
    inset     = {0.08, 0.10, 0.17},   -- one shade up again
    card      = {0.10, 0.13, 0.20},   -- cards sit on panels
    border    = {0.18, 0.22, 0.30},   -- dusty slate
    text      = {0.92, 0.93, 0.96},   -- primary
    muted     = {0.58, 0.60, 0.65},   -- secondary
    accent    = {0.00, 0.78, 1.00},   -- MidnightUI cyan
    accentDim = {0.00, 0.45, 0.62},
    rowHover  = {0.13, 0.17, 0.27},
}

local LAYOUT = {
    width       = 1280,
    height      = 780,
    headerH     = 72,
    tabStripH   = 44,
    padX        = 20,
    padY        = 16,
    accentBarH  = 3,
    tabW        = 118,
    tabGap      = 4,
    indicatorH  = 3,
}

local TABS = {
    { id = "overview", label = "Overview" },
    { id = "stream",   label = "Stream"   },
    { id = "archive",  label = "Archive"  },
    { id = "memory",   label = "Memory"   },
    { id = "reports",  label = "Reports"  },
    { id = "settings", label = "Settings" },
}

-- ============================================================
-- SECTION: Typography
-- Uses MidnightUI's font resolver which prefers the same font
-- object as Messenger's chat frame (ChatFontNormal). Falls back
-- gracefully through GameFontNormal and finally ARIALN.TTF.
-- A single resolved font path is used for every widget so the
-- Console reads as a single typographic family.
-- ============================================================
local FONT_PATH = "Fonts\\ARIALN.TTF"
do
    local resolver = _G.MidnightUI_ResolveFontPath
    local resolved = resolver and resolver() or nil
    if type(resolved) == "string" and resolved ~= "" then
        FONT_PATH = resolved
    end
end

local FONT = {
    huge       = 34,   -- hero duration
    display    = 22,   -- big titles
    heading    = 17,   -- card titles
    body       = 13,   -- default body text
    small      = 12,   -- subtitles, descriptions
    tiny       = 11,   -- tag labels
}

local function NewFontString(parent, layer, size, color, flags)
    local fs = parent:CreateFontString(nil, layer or "OVERLAY")
    fs:SetFont(FONT_PATH, size or FONT.body, flags or "")
    if color then
        fs:SetTextColor(color[1], color[2], color[3])
    end
    return fs
end

function M.FormatClassTag(classTag)
    if not classTag or classTag == "" then return "" end
    local tag = tostring(classTag)
    local pretty = _G.LOCALIZED_CLASS_NAMES_MALE and _G.LOCALIZED_CLASS_NAMES_MALE[tag]
    if not pretty or pretty == "" then
        pretty = tag:sub(1, 1):upper() .. tag:sub(2):lower()
    end
    local core = _G.MidnightUI_Core
    if core and core.GetClassColor then
        local r, g, b = core.GetClassColor(tag)
        if r then
            return string.format("|cff%02x%02x%02x%s|r",
                math.floor(r * 255 + 0.5),
                math.floor(g * 255 + 0.5),
                math.floor(b * 255 + 0.5),
                pretty)
        end
    end
    return pretty
end

-- ============================================================
-- SECTION: State
-- ============================================================
local frame                      -- main window, built lazily
local tabButtons = {}            -- id -> Button
local tabBodies  = {}            -- id -> Frame
local activeTabId = "overview"
local deferredOpen = false

-- Tab populator registry. Tab-content files register a pair of
-- callbacks: build(body) runs once when the body frame is first
-- created, refresh() runs every time the window opens or the tab
-- is activated. If no populator is registered for a tab, the
-- default placeholder card is used instead.
M.tabPopulators = M.tabPopulators or {}

-- ============================================================
-- SECTION: Style Helpers
-- ============================================================
local WHITE_TEX = "Interface\\Buttons\\WHITE8X8"

local function SolidTexture(parent, layer, color, alpha)
    local tex = parent:CreateTexture(nil, layer or "BACKGROUND")
    tex:SetTexture(WHITE_TEX)
    tex:SetVertexColor(color[1], color[2], color[3], alpha or 1)
    return tex
end

local function EnsureBackdropCapable(f)
    if f.SetBackdrop then return end
    if BackdropTemplateMixin and Mixin then
        Mixin(f, BackdropTemplateMixin)
        if f.OnBackdropLoaded then f:OnBackdropLoaded() end
    end
end

local function ApplyBackdrop(f, bg, border, bgAlpha, borderAlpha)
    EnsureBackdropCapable(f)
    if not f.SetBackdrop then return end
    f:SetBackdrop({
        bgFile   = WHITE_TEX,
        edgeFile = WHITE_TEX,
        edgeSize = 1,
        insets   = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    f:SetBackdropColor(bg[1], bg[2], bg[3], bgAlpha or 0.98)
    if border then
        f:SetBackdropBorderColor(border[1], border[2], border[3], borderAlpha or 1)
    end
end

-- ============================================================
-- SECTION: Header Construction
-- Title, subtitle, live stats line on the right, close button.
-- A thin cyan accent bar sits along the very top edge.
-- ============================================================
local function BuildHeader(parent)
    local header = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    header:SetHeight(LAYOUT.headerH)
    header:SetPoint("TOPLEFT",  parent, "TOPLEFT",  1, -1)
    header:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -1, -1)

    local bg = SolidTexture(header, "BACKGROUND", UI.panel, 1)
    bg:SetAllPoints()

    -- Cyan accent bar along the very top edge
    local accent = SolidTexture(header, "ARTWORK", UI.accent, 1)
    accent:SetPoint("TOPLEFT",  header, "TOPLEFT",  0, 0)
    accent:SetPoint("TOPRIGHT", header, "TOPRIGHT", 0, 0)
    accent:SetHeight(LAYOUT.accentBarH)

    local title = NewFontString(header, "OVERLAY", FONT.display, UI.text)
    title:SetPoint("LEFT", header, "LEFT", LAYOUT.padX, 0)
    title:SetText("MidnightUI Diagnostics Console")

    -- Live stats on the right side
    local stats = NewFontString(header, "OVERLAY", FONT.small, UI.text)
    stats:SetPoint("RIGHT", header, "RIGHT", -56, 0)
    stats:SetJustifyH("RIGHT")
    stats:SetText("")
    parent.statsLabel = stats

    -- Close button (top-right)
    local close = CreateFrame("Button", nil, header, "BackdropTemplate")
    close:SetSize(28, 28)
    close:SetPoint("TOPRIGHT", header, "TOPRIGHT", -12, -8)
    ApplyBackdrop(close, UI.inset, UI.border, 1, 1)
    local closeLabel = NewFontString(close, "OVERLAY", FONT.body, UI.text)
    closeLabel:SetPoint("CENTER")
    closeLabel:SetText("X")
    close:SetScript("OnClick", function() M.Close() end)
    close:SetScript("OnEnter", function(self)
        self:SetBackdropColor(UI.rowHover[1], UI.rowHover[2], UI.rowHover[3], 1)
        closeLabel:SetTextColor(UI.accent[1], UI.accent[2], UI.accent[3])
    end)
    close:SetScript("OnLeave", function(self)
        self:SetBackdropColor(UI.inset[1], UI.inset[2], UI.inset[3], 1)
        closeLabel:SetTextColor(UI.text[1], UI.text[2], UI.text[3])
    end)

    return header
end

-- ============================================================
-- SECTION: Tab Strip Construction
-- Horizontal tab bar with muted idle labels, hover brightens,
-- active tab gets cyan label + cyan underline indicator.
-- ============================================================
local function BuildTabStrip(parent)
    local strip = CreateFrame("Frame", nil, parent)
    strip:SetHeight(LAYOUT.tabStripH)
    strip:SetPoint("TOPLEFT",  parent, "TOPLEFT",  1, -(LAYOUT.headerH + 1))
    strip:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -1, -(LAYOUT.headerH + 1))

    local bg = SolidTexture(strip, "BACKGROUND", UI.inset, 1)
    bg:SetAllPoints()

    -- Hairline separator at the bottom of the strip
    local hairline = SolidTexture(strip, "OVERLAY", UI.border, 0.8)
    hairline:SetHeight(1)
    hairline:SetPoint("BOTTOMLEFT",  strip, "BOTTOMLEFT",  0, 0)
    hairline:SetPoint("BOTTOMRIGHT", strip, "BOTTOMRIGHT", 0, 0)

    -- Cyan indicator that slides under the active tab
    local indicator = SolidTexture(strip, "OVERLAY", UI.accent, 1)
    indicator:SetHeight(LAYOUT.indicatorH)
    parent.tabIndicator = indicator

    local xCursor = LAYOUT.padX
    for _, t in ipairs(TABS) do
        local btn = CreateFrame("Button", nil, strip)
        btn:SetSize(LAYOUT.tabW, LAYOUT.tabStripH)
        btn:SetPoint("LEFT", strip, "LEFT", xCursor, 0)
        btn.id = t.id

        local label = NewFontString(btn, "OVERLAY", FONT.heading, UI.muted)
        label:SetPoint("CENTER", btn, "CENTER", 0, 0)
        label:SetText(t.label)
        btn.label = label

        btn:SetScript("OnEnter", function(self)
            if activeTabId ~= self.id then
                self.label:SetTextColor(UI.text[1], UI.text[2], UI.text[3])
            end
        end)
        btn:SetScript("OnLeave", function(self)
            if activeTabId ~= self.id then
                self.label:SetTextColor(UI.muted[1], UI.muted[2], UI.muted[3])
            end
        end)
        btn:SetScript("OnClick", function(self)
            M.SetActiveTab(self.id)
        end)

        tabButtons[t.id] = btn
        xCursor = xCursor + LAYOUT.tabW + LAYOUT.tabGap
    end

    return strip
end

-- ============================================================
-- SECTION: Tab Body Construction
-- Each tab's body is a full-size frame below the tab strip.
-- Phase 2a: centered placeholder card per tab so the shell is
-- visually complete and you can see all tab transitions working.
-- ============================================================
local function DefaultPlaceholder(body, tabDef)
    local card = CreateFrame("Frame", nil, body, "BackdropTemplate")
    card:SetSize(560, 200)
    card:SetPoint("CENTER", body, "CENTER", 0, 0)
    ApplyBackdrop(card, UI.card, UI.border, 1, 1)

    local topEdge = SolidTexture(card, "OVERLAY", UI.accent, 0.85)
    topEdge:SetHeight(2)
    topEdge:SetPoint("TOPLEFT",  card, "TOPLEFT",  1, -1)
    topEdge:SetPoint("TOPRIGHT", card, "TOPRIGHT", -1, -1)

    local title = NewFontString(card, "OVERLAY", FONT.display, UI.accent)
    title:SetPoint("TOP", card, "TOP", 0, -36)
    title:SetText(tabDef.label)

    local body1 = NewFontString(card, "OVERLAY", FONT.heading, UI.text)
    body1:SetPoint("CENTER", card, "CENTER", 0, 0)
    body1:SetText("Tab Scaffolding Ready")
end

local function BuildTabBody(parent, tabDef, bodyTopOffset)
    local body = CreateFrame("Frame", nil, parent)
    body:SetPoint("TOPLEFT",     parent, "TOPLEFT",     1, bodyTopOffset)
    body:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -1, 1)
    body:Hide()

    -- Faint backing tint so the body area reads as a distinct surface
    local tint = SolidTexture(body, "BACKGROUND", UI.bg, 1)
    tint:SetAllPoints()

    tabBodies[tabDef.id] = body

    -- Refresh runs every time this body becomes visible. Belt-and-
    -- braces with the explicit refresh calls in SetActiveTab and
    -- DoOpen — guarantees content renders on the first open even
    -- if the window was still mid-construction when those earlier
    -- refresh calls fired.
    body:SetScript("OnShow", function(self)
        local pop = M.tabPopulators[tabDef.id]
        if pop and pop.refresh then
            pcall(pop.refresh)
        end
    end)

    -- Let a registered populator fill the body if one exists;
    -- otherwise fall back to the default "scaffolding ready" card.
    -- Wrapped in pcall so one broken tab cannot break the whole
    -- window build loop — if build fails we fall back to the
    -- placeholder card so the window is still usable.
    local pop = M.tabPopulators[tabDef.id]
    if pop and pop.build then
        local ok, err = pcall(pop.build, body)
        if not ok then
            DefaultPlaceholder(body, tabDef)
            if _G.MidnightUI_Debug then
                _G.MidnightUI_Debug(string.format(
                    "[MUI Console] tab '%s' build failed: %s",
                    tostring(tabDef.id), tostring(err)))
            end
        end
    else
        DefaultPlaceholder(body, tabDef)
    end

    return body
end

-- ============================================================
-- SECTION: Main Window Assembly
-- ============================================================
local function BuildWindow()
    frame = CreateFrame("Frame", "MidnightUI_Console_Frame", UIParent, "BackdropTemplate")
    frame:SetSize(LAYOUT.width, LAYOUT.height)
    frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    frame:SetFrameStrata("FULLSCREEN_DIALOG")
    frame:SetFrameLevel(500)
    frame:SetToplevel(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:SetClampedToScreen(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop",  frame.StopMovingOrSizing)
    ApplyBackdrop(frame, UI.bg, UI.border, 0.98, 1)

    -- ESC closes the window
    if type(_G.UISpecialFrames) == "table" then
        table.insert(_G.UISpecialFrames, "MidnightUI_Console_Frame")
    end

    BuildHeader(frame)
    BuildTabStrip(frame)

    local bodyTopOffset = -(LAYOUT.headerH + LAYOUT.tabStripH + 2)
    for _, t in ipairs(TABS) do
        BuildTabBody(frame, t, bodyTopOffset)
    end

    M.SetActiveTab(activeTabId)
    frame:Hide()
end

-- ============================================================
-- SECTION: Public API
-- ============================================================
function M.SetActiveTab(id)
    if not tabBodies[id] then return end
    activeTabId = id
    for tid, body in pairs(tabBodies) do
        if tid == id then body:Show() else body:Hide() end
    end
    for tid, btn in pairs(tabButtons) do
        if tid == id then
            btn.label:SetTextColor(UI.accent[1], UI.accent[2], UI.accent[3])
        else
            btn.label:SetTextColor(UI.muted[1], UI.muted[2], UI.muted[3])
        end
    end
    local ind = frame and frame.tabIndicator
    local btn = tabButtons[id]
    if ind and btn then
        ind:ClearAllPoints()
        ind:SetPoint("BOTTOMLEFT",  btn, "BOTTOMLEFT",  0, 0)
        ind:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0)
    end
    local pop = M.tabPopulators[id]
    if pop and pop.refresh then
        pcall(pop.refresh)
    end
end

-- Public registration entry point used by per-tab content files.
-- build(body)   — runs once when the body frame is constructed.
-- refresh()     — runs every time the window opens or this tab is
--                 activated. Safe to be a no-op.
function M.RegisterTabContent(id, buildFn, refreshFn)
    M.tabPopulators[id] = { build = buildFn, refresh = refreshFn }
    if tabBodies[id] and buildFn then
        -- Tab body already constructed — populate it retroactively
        pcall(buildFn, tabBodies[id])
    end
end

-- Expose palette, layout, typography, and style helpers so per-tab
-- files can match the Console's visual language without
-- reimplementing it.
M.UI             = UI
M.LAYOUT         = LAYOUT
M.FONT           = FONT
M.FONT_PATH      = FONT_PATH
M.SolidTexture   = SolidTexture
M.ApplyBackdrop  = ApplyBackdrop
M.NewFontString  = NewFontString

-- ============================================================
-- SECTION: Shared Copy Dialog
-- A reusable popup that shows text in a pre-selected EditBox
-- ready for Ctrl+C. Any tab can call M.ShowCopyDialog(text, title)
-- to produce a copy surface without having to build its own.
-- Singleton - created on first use, reused after.
-- ============================================================
local _copyDialog

local function BuildCopyDialog()
    local f = CreateFrame("Frame", "MidnightUI_Console_CopyDialog",
                          UIParent, "BackdropTemplate")
    f:SetSize(680, 540)
    f:SetPoint("CENTER")
    f:SetFrameStrata("TOOLTIP")
    f:SetToplevel(true)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:SetClampedToScreen(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop",  f.StopMovingOrSizing)
    ApplyBackdrop(f, UI.bg, UI.border, 0.98, 1)

    if type(UISpecialFrames) == "table" then
        table.insert(UISpecialFrames, "MidnightUI_Console_CopyDialog")
    end

    -- Cyan accent along the top edge
    local accent = SolidTexture(f, "OVERLAY", UI.accent, 1)
    accent:SetHeight(LAYOUT.accentBarH)
    accent:SetPoint("TOPLEFT",  f, "TOPLEFT",  0, 0)
    accent:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)

    f.title = NewFontString(f, "OVERLAY", FONT.heading, UI.text)
    f.title:SetPoint("TOPLEFT", f, "TOPLEFT", 20, -18)
    f.title:SetText("Copy")

    f.hint = NewFontString(f, "OVERLAY", FONT.small, UI.muted)
    f.hint:SetPoint("TOPLEFT", f, "TOPLEFT", 20, -44)
    f.hint:SetText("Text is selected - press Ctrl+C to copy.  Esc closes.")

    local close = CreateFrame("Button", nil, f, "BackdropTemplate")
    close:SetSize(28, 28)
    close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -14, -10)
    ApplyBackdrop(close, UI.inset, UI.border, 1, 1)
    local closeLabel = NewFontString(close, "OVERLAY", FONT.body, UI.text)
    closeLabel:SetPoint("CENTER")
    closeLabel:SetText("X")
    close:SetScript("OnClick", function() f:Hide() end)

    local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT",     f, "TOPLEFT",     20,  -78)
    scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -36,  20)

    local box = CreateFrame("EditBox", nil, scroll)
    box:SetMultiLine(true)
    box:SetAutoFocus(false)
    box:SetFontObject("ChatFontNormal")
    box:SetTextColor(1, 1, 1, 1)
    box:SetTextInsets(8, 8, 6, 6)
    -- Pre-size so text renders even if SetText is called before the
    -- scroll frame's first OnSizeChanged.
    box:SetSize(608, 1)
    -- The default character/byte cap on EditBox is well under what a
    -- diagnostic dump can reach. Set explicit large caps; do NOT pass
    -- 0 (which silently clamps to zero capacity in current WoW).
    if box.SetMaxBytes   then box:SetMaxBytes(10000000) end
    if box.SetMaxLetters then box:SetMaxLetters(10000000) end
    box:SetScript("OnEscapePressed", function() f:Hide() end)
    scroll:SetScrollChild(box)
    scroll:SetScript("OnSizeChanged", function(_, w, _)
        box:SetWidth(math.max(100, w - 16))
    end)

    f.scroll = scroll
    f.box    = box
    f:Hide()
    return f
end

function M.ShowCopyDialog(text, title)
    if not _copyDialog then _copyDialog = BuildCopyDialog() end
    -- Strip WoW's protected-identity escape (|K...|k). EditBox silently
    -- refuses to display any text containing it, so we replace it with a
    -- readable placeholder before handing it to the copy dialog.
    local incoming = tostring(text or ""):gsub("|K.-|k", "<protected>")
    _copyDialog.title:SetText("Copy  -  " .. tostring(title or "Text"))
    _copyDialog:Show()
    _copyDialog:Raise()
    _copyDialog.box:SetText(incoming)
    _copyDialog.scroll:SetVerticalScroll(0)
    _copyDialog.box:SetFocus()
    _copyDialog.box:HighlightText()
end

function M.RefreshStats()
    if not frame or not frame.statsLabel then return end
    local console = _G.MidnightUI_Console
    if not (console and console.GetStats) then
        frame.statsLabel:SetText("engine offline")
        return
    end
    local s = console.GetStats()
    frame.statsLabel:SetText(string.format(
        "Session %d  |  %d Events  |  %d Incidents  |  %d Archived",
        s.session or 0,
        s.totalCaptured or 0,
        s.activeIncidents or 0,
        s.archivedIncidents or 0
    ))
end

local EnsureHeaderTicker  -- forward-declared; defined in the Header Stats section below

local function DoOpen()
    if not frame then BuildWindow() end
    frame:Show()
    frame:Raise()
    M.RefreshStats()
    EnsureHeaderTicker()
    local pop = M.tabPopulators[activeTabId]
    if pop and pop.refresh then
        pcall(pop.refresh)
    end
end

function M.Open()
    if InCombatLockdown and InCombatLockdown() then
        deferredOpen = true
        return
    end
    DoOpen()
end

function M.Close()
    if frame then frame:Hide() end
end

function M.Toggle()
    if frame and frame:IsShown() then
        M.Close()
    else
        M.Open()
    end
end

function M.IsShown()
    return (frame and frame:IsShown()) and true or false
end

-- ============================================================
-- SECTION: Header Stats Live Refresh
-- Header stats (Session N | N Events | N Incidents | N Archived)
-- are cheap to compute, so we refresh them on a short ticker
-- whenever the window is visible. This keeps them in sync with
-- engine state without needing every mutation path to remember
-- to call RefreshStats explicitly.
-- ============================================================
local _headerTicker
function EnsureHeaderTicker()  -- assigns to the forward-declared local above
    if _headerTicker then return end
    if not (_G.C_Timer and _G.C_Timer.NewTicker) then return end
    _headerTicker = _G.C_Timer.NewTicker(1.0, function()
        if frame and frame:IsShown() then
            M.RefreshStats()
        end
    end)
end

-- ============================================================
-- SECTION: Combat Deferral
-- If Open() is called while the player is in combat lockdown,
-- the request is stashed and the window opens automatically
-- when combat ends.
-- ============================================================
local combatFrame = CreateFrame("Frame", "MidnightUI_Console_UI_Combat")
combatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
combatFrame:SetScript("OnEvent", function()
    if deferredOpen then
        deferredOpen = false
        DoOpen()
    end
end)

-- End of Console_UI.lua
