-- DopingControl ui/report.lua
-- Report preview overlay + raid-chat send queue.
--
-- Pure part (offline-testable):
--   DC_Report.SendDisabled(source)     -> bool (true iff source == "sim")
--   DC_Report.QueueCreate(lines)       -> queue (independent copy of lines)
--   DC_Report.QueueTick(q, now, src)   -> line-to-send-now | nil
--   DC_Report.QueueLabel(q)            -> "sending k/n" button label
--   DC_Report.BuildLines(store, db, tab) -> lines, vm (via DC_Model)
--
-- Queue semantics:
--   * First line goes out immediately on Send; every further line waits
--     >= SEND_INTERVAL (0.6 s) measured from the PREVIOUS ACTUAL SEND
--     (the client chat throttle is ~10 msgs/10 s --
--     0.6 s spacing stays safely under it even with other addons chatting).
--   * ABORT: if store.source flips to "sim" mid-drain, the very next tick
--     aborts the queue without sending (synthetic names must
--     never reach real chat). An aborted queue never resumes.
--   * In sim mode the Send button never activates in the first place
--     (SendDisabled predicate), with tooltip "disabled in test mode".
--
-- WoW wiring (behind `if CreateFrame then`): overlay frame
-- DopingControlReportFrame, DIALOG strata, anchored over the matrix frame,
-- scrollable preview FontString showing DC_Model.reportLines output EXACTLY
-- as it will be sent, buttons "Send to raid chat" + "Close".
-- SendChatMessage(line, "RAID") only ever fires from the queue drainer here
-- (single-sender rule).
--
-- Deliberate resolutions:
--   * DC_Report.Open() is exposed with OPTIONAL (vm, tab) arguments: the
--     matrix "Report..." button may pass its already-built vm; with no
--     arguments the report builds its own vm from DC.store via
--     DC_Model/DC_Roles and the current tab (DC_Matrix.GetTab()/.tab if
--     the matrix exposes it, else DC.currentTab, else "CONSUMABLES").
--   * The matrix frame global name is not fixed; anchoring
--     tries DC_Matrix.frame, then DopingControlFrame /
--     DopingControlMatrixFrame, else UIParent.
--   * Closing the overlay (Close / ESC) while draining cancels the rest of
--     the queue -- hidden frames get no OnUpdate on 1.12, so a hidden
--     "paused" queue would silently resume minutes later; cancel is the
--     predictable behavior.

DopingControl = DopingControl or {}
local DC = DopingControl

DC_Report = DC_Report or {}
local R = DC_Report

-- ==================================================================
-- Pure queue core
-- ==================================================================

R.SEND_INTERVAL = 0.6     -- seconds between two sent lines
R.CHAT_TYPE = "RAID"

-- Send is dead exactly in test mode. nil source (no scan
-- yet) is NOT sim: sending a "coverage 0/0" report is pointless but safe.
function R.SendDisabled(source)
    return source == "sim"
end

-- The chat API rejects the WHOLE message with "Invalid escape code in chat
-- message" when it meets a "|" that does not open a valid escape sequence
-- (|c |r |H |h |T |t |n ||). Our header line uses " | " as a plain ASCII
-- separator, so every report was refused. "||" is the escape for a literal
-- pipe and still renders as a single "|", which is why this runs at the
-- send point and NOT inside DC_Model.reportLines: the preview shows what
-- reportLines returns and must keep showing the text as it APPEARS in chat.
-- Report lines are pure ASCII by design (no item links, no colour codes),
-- so doubling every pipe cannot damage a real escape sequence here.
function R.ChatSafe(line)
    if not line then
        return line
    end
    return (string.gsub(line, "|", "||"))
end

-- Create a drain queue from an array of lines. Takes an independent copy
-- (the preview may be rebuilt while an old queue is still draining).
function R.QueueCreate(lines)
    local q = {
        lines = {},
        n = 0,
        sent = 0,
        nextAt = 0,        -- absolute time the next send is allowed
        done = false,
        aborted = false,
    }
    local n = table.getn(lines)
    for i = 1, n do
        table.insert(q.lines, lines[i])
    end
    q.n = n
    if n == 0 then
        q.done = true
    end
    return q
end

-- One drain step. now = current time (GetTime() in-game), source = current
-- store source ("scan"/"sim"/nil). Returns the line to send NOW, or nil.
-- Sets q.done when the last line went out, q.aborted on a sim flip.
function R.QueueTick(q, now, source)
    if not q or q.done or q.aborted then
        return nil
    end
    if source == "sim" then
        q.aborted = true          -- never resumes
        return nil
    end
    if now < q.nextAt then
        return nil
    end
    q.sent = q.sent + 1
    local line = q.lines[q.sent]
    q.nextAt = now + R.SEND_INTERVAL
    if q.sent >= q.n then
        q.done = true
    end
    return line
end

-- Button label while a queue exists.
function R.QueueLabel(q)
    if not q then
        return nil
    end
    if q.aborted then
        return "aborted"
    end
    return string.format("sending %d/%d", q.sent, q.n)
end

-- ==================================================================
-- Line building (pure given a store; uses DC_Model + DC_Roles)
-- ==================================================================

-- "14:04"-style timestamp: WoW 1.12 exposes a global date(); plain
-- Lua 5.0 has os.date.
function R.TimeStr()
    if type(date) == "function" then
        return date("%H:%M")
    end
    if os and type(os.date) == "function" then
        return os.date("%H:%M")
    end
    return "--:--"
end

-- Build the report lines for a store + db + tab. Returns lines, vm.
function R.BuildLines(store, dbt, tab)
    store = store or DC.store or { players = {} }
    tab = tab or "CONSUMABLES"
    -- Same expectation resolver the matrix uses, or the report preview
    -- would show different columns and different gap totals than the grid
    -- it was opened from (demo mode). Safe for the SEND path too: the demo
    -- layer only applies over the "sim" store, and R.SendDisabled blocks
    -- sending on exactly that source. Guarded because ui/report.lua is
    -- loadable without ui/matrix.lua (tools/luatests/test_report_queue.lua).
    local expect
    if DC_Matrix and DC_Matrix.ExpectTable then
        expect = DC_Matrix.ExpectTable(dbt)
    else
        expect = (dbt and dbt.expectations) or DC.DEFAULT_EXPECT or {}
    end
    local function roleOf(p)
        return DC_Roles.resolve(dbt, store.source, p)
    end
    local vm = DC_Model.build(store, roleOf, expect, tab)
    local label = (DC.TAB_LABEL and DC.TAB_LABEL[tab]) or tab
    return DC_Model.reportLines(vm, label, R.TimeStr()), vm
end

-- ==================================================================
-- WoW wiring
-- ==================================================================
if CreateFrame then

    local frame = nil          -- DopingControlReportFrame (lazy)
    local previewText = nil    -- the preview FontString
    local scrollChild = nil
    local infoText = nil
    local sendBtn = nil
    local sendBlocker = nil    -- invisible tooltip overlay while disabled
    local lines = nil          -- current preview lines (array)
    local queue = nil          -- active drain queue or nil

    local function db()
        return DopingControlDB
    end

    local function currentSource()
        return DC.store and DC.store.source
    end

    local function currentTab()
        if DC_Matrix then
            if DC_Matrix.GetTab then
                return DC_Matrix.GetTab()
            end
            if DC_Matrix.tab then
                return DC_Matrix.tab
            end
        end
        return DC.currentTab or "CONSUMABLES"
    end

    -- The matrix frame global name is not guaranteed; try the likely
    -- handles, fall back safely.
    local function anchorFrame()
        if DC_Matrix and DC_Matrix.frame then
            return DC_Matrix.frame
        end
        local m = getglobal("DopingControlFrame")
            or getglobal("DopingControlMatrixFrame")
        return m or UIParent
    end

    local function panelBackdrop()
        if DC_Widgets and DC_Widgets.PANEL_BACKDROP then
            return DC_Widgets.PANEL_BACKDROP
        end
        if DC.PANEL_BACKDROP then
            return DC.PANEL_BACKDROP
        end
        return {
            bgFile = "Interface\\AddOns\\DopingControl\\textures\\panel_bg",
            edgeFile = "Interface\\AddOns\\DopingControl\\textures\\panel_border",
            tile = true, tileSize = 128, edgeSize = 16,
            insets = { left = 5, right = 5, top = 5, bottom = 5 },
        }
    end

    -- Send button + blocker state machine: draining -> disabled "sending
    -- k/n"; sim -> disabled with tooltip blocker; else enabled.
    local function UpdateSendState()
        if not sendBtn then
            return
        end
        if queue and not queue.done and not queue.aborted then
            sendBtn:Disable()
            sendBtn:SetText(R.QueueLabel(queue))
            sendBlocker:Hide()
        elseif R.SendDisabled(currentSource()) then
            sendBtn:Disable()
            sendBtn:SetText("Send to raid chat")
            sendBlocker:Show()
        else
            sendBtn:Enable()
            sendBtn:SetText("Send to raid chat")
            sendBlocker:Hide()
        end
    end

    -- OnUpdate drainer (installed on the frame only while a queue exists;
    -- no allocation on the idle path).
    local function OnUpdateDrain()
        if not queue then
            this:SetScript("OnUpdate", nil)
            return
        end
        local line = R.QueueTick(queue, GetTime(), currentSource())
        if line then
            SendChatMessage(R.ChatSafe(line), R.CHAT_TYPE)
            UpdateSendState()
        end
        if queue.done or queue.aborted then
            queue = nil
            this:SetScript("OnUpdate", nil)
            UpdateSendState()
        end
    end

    local function OnSendClick()
        if queue then
            return
        end
        if not lines or table.getn(lines) == 0 then
            return
        end
        if R.SendDisabled(currentSource()) then
            return
        end
        queue = R.QueueCreate(lines)
        frame:SetScript("OnUpdate", OnUpdateDrain)
        UpdateSendState()
    end

    local function BuildReportFrame()
        if frame then
            return frame
        end
        local f = CreateFrame("Frame", "DopingControlReportFrame", UIParent)
        f:SetWidth(460)
        f:SetHeight(340)
        f:SetFrameStrata("DIALOG")
        f:SetPoint("CENTER", anchorFrame(), "CENTER", 0, 0)
        f:SetBackdrop(panelBackdrop())
        f:SetBackdropColor(1, 1, 1, 0.97)
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", function() this:StartMoving() end)
        f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
        f:SetClampedToScreen(true)

        -- title (gold)
        local title = f:CreateFontString("DopingControlReportTitle",
            "ARTWORK", "GameFontNormal")
        title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -14)
        local g = DC.COLORS and DC.COLORS.goldHi
        if g then
            title:SetTextColor(g.r, g.g, g.b)
        else
            title:SetTextColor(0.85, 0.66, 0.31)
        end
        title:SetText("Report preview")

        -- info line (line count + pacing note)
        infoText = f:CreateFontString("DopingControlReportInfo",
            "ARTWORK", "GameFontNormalSmall")
        infoText:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -30)
        infoText:SetTextColor(0.6, 0.6, 0.6)
        infoText:SetText("")

        -- close X
        local closeX = CreateFrame("Button", "DopingControlReportCloseX",
            f, "UIPanelCloseButton")
        closeX:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)

        -- scrollable preview: shows reportLines output EXACTLY as sent
        local scroll = CreateFrame("ScrollFrame", "DopingControlReportScroll",
            f, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -48)
        scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -36, 44)

        scrollChild = CreateFrame("Frame", "DopingControlReportScrollChild",
            scroll)
        scrollChild:SetWidth(400)
        scrollChild:SetHeight(1)
        scroll:SetScrollChild(scrollChild)

        previewText = scrollChild:CreateFontString("DopingControlReportText",
            "ARTWORK", "GameFontHighlightSmall")
        previewText:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0)
        previewText:SetWidth(390)
        previewText:SetJustifyH("LEFT")
        previewText:SetJustifyV("TOP")
        -- mono-ish: 1.12 ships no true monospace; the condensed number
        -- font keeps the indented name lists readable
        previewText:SetFont("Fonts\\ARIALN.TTF", 12)

        -- buttons
        sendBtn = CreateFrame("Button", "DopingControlReportSendButton",
            f, "UIPanelButtonTemplate")
        sendBtn:SetWidth(150)
        sendBtn:SetHeight(22)
        sendBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 14)
        sendBtn:SetText("Send to raid chat")
        sendBtn:SetScript("OnClick", OnSendClick)

        local closeBtn = CreateFrame("Button", "DopingControlReportCloseButton",
            f, "UIPanelButtonTemplate")
        closeBtn:SetWidth(80)
        closeBtn:SetHeight(22)
        closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 14)
        closeBtn:SetText("Close")
        closeBtn:SetScript("OnClick", function() f:Hide() end)

        if pfUI and pfUI.api then
            if pfUI.api.SkinButton then
                pfUI.api.SkinButton(sendBtn)
                pfUI.api.SkinButton(closeBtn)
            end
            if pfUI.api.SkinCloseButton then
                pfUI.api.SkinCloseButton(closeX)
            end
        end

        -- invisible overlay for the disabled-in-test-mode tooltip
        -- (disabled buttons receive no mouse events on 1.12)
        sendBlocker = CreateFrame("Frame", "DopingControlReportSendBlocker", f)
        sendBlocker:SetAllPoints(sendBtn)
        sendBlocker:SetFrameLevel(sendBtn:GetFrameLevel() + 2)
        sendBlocker:EnableMouse(true)
        sendBlocker:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
            GameTooltip:SetText("disabled in test mode", 1, 1, 1)
            GameTooltip:AddLine("Simulated names never reach real chat.", 0.8, 0.8, 0.8)
            GameTooltip:AddLine("The preview stays fully usable.", 0.8, 0.8, 0.8)
            GameTooltip:Show()
        end)
        sendBlocker:SetScript("OnLeave", function() GameTooltip:Hide() end)
        sendBlocker:Hide()

        -- closing cancels a running drain (see header)
        f:SetScript("OnHide", function()
            if queue then
                queue = nil
                this:SetScript("OnUpdate", nil)
            end
        end)

        -- ESC closes
        table.insert(UISpecialFrames, "DopingControlReportFrame")

        f:Hide()
        frame = f
        return f
    end

    -- Rebuild the preview content. vm/tab optional (see header).
    function R.Refresh(vm, tab)
        BuildReportFrame()
        tab = tab or currentTab()
        if vm then
            local label = (DC.TAB_LABEL and DC.TAB_LABEL[tab]) or tab
            lines = DC_Model.reportLines(vm, label, R.TimeStr())
        else
            lines = R.BuildLines(DC.store, db(), tab)
        end
        local n = table.getn(lines)
        previewText:SetText(table.concat(lines, "\n"))
        scrollChild:SetHeight(n * 14 + 24)
        infoText:SetText(string.format(
            "%d line%s - sent one per %.1f s", n, (n == 1) and "" or "s",
            R.SEND_INTERVAL))
        UpdateSendState()
    end

    -- Public entry point (matrix "Report..." button, /dc report).
    function R.Open(vm, tab)
        local f = BuildReportFrame()
        R.Refresh(vm, tab)
        f:Show()
    end

    function R.Toggle(vm, tab)
        local f = BuildReportFrame()
        if f:IsShown() then
            f:Hide()
        else
            R.Open(vm, tab)
        end
    end

end
