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

-- The addon's core game-event + comm-dispatch hub: owns the two OnEvent
-- trampolines (mainFrame, encounterFrame) that route Blizzard game events to
-- named handler methods, and registers the addon's PARTY/PARTY_POLICE comm
-- handlers via Private:RegisterCommEvent.
--
-- Expected Private.* dependencies: Private.Ace (wt, from WildThings.lua),
-- Private:RegisterCommEvent (from Init.lua), and the error-containment
-- helper added to Utility.lua in 24-01 (wraps every OnEvent trampoline
-- below) -- all must be defined before this file loads or before the
-- relevant handler fires (verify actual load order in WildThings.toc).
--
-- Timing: mainFrame and encounterFrame are created and SetScript'd for
-- OnEvent at file-load time, but their handlers only fire after login/reload
-- once the relevant events are registered and dispatched by the WoW client.
-- encounterFrame itself is created lazily, on the first ENCOUNTER_START.
-- The RegisterCommEvent calls below run at file-load time; the actual
-- AceComm:RegisterComm("WT_MSG", ...) wiring happens later, from
-- wt:OnInitialize() (see Transmission.lua).
local mainFrame = nil
if not mainFrame then mainFrame = CreateFrame("Frame") end

local encounterFrame

-- Break detection: the DEDICATED BigWigs_StartBreak / BigWigs_StopBreak messages, which
-- only BigWigs' Break plugin ever sends (Break.lua:151 and :112 are the sole send sites in
-- the entire AddOns folder).
--
-- This deliberately does NOT listen on BigWigs_StartBar any more. That channel carries
-- EVERY bar BigWigs creates, so a break could only be recognised by matching the bar TEXT
-- for "break"/"pause" -- a heuristic on a shared channel, and boss modules name their bars
-- freely. It duly misfired: LittleWigs renames Gemellus' *Neural Link* (spellID 1253709)
-- to CL.break_shields = "Break Shields" (BigWigs/Locales/enUS_common.lua:344), so the break
-- HUD popped up mid-pull on enUS clients -- structurally guaranteed, not bad luck. See
-- .planning/debug/resolved/break-timer-fires-in-combat.md.
-- Break.lua sends both messages side by side (:150 the generic bar, :151 the dedicated
-- one), so switching channels loses no coverage and needs no text matching at all. The DBM
-- break sync goes through the same plugin:StartBreak (Break.lua:155-158) and is therefore
-- still covered, as is a break restored after a /reload (Break.lua:63, reboot = true).
--
-- The leading `event` parameter is LOAD-BEARING, not decoration. This is NOT Ace3's
-- CallbackHandler -- BigWigs' loader carries its own hand-rolled callbackMap and invokes a
-- callback registered as a plain function via `securecallfunction(v, event, ...)`
-- (BigWigs/Loader.lua:1645), i.e. it PREPENDS the message name to the payload. Omitting the
-- parameter shifts every argument one slot right, which is exactly how this feature once
-- shipped 100% dead (.planning/debug/resolved/break-timer-no-icon.md). BigWigs' own
-- consumers declare it the same way -- BigWigs_Plugins/Bars.lua:1903.
--
-- Real payload for a `/break 10` (Break.lua:151):
--   module = the Break plugin table, seconds = 600 (Break.lua:185 converts minutes*60),
--   nick = who called it, isDBM, reboot, barText = L.breakBar, icon = 134062.
-- StopBreak (Break.lua:112) carries the same head minus barText/icon and always
-- seconds = 0 -- it is sent only from the `seconds == 0` abort branch.
--
-- Dispatches to Private:OnBreakTimerStart(duration) which BreakReminder.lua defines at
-- runtime -- the function only needs to exist when a break actually fires, not at
-- file-load time (the standard Core.lua → feature-module dispatch pattern). Its existing
-- `duration <= 0` cancel semantics is what StopBreak forwards, so no new entry point.
--
-- Registration is idempotent and unconditional: EnsureBreakBarRegistered()
-- runs at file scope and re-runs from PLAYER_ENTERING_WORLD so a BigWigs
-- load that completes after Core.lua is still picked up. Once registered,
-- the subscription stays for the whole session -- the feature is purely
-- local and there is no path that calls UnregisterMessage.
local breakBarRegistered = false

local function EnsureBreakBarRegistered()
    if breakBarRegistered or not BigWigsLoader then return end
    BigWigsLoader.RegisterMessage(WTAPI, "BigWigs_StartBreak", function(...)
        mainFrame:BigWigs_StartBreak(...)
    end)
    BigWigsLoader.RegisterMessage(WTAPI, "BigWigs_StopBreak", function(...)
        mainFrame:BigWigs_StopBreak(...)
    end)
    breakBarRegistered = true
end

EnsureBreakBarRegistered()

function mainFrame:BigWigs_StartBreak(event, module, seconds, nick, isDBM, reboot, barText, icon)
    if Private.OnBreakTimerStart then
        Private:OnBreakTimerStart(seconds)
    end
end

-- Break aborted (`/break 0`, e.g. the RL pulls early). Pass an explicit 0 rather than the
-- payload's `seconds`: 0 is what the cancel path documents, and it stays correct even if
-- BigWigs ever sends this message from somewhere other than its `seconds == 0` branch.
function mainFrame:BigWigs_StopBreak(event, module, seconds, nick, isDBM, reboot)
    if Private.OnBreakTimerStart then
        Private:OnBreakTimerStart(0)
    end
end

function mainFrame:OnEvent(event, ...)
    Private.safecall(self[event], self, event, ...)
end

function mainFrame:PLAYER_ENTERING_WORLD(event, isInitialLogin, isReloadingUi)
    EnsureBreakBarRegistered()

    if isInitialLogin and wt:CheckPartyModeRequirements() then
        print("|cff99ff00Wild Things:|r ", "|cffff0000Attenzione:|r You are Raid Lead and have Party Mode enabled.")
    end
end

function mainFrame:ENCOUNTER_START(event, ...)
    if encounterFrame == nil then
        encounterFrame = CreateFrame("Frame")

        function encounterFrame:ENCOUNTER_START(encounterID, encounterName, difficultyID, groupSize)
            if not WTAPI then return end

            if wt:CheckPartyModeRequirements() then
                wt:PartyMode_EncounterStart()
            end
        end

        function encounterFrame:ENCOUNTER_END(encounterID, encounterName, difficultyID, groupSize, success)
            encounterFrame:UnregisterAllEvents()

            if WildThingsSavedVars.update.party_mode then
                local index = UnitInRaid("player")
                if index then
                    local rank = select(2, GetRaidRosterInfo(index))
                    if rank == 2 then
                        wt:PartyMode_EncounterEnd()
                    end
                end
            end
        end

        -- Trampoline required because encounterFrame:SetScript("OnEvent", ...) below referenced
        -- an undefined handler, so ENCOUNTER_END/PLAYER_ENTERING_WORLD never dispatched. Unlike
        -- mainFrame's trampoline, this one must NOT forward the event name: encounterFrame's
        -- methods use positional signatures (ENCOUNTER_START/ENCOUNTER_END take the WoW event
        -- payload args directly, no leading event string), matching the direct
        -- encounterFrame:ENCOUNTER_START(...) call below. PLAYER_ENTERING_WORLD is registered
        -- but has no method here, so nil-guard the lookup rather than erroring on call.
        function encounterFrame:OnEvent(event, ...)
            local handler = self[event]
            Private.safecall(handler, self, ...)
        end

        encounterFrame:SetScript("OnEvent", encounterFrame.OnEvent)
    end

    encounterFrame:RegisterEvent("ENCOUNTER_END")
    encounterFrame:RegisterEvent("PLAYER_ENTERING_WORLD")

    encounterFrame:ENCOUNTER_START(...)
end

-- READY-05: cooldown check on raid ready-checks. Vision-Angleichung:
-- the CHAT/DECLINE trigger gate (raid + difficulty + trigger_mode +
-- wt_only) is centralized in Private:ShouldRunReadyCheckCooldown; the
-- BROADCAST leg (cooldown window popup) has its own gate in
-- Private:ShouldRunReadyCheckCooldownBroadcast which omits the
-- trigger_mode check -- so a sender with trigger_mode="disabled" still
-- contributes their cooldown data to the WT-Blackhand raid lead's
-- popup (user clarification 2026-07-26: "das disabled betrifft nur
-- seine Seite, also das in Chat schreiben"). Core.lua gates the
-- initial READY_CHECK AND the post-combat PLAYER_REGEN_ENABLED
-- resume via the BROADCAST gate (so a player who left the raid
-- during combat, dropped back to Normal, or whose leader's guild
-- check now fails does NOT get a stale post-combat broadcast) --
-- the chat/decline leg inside RunCooldownCheck re-gates via
-- ShouldRunReadyCheckCooldown, so a player with trigger_mode="disabled"
-- OR both chat toggles off still skips the local chat writing. If a
-- READY_CHECK fires while the player is in combat, the work is deferred
-- by setting `pendingCooldownCheck` and re-firing PLAYER_REGEN_ENABLED
-- after combat ends (mirrors the RosterArrange:pendingCombatArrange
-- defer pattern) -- applies to BOTH legs since both live inside
-- RunCooldownCheck. All handlers run through this file's existing
-- safecall-wrapped OnEvent trampoline above -- no new frame/wrapping
-- needed.

-- Session-local pending flag: a READY_CHECK arrived while in combat, defer
-- the cooldown check until combat ends. Not SavedVars-persisted (unlike
-- RosterArrange's pending_arrange) -- a reload-during-combat would
-- re-resolve the *next* READY_CHECK via the live gate, there's no source
-- value to replay.
local pendingCooldownCheck = false

function mainFrame:READY_CHECK(event, initiatorName, readyCheckTimeLeft)
    -- WR-01 fix: use the BROADCAST gate here (no trigger_mode check) so the
    -- broadcast leg fires even when trigger_mode="disabled". The chat/decline
    -- path inside RunCooldownCheck re-gates via ShouldRunReadyCheckCooldown
    -- (which DOES include trigger_mode) so a sender with disabled mode still
    -- skips the local chat writing -- "das disabled betrifft nur seine Seite,
    -- also das in Chat schreiben" (user clarification 2026-07-26).
    if not Private:ShouldRunReadyCheckCooldownBroadcast() then return end

    if InCombatLockdown() then
        pendingCooldownCheck = true
        return
    end

    Private:RunCooldownCheck()
end

function mainFrame:READY_CHECK_FINISHED(event, preempted)
    Private:ResetCooldownThrottle()
    pendingCooldownCheck = false
end

-- Deferred resume: re-fire the SAME broadcast gate (raid may have been
-- left, difficulty changed) before re-issuing the deferred broadcast +
-- chat/decline (RunCooldownCheck handles its own chat/decline gating
-- internally). If the gate now fails, silently drop the pending state.
function mainFrame:PLAYER_REGEN_ENABLED(event)
    if not pendingCooldownCheck then return end
    pendingCooldownCheck = false
    if Private:ShouldRunReadyCheckCooldownBroadcast() then
        Private:RunCooldownCheck()
    end
end

-- ACCESS-03: live guild-rank re-evaluation bridge. mainFrame is file-local to
-- this file, so Access.lua (which defines Private:ReEvaluateOwnRank) cannot
-- register these events itself -- this trampoline hosts them and delegates,
-- mirroring the READY_CHECK -> Private:RunCooldownCheck() shape above. Both
-- take a leading `event` param since they ride mainFrame:OnEvent (which
-- forwards the event string), unlike encounterFrame's no-event-arg handlers.
function mainFrame:PLAYER_GUILD_UPDATE(event, unitTarget)
    Private:ReEvaluateOwnRank()
end

function mainFrame:GUILD_ROSTER_UPDATE(event)
    Private:ReEvaluateOwnRank()
end

-- LIVE-01/LIVE-02: persistent, whole-session ENCOUNTER_END observer, distinct
-- from the lazy encounterFrame:ENCOUNTER_END above (positional signature,
-- assignment-feature-scoped, created only after the first ENCOUNTER_START of
-- the session, unregistered/re-registered every cycle). Two frames
-- independently registering ENCOUNTER_END is safe -- each gets its own
-- dispatch (WoW fires the event to every registered frame).
function mainFrame:ENCOUNTER_END(event, encounterID, encounterName, difficultyID, groupSize, success)
    Private:HandleTimelineEncounterEnd(encounterID, difficultyID, success)
end

Private:RegisterCommEvent("PARTY", function(sender, sound, duration)
    if wt:CheckPartyModeReceiverRequirements() then
        wt:ActivateParty(sound, duration)
    end
end)

Private:RegisterCommEvent("PARTY_POLICE", function(sender, name, duration)
    if wt:CheckPartyModeReceiverRequirements() then
        wt:ActivatePartyPolice(name, duration)
    end
end)

local function OnMemeChat(self, event, ...)
    if not IsInRaid() then return end
    if not WildThingsSavedVars.update.memes then return end

    local text = ...
    -- Midnight secret values (RosterKeys.lua's Private.HasSecretValue): comparing
    -- `text` raises when the payload is secret. CHAT_MSG_YELL in particular fires
    -- for arbitrary nearby players, whose identity -- and with it their chat
    -- payload -- is secret on instanced maps and in combat.
    if Private.HasSecretValue(text) then return end

    if text == "!ad" then
        wt:ActivateAd(6.8)
    elseif text == "!meme" then
        -- Existence-guarded so a load-order regression degrades to a no-op instead
        -- of a nil call (Memes.lua/Private.OnMemeCommand -- MEME-01..07).
        if Private.OnMemeCommand then
            Private.OnMemeCommand(...)
        end
    end
end
mainFrame.CHAT_MSG_RAID = OnMemeChat
mainFrame.CHAT_MSG_RAID_LEADER = OnMemeChat
mainFrame.CHAT_MSG_YELL = OnMemeChat

mainFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
mainFrame:RegisterEvent("ENCOUNTER_START")
mainFrame:RegisterEvent("READY_CHECK")
mainFrame:RegisterEvent("READY_CHECK_FINISHED")
mainFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
mainFrame:RegisterEvent("PLAYER_GUILD_UPDATE")
mainFrame:RegisterEvent("GUILD_ROSTER_UPDATE")
mainFrame:RegisterEvent("ENCOUNTER_END")

-- Chat events feeding OnMemeChat: registered here, unconditionally, and
-- deliberately NOT inside an activation gate. The previous gate was
-- Private.CheckInstanceID(instanceID), which has been always-false since
-- 88d79da deleted DefaultNotes/ (the only writer of Private.activeInstances).
-- The handler (mainFrame.CHAT_MSG_* = OnMemeChat below) gates on
-- WildThingsSavedVars.update.memes, and Private.OnMemeCommand gates on
-- author identity, combat and the 7s cooldown -- no instance restriction
-- intended (matches quick-task 260728-50i's "anyone in the raid" scope).
-- Pattern follows worktree-debug-meme-ad-chat-commands (commit 0e5d510).
mainFrame:RegisterEvent("CHAT_MSG_RAID")
mainFrame:RegisterEvent("CHAT_MSG_RAID_LEADER")
mainFrame:RegisterEvent("CHAT_MSG_YELL")

mainFrame:SetScript("OnEvent", mainFrame.OnEvent)
