---@diagnostic disable: undefined-field, undefined-global
---@type string
local AddonName = ...
---@class Private
local Private = select(2, ...)

local Theme = Private.Theme
local WTUI = Private.WildThingsUI

-- BREAK-01..03: local-only BigWigs break-timer HUD reminder — Wild Things logo plus a
-- mm:ss countdown that turns yellow at ≤60s and red at ≤10s. Entirely local (no comm,
-- no broadcast): BigWigs_StartBreak fires on every client that has BigWigs loaded, and
-- Core.lua dispatches it to Private:OnBreakTimerStart(duration) defined here.
-- This is one complete vertical slice — detection (Core.lua), display (this file),
-- countdown ticker, colour transitions, settings seam, and movable anchor — proven
-- end-to-end before any later plans extend it.
--
-- Private.* dependencies:
--   At file-load time: Private.OnBreakTimerStart (defined below as a function on
--   Private — Core.lua calls it at runtime, not at load time).
--   At runtime only: WTUI:SavePosition/RestorePosition (UI/Functions.lua) and
--   WTUI:AddRoundedFill (UI/Functions.lua:180). WTUI.frameSettings is nil until
--   WTUI:Initialize runs from wt:OnInitialize, so nothing in this file may touch
--   position helpers before then (see EnsureBreakAnchor below — lazy, never called
--   at file-load time).
--
-- TOC slot is load-bearing: BreakReminder.lua must load after Init.lua (establishes
-- Private.Theme/Private.WildThingsUI) and after Core.lua (which dispatches to
-- Private.OnBreakTimerStart at runtime). Placed after FeastReminder.lua.

local BREAK_ANCHOR_KEY = "BreakReminder"

-- Construction-time constants.
local LOGO_PATH = "Interface\\AddOns\\WildThings\\Media\\Icons\\icon_big"
local LOGO_SIZE = 192

-- Build-once lazy HUD frame — created on first call, re-shown afterwards, never
-- recreated. Built lazily because WTUI.frameSettings does not exist until
-- WTUI:Initialize runs from wt:OnInitialize, well after every file has loaded.
local anchor

-- Edit-mode state, false at load. Toggled by the Settings unlock row (the ONLY entry
-- and exit point). MUST be declared above EnsureBreakAnchor: the drag scripts it
-- installs close over this variable, and in Lua a local is invisible to code written
-- before its declaration.
local editMode = false

-- EnsureBreakAnchor(): lazily builds the reminder's HUD frame — the Wild Things logo
-- anchored TOP plus a countdown label anchored TOP to the logo's BOTTOM — and
-- restores its saved screen position, falling back to a sane default above
-- center-screen on first-ever use.
local function EnsureBreakAnchor()
    if anchor then return anchor end

    -- Parenting to UIParent (not nil) is mandatory: WTUI:SavePosition only records
    -- points whose relativeTo is nil or UIParent (UI/Functions.lua:467), and a frame
    -- that records none produces an empty `points` table that RestorePosition reports
    -- success on while applying nothing.
    anchor = CreateFrame("Frame", nil, UIParent)
    anchor:SetSize(LOGO_SIZE, LOGO_SIZE + 56) -- logo + 48px label + gap
    anchor:SetFrameStrata("DIALOG")
    anchor:SetClampedToScreen(true)
    anchor:EnableMouse(false) -- never intercepts mouse clicks while locked
    anchor:Hide()

    -- Logo texture — direct SetTexture for the TGA asset (no spell-icon resolution
    -- needed, unlike Feast which resolves spellID → texture). Parented to anchor,
    -- ARTWORK draw layer, 192×192 centred at top.
    anchor.logo = anchor:CreateTexture(nil, "ARTWORK")
    anchor.logo:SetTexture(LOGO_PATH)
    anchor.logo:SetSize(LOGO_SIZE, LOGO_SIZE)
    anchor.logo:SetSnapToPixelGrid(false)
    anchor.logo:SetTexelSnappingBias(0)
    anchor.logo:SetPoint("TOP", anchor, "TOP", 0, 0)

    -- Countdown label — FontString below the logo, centred, theme text colour.
    -- 48px for visibility from across the room (break timer is looked at while AFK).
    anchor.label = anchor:CreateFontString(nil, "ARTWORK")
    anchor.label:SetFontObject(WildThingsFont48)
    anchor.label:SetJustifyH("CENTER")
    anchor.label:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
    anchor.label:SetPoint("TOP", anchor.logo, "BOTTOM", 0, -Theme.GAP)

    -- Edit-mode mover wiring, patterned after UI/Widgets/Window.lua:100-148 and
    -- FeastReminder.lua:119-130. The anchor itself is the drag handle.
    anchor:SetMovable(true)
    anchor:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" and editMode then
            anchor:StartMoving()
        end
    end)
    anchor:SetScript("OnMouseUp", function(_, button)
        if button == "LeftButton" and editMode then
            anchor:StopMovingOrSizing()
            WTUI:SavePosition(anchor, BREAK_ANCHOR_KEY)
        end
    end)

    -- Edit-mode visual treatment — rounded accent fill shown only while unlocked.
    anchor.editTreatment = CreateFrame("Frame", nil, anchor)
    anchor.editTreatment:SetAllPoints(anchor)
    WTUI:AddRoundedFill(anchor.editTreatment, Theme.RADIUS_SM, Theme.ACCENT_A20)
    anchor.editTreatment:Hide()

    -- RestorePosition returns TRUE after applying nothing when
    -- frameSettings[name].points is an empty table, so guard against that with
    -- GetNumPoints() == 0 (same trap FeastReminder.lua documents at :145).
    if not WTUI:RestorePosition(anchor, BREAK_ANCHOR_KEY) or anchor:GetNumPoints() == 0 then
        anchor:SetPoint("CENTER", UIParent, "CENTER", 0, 200)
    end

    return anchor
end

-- Active countdown ticker handle. A C_Timer.NewTicker handle is the mechanism by
-- which a second break restarts the window instead of inheriting the first showing's
-- remaining time (same "restart, not truncate" rule as Feast, FEAST-02).
local countdownTicker
local countdownRemaining = 0

-- Edit-mode preview ticker — separate from the real countdownTicker so the preview
-- never interferes with a live break. Only runs when edit mode is active AND no real
-- countdown is in progress. Started by EnterBreakEditMode, cancelled by
-- ExitBreakEditMode or by ShowBreakReminder (real break takes over).
local previewTicker
local previewRemaining = 300 -- 5:00 default preview duration

-- UpdateCountdownLabel(): called every second by the ticker. Decrements remaining
-- time, formats as mm:ss, applies colour thresholds, and hides the anchor at zero.
local function UpdateCountdownLabel()
    countdownRemaining = countdownRemaining - 1

    if countdownRemaining <= 0 then
        -- Tear down ticker and hide the anchor. Do NOT hide while editMode is
        -- active — a break expiring mid-positioning must not yank the frame the
        -- user is dragging out from under them.
        if countdownTicker then
            countdownTicker:Cancel()
            countdownTicker = nil
        end
        if not editMode then
            anchor:Hide()
        end
        return
    end

    local minutes = math.floor(countdownRemaining / 60)
    local seconds = countdownRemaining % 60
    anchor.label:SetFormattedText("%d:%02d", minutes, seconds)

    -- Colour thresholds: yellow at ≤60s, red at ≤10s, theme default otherwise.
    if countdownRemaining <= 10 then
        anchor.label:SetTextColor(1, 0, 0, 1)       -- red
    elseif countdownRemaining <= 60 then
        anchor.label:SetTextColor(1, 1, 0, 1)       -- yellow
    else
        anchor.label:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
    end
end

-- ShowBreakReminder(duration): shows the anchor, initialises the countdown label with
-- the full duration formatted as mm:ss, and (re)arms a 1-second ticker. A second call
-- while a countdown is already running cancels the outstanding ticker and starts a
-- brand-new one — one live countdown, promoted, never a second concurrent showing.
-- Also cancels any edit-mode preview ticker: a real break always takes over.
--
-- duration <= 0 is the cancel signal (BigWigs sends /break 0 to abort a break timer
-- early, e.g. when the RL pulls before the break ends). Tears down all tickers and
-- hides the anchor immediately — but NOT when editMode is active (same guard as the
-- countdown expiry path).
local function ShowBreakReminder(duration)
    if type(duration) ~= "number" then return end

    -- Cancel all tickers — preview and real — unconditionally. Then decide whether
    -- this is a cancel (hide) or a new timer (show).
    if previewTicker then
        previewTicker:Cancel()
        previewTicker = nil
    end
    if countdownTicker then
        countdownTicker:Cancel()
        countdownTicker = nil
    end

    -- Cancel signal: duration <= 0 means the break was aborted. Hide immediately
    -- unless the user is currently positioning the anchor.
    if duration <= 0 then
        if not editMode then
            EnsureBreakAnchor():Hide()
        end
        return
    end

    local breakAnchor = EnsureBreakAnchor()
    countdownRemaining = math.floor(duration)

    local minutes = math.floor(countdownRemaining / 60)
    local seconds = countdownRemaining % 60
    breakAnchor.label:SetFormattedText("%d:%02d", minutes, seconds)
    breakAnchor.label:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)

    -- Combat gate: arm the countdown either way, but never put the HUD on screen
    -- mid-fight. The combatGate frame below re-shows it the moment combat drops,
    -- provided the countdown is still running.
    if not InCombatLockdown() then
        breakAnchor:Show()
    end

    -- 1-second ticker for the countdown display. C_Timer.NewTicker fires every
    -- ~1.0s; the slight imprecision is acceptable for a break timer (it is not
    -- combat-critical).
    countdownTicker = C_Timer.NewTicker(1, UpdateCountdownLabel)
end

-- UpdatePreviewLabel(): edit-mode preview ticker callback. Decrements previewRemaining,
-- formats as mm:ss, restarts from 5:00 when it hits zero (loop, don't hide — the user
-- is still positioning). Stops itself if editMode becomes false (Settings toggle was
-- flipped off while preview was running).
local function UpdatePreviewLabel()
    if not editMode then
        if previewTicker then
            previewTicker:Cancel()
            previewTicker = nil
        end
        return
    end

    previewRemaining = previewRemaining - 1

    if previewRemaining <= 0 then
        -- Loop back to 5:00 — preview runs continuously while edit mode is active.
        previewRemaining = 300
    end

    local minutes = math.floor(previewRemaining / 60)
    local seconds = previewRemaining % 60
    anchor.label:SetFormattedText("%d:%02d", minutes, seconds)

    -- Colour thresholds in preview too: yellow at ≤60s, red at ≤10s.
    if previewRemaining <= 10 then
        anchor.label:SetTextColor(1, 0, 0, 1)
    elseif previewRemaining <= 60 then
        anchor.label:SetTextColor(1, 1, 0, 1)
    else
        anchor.label:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
    end
end

-- Core.lua dispatches BigWigs_StartBreak / BigWigs_StopBreak to this function at
-- runtime. Defined on Private so Core.lua can call it — Core.lua loads BEFORE this
-- file, but the function only needs to exist when BigWigs actually starts a break
-- (runtime, not file-load time).
--
-- duration <= 0 is a cancel (BigWigs /break 0) — always honoured, even when the
-- feature is disabled, to clean up a display that might have been started before
-- the toggle was flipped off. Positive durations respect the enabled toggle.
function Private:OnBreakTimerStart(duration)
    if type(duration) == "number" and duration <= 0 then
        ShowBreakReminder(duration) -- cancel, always honoured
        return
    end
    if not (WildThingsSavedVars and WildThingsSavedVars.breakreminder and WildThingsSavedVars.breakreminder.enabled) then
        return
    end
    ShowBreakReminder(duration)
end

---------------------------------------------------------------------------
-- Combat gate — the reminder is a 192px HUD overlay and has no business sitting
-- on top of a boss fight. A pull can legitimately land inside a running break,
-- and a misfiring trigger once put it there outright
-- (.planning/debug/resolved/break-timer-fires-in-combat.md).
--
-- Only VISIBILITY is gated; countdownTicker keeps running through combat. So a
-- break that expires mid-fight is correctly gone afterwards, and one that does
-- not comes back with the right remaining time — no re-arming, no drift.
--
-- Own frame rather than a Core.lua mainFrame handler: every piece of state this
-- touches (anchor, countdownTicker, editMode) is a file-local here, so routing
-- through Core.lua's named-method dispatch would mean exporting two Private
-- methods that exist purely as a relay. Same self-contained shape as
-- UI/Widgets/Tooltip.lua:112-120 and FeastReminder.lua's feastFrame.
---------------------------------------------------------------------------
local combatGate = CreateFrame("Frame")
combatGate:RegisterEvent("PLAYER_REGEN_DISABLED")
combatGate:RegisterEvent("PLAYER_REGEN_ENABLED")
combatGate:SetScript("OnEvent", function(_, event)
    -- editMode outranks the gate, exactly as it does the expiry and cancel hide
    -- paths above (:141-143, :187-191) — a pull must not yank the frame the user
    -- is dragging out from under them.
    if not anchor or editMode then return end

    if event == "PLAYER_REGEN_DISABLED" then
        anchor:Hide()
    elseif countdownTicker then
        -- Re-show only while a countdown is still live. A break that ran out
        -- during the fight must stay gone.
        anchor:Show()
    end
end)

---------------------------------------------------------------------------
-- BREAK-03: anchor edit mode — drag, persistence, reset.
--
-- Four exported entry points drive the Settings unlock toggle's get/set and
-- the reset button. Edit mode is entered and left ONLY through Settings — do
-- NOT add an automatic exit on ENCOUNTER_START (same rationale as
-- FeastReminder.lua:284-286: the Settings toggle's get() runs once at build
-- time, so any second exit path would leave the checkbox stale).
---------------------------------------------------------------------------

function Private:EnterBreakEditMode()
    EnsureBreakAnchor()
    editMode = true
    anchor:EnableMouse(true)
    anchor.editTreatment:Show()
    anchor.logo:Show()
    anchor.label:Show()
    anchor:Show()

    -- Start a live preview countdown at 5:00 so the user sees the full animated
    -- layout (logo + running timer + colour transitions) while positioning.
    -- Only start if no real break countdown is already in progress — a real break
    -- always takes precedence and already drives the label through its own ticker.
    if not countdownTicker then
        -- Cancel any stale preview before starting a fresh one.
        if previewTicker then
            previewTicker:Cancel()
            previewTicker = nil
        end
        previewRemaining = 300
        local minutes = math.floor(previewRemaining / 60)
        local seconds = previewRemaining % 60
        anchor.label:SetFormattedText("%d:%02d", minutes, seconds)
        anchor.label:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
        previewTicker = C_Timer.NewTicker(1, UpdatePreviewLabel)
    end
end

function Private:ExitBreakEditMode()
    if not anchor then return end
    -- Defensive StopMovingOrSizing: the user may have released the mouse
    -- outside the frame, leaving a drag gesture in progress.
    anchor:StopMovingOrSizing()
    -- Save on exit as well as on drag-stop — cheap insurance for the case
    -- where the user drags and then /reloads without a mouse-up landing on
    -- the frame.
    WTUI:SavePosition(anchor, BREAK_ANCHOR_KEY)
    editMode = false

    -- Cancel the preview ticker if it was running. Do NOT touch countdownTicker
    -- — a real break that started during edit mode must keep running.
    if previewTicker then
        previewTicker:Cancel()
        previewTicker = nil
    end

    anchor:EnableMouse(false)
    anchor.editTreatment:Hide()
    -- Hide the anchor unless a real countdown is currently running
    -- (countdownTicker outstanding means ShowBreakReminder's ticker is active).
    -- InCombatLockdown closes the one hole the combat gate cannot cover: leaving
    -- edit mode mid-fight, where the gate's editMode bypass had let the anchor
    -- stay on screen.
    if not countdownTicker or InCombatLockdown() then
        anchor:Hide()
    end
end

function Private:IsBreakEditMode()
    return editMode
end

function Private:ResetBreakAnchorPosition()
    local breakAnchor = EnsureBreakAnchor()
    breakAnchor:ClearAllPoints()
    breakAnchor:SetPoint("CENTER", UIParent, "CENTER", 0, 200)
    -- Persist the reset itself so it survives /reload rather than being
    -- undone by the next RestorePosition.
    WTUI:SavePosition(breakAnchor, BREAK_ANCHOR_KEY)
    print("|cff99ff00Wild Things:|r Break reminder position reset to default.")
end

---------------------------------------------------------------------------
-- Dev harness — /wt breaktest
---------------------------------------------------------------------------

-- Private:BreakTest(durationArg): dev-only, single-client proof of the display,
-- countdown, colour transitions, and restart behaviour with no BigWigs event.
-- Drives ShowBreakReminder directly — the /wt timelinetest/feasttest precedent,
-- NEVER through Private:Broadcast or comm (this feature has no comm path at all).
--
-- Plain /wt breaktest (no arg) defaults to 300s (5 min), matching a typical
-- mythic-raid break length. An explicit numeric argument overrides it.
function Private:BreakTest(durationArg)
    local duration = tonumber(durationArg) or 300
    if duration <= 0 then
        print("|cff99ff00Wild Things:|r Usage: /wt breaktest [duration_seconds]")
        return
    end

    ShowBreakReminder(duration)

    local breakAnchor = EnsureBreakAnchor()
    local point, _, _, offsetX, offsetY = breakAnchor:GetPoint(1)

    print("|cff99ff00Wild Things:|r breaktest duration=" .. tostring(duration)
        .. " point=" .. tostring(point) .. " (" .. tostring(offsetX) .. ", " .. tostring(offsetY) .. ")")
end

-- Private:BreakBarTest(minutesArg): dev-only end-to-end proof of the DETECTION half --
-- the seam /wt breaktest structurally cannot reach. /wt breaktest calls
-- ShowBreakReminder directly and therefore stays green even when the BigWigs dispatch in
-- Core.lua is 100% broken, which is exactly how a fully non-functional break detection
-- shipped (see .planning/debug/resolved/break-timer-no-icon.md).
--
-- This one instead publishes a real BigWigs_StartBreak message on BigWigs' actual message
-- bus, so it exercises everything /wt breaktest skips: that our callback is registered at
-- all in the current zone, that the loader's argument order matches the handler signature
-- in Core.lua, and that the value arriving at ShowBreakReminder is a number.
--
-- The message is BigWigs_StartBreak, NOT BigWigs_StartBar: since the Gemellus misfire
-- (.planning/debug/resolved/break-timer-fires-in-combat.md) Core.lua listens on the
-- dedicated break channel only, so a StartBar harness would exercise a path nothing reads
-- any more -- the exact trap break-timer-no-icon.md documents, one channel further on.
-- The payload mirrors BigWigs' own send site (BigWigs_Plugins/Break.lua:151
-- `SendMessage("BigWigs_StartBreak", plugin, seconds, nick, isDBM, reboot, L.breakBar, 134062)`);
-- module is nil here because our handler never reads it. Note that nothing inside BigWigs
-- consumes this message (it exists purely as a third-party hook), so unlike the previous
-- StartBar harness this produces NO BigWigs bar of its own -- the WildThings HUD appearing
-- is the whole signal.
--
-- Takes MINUTES (not seconds like /wt breaktest) to mirror `/break N`; 134062 is the
-- fork&knife icon BigWigs uses for break bars.
function Private:BreakBarTest(minutesArg)
    local minutes = tonumber(minutesArg) or 10
    if minutes <= 0 then
        print("|cff99ff00Wild Things:|r Usage: /wt breakbartest [duration_minutes]")
        return
    end

    -- BigWigs is a soft dependency -- existence-guard before touching it.
    if not (BigWigsLoader and BigWigsLoader.SendMessage) then
        print("|cff99ff00Wild Things:|r breakbartest: BigWigs is not loaded -- cannot test the dispatch path.")
        return
    end

    local seconds = minutes * 60
    print("|cff99ff00Wild Things:|r breakbartest: sending a real BigWigs_StartBreak ("
        .. tostring(seconds) .. "s). The reminder must appear WITHOUT /wt breaktest being involved.")

    BigWigsLoader:SendMessage("BigWigs_StartBreak", nil, seconds, UnitName("player"), false, false, "Break time", 134062)
end
