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

-- READY-05: cooldown check + opt-in side effect (auto-decline + chat announce).
-- Wired up by Core.lua's mainFrame:READY_CHECK handler, which now defers on
-- InCombatLockdown via a `pendingCooldownCheck` flag and re-runs
-- Private:RunCooldownCheck from PLAYER_REGEN_ENABLED.
--
-- Private.* deps: Private:RaidLeaderIsWildThings (RaidSnapshot.lua, TOC slot 58 --
-- well above this file's slot 81, and called only at runtime) for the wt_only
-- trigger mode; otherwise nothing beyond WildThingsSavedVars and the game API
-- surface used directly below (the overlay's Theme/Create*/IterateGroupMembers
-- dependencies were removed along with it). This file registers no events
-- of its own -- every trigger flows through mainFrame.

-- READY-05: capture the machine-generated global (cooldown_candidates.lua, an
-- accepted generated-global exception like TableContains/WTAPI -- see
-- .claude/CLAUDE.md Conventions) into Private so downstream consumers
-- (SettingsScreen.lua) read it via the shared Private table rather than the
-- raw global. `or {}` tolerates cooldown_candidates.lua being absent/stripped.
Private.CooldownCandidates = KEY_COOLDOWN_CANDIDATES or {}

-- ============================================================
-- READY-05 trigger gate (Vision-Angleichung)
--
-- Three layered conditions that BOTH the initial READY_CHECK handler AND the
-- PLAYER_REGEN_ENABLED re-fire must satisfy -- so the deferred resume doesn't
-- blindly re-issue a chat/decline against a raid the player has since left, or
-- on a different difficulty.
-- ============================================================

-- Difficulty gate (Normal/Heroic/Mythic/Mythic Flex). GetRaidDifficultyID()
-- returns the player's active raid difficulty, NOT GetInstanceInfo's field
-- (matches the pattern PartyMode.lua uses).
local READY_CHECK_COOLDOWN_DIFFICULTIES = {
    [14]  = true, -- Normal
    [15]  = true, -- Heroic
    [16]  = true, -- Mythic
    [233] = true, -- Mythic Flex ("Mythic - Flexible Raiding", 15-25 player;
                  -- FallbackDifficultyID 15, per Difficulty.db2)
}

-- Der wt_only-Leader-Check und die beiden Gildenliterale dahinter lagen früher hier
-- als Datei-lokale Kopie von Access.lua's WT_GUILD_NAME/WT_GUILD_REALMS. Beide
-- Kopien trugen einen "keep in sync"-Kommentar, und diese Duplizierung hat schon
-- einmal einen echten Bug ausgeliefert: eine einzige hartcodierte Realm brach jeden
-- Leader-Gilden-Check für einen Raid Lead, der auf der ANDEREN Realm des Connected-
-- Realm-Clusters eingeloggt war (gefunden 2026-07-26, gleichzeitig in Access.lua).
--
-- Der Check wohnt jetzt als Private:RaidLeaderIsWildThings in RaidSnapshot.lua
-- (TOC-Slot 58, weit über ReadyCheck.lua auf 81 -- und der Aufruf löst ohnehin erst
-- zur Laufzeit auf), die Literale ausschließlich in Access.lua hinter
-- Private:IsWildThingsGuildInfo. Verhalten unverändert; eine dritte Kopie für das
-- Dashboard-Gate wäre sonst dazugekommen (Debug-Session
-- dashboard-kill-scope-and-reset, 2026-07-29).

--- Combines the three trigger-mode gate conditions: in-raid + difficulty in
--- the allow-list + trigger_mode resolved. Always re-evaluates from live state
--- (raid, difficulty, leader guild); the PLAYER_REGEN_ENABLED re-fire calls
--- this to revalidate post-combat before issuing the deferred chat/decline.
--- @return boolean true if RunCooldownCheck should proceed
function Private:ShouldRunReadyCheckCooldown()
    if not IsInRaid() then return false end

    local diffID = GetRaidDifficultyID()
    if not READY_CHECK_COOLDOWN_DIFFICULTIES[diffID] then return false end

    local mode = WildThingsSavedVars.readycheck.cooldown.trigger_mode
    if mode == "disabled" then return false end
    if mode == "wt_only" then
        return Private:RaidLeaderIsWildThings()
    end
    -- "always" AND any unknown/legacy value (tolerated to keep a corrupted /
    -- never-migrated SavedVar from locking the feature off).
    return true
end

--- Pure raid-context gate (in-raid + difficulty + wt_only). Independent of
--- trigger_mode -- a sender with trigger_mode="disabled" still passes this
--- gate so the broadcast leg of RunCooldownCheck fires even when the local
--- chat/decline path is suppressed. Per user clarification 2026-07-26:
--- "Auch wenn ein Sender es auf Disabled gestellt hat, soll er trotzdem
--- seine Cooldowns per Comm channel senden, das disabled betrifft nur
--- seine Seite, also das in Chat schreiben." wt_only is preserved because
--- it is a per-raid context gate (not a per-user toggle) -- a sender with
--- wt_only should only contribute broadcasts in WT-led raids.
--- @return boolean true if broadcast-eligible
function Private:ShouldRunReadyCheckCooldownBroadcast()
    if not IsInRaid() then return false end

    local diffID = GetRaidDifficultyID()
    if not READY_CHECK_COOLDOWN_DIFFICULTIES[diffID] then return false end

    local mode = WildThingsSavedVars.readycheck.cooldown.trigger_mode
    if mode == "wt_only" then
        return Private:RaidLeaderIsWildThings()
    end
    -- "always" AND "disabled" AND any unknown/legacy value (the "disabled"
    -- branch is the WR-01 user clarification 2026-07-26; legacy tolerance
    -- mirrors the same default-on philosophy as ShouldRunReadyCheckCooldown).
    return true
end

-- ============================================================
-- READY-05 cooldown read path -- unchanged from prior phases (charges-aware
-- GetSpellRemaining; presence-guarded dual-path DeclineReadyCheck; per-spec
-- enabled-set iteration).
-- ============================================================

-- Threshold (seconds), shared by every spell in the user's per-spec selection
-- (READY-05) -- a spell only counts as "not ready" once its remaining
-- cooldown exceeds this.
local DEFAULT_COOLDOWN_THRESHOLD = 30

-- Spells: charges-aware -- a spell with >= requiredCharges charges available is NOT on
-- cooldown (GetSpellCharges takes priority over GetSpellCooldown when the
-- spell IS charge-based). requiredCharges defaults to 1 (today's "any charge
-- is ready" behaviour); for charge-based spells with more than one max charge
-- a higher value lets the user declare e.g. a 2-charge Balance Druid ability
-- "not ready" while only one charge has recharged. tonumber coercion is a
-- defensive guard against a hand-edited SavedVars value that is not a number.
local function GetSpellRemaining(spellID, requiredCharges)
    requiredCharges = tonumber(requiredCharges) or 1
    local chargeInfo = C_Spell.GetSpellCharges(spellID)
    if chargeInfo then
        if chargeInfo.currentCharges >= requiredCharges then
            return 0
        end
        if chargeInfo.isActive and chargeInfo.cooldownDuration > 0 then
            -- [ASSUMED -- not in-game verified] remaining time = time to next
            -- single-charge PLUS (requiredCharges - currentCharges - 1) full
            -- recharge cycles for the charges still missing beyond the one
            -- already accumulating. Compare against the in-game charge-recharge
            -- bars on a real multi-charge spell during UAT.
            local timeToNext = math.max(0, (chargeInfo.cooldownStartTime + chargeInfo.cooldownDuration) - GetTime())
            local extraCycles = (requiredCharges - chargeInfo.currentCharges - 1) * chargeInfo.cooldownDuration
            return timeToNext + extraCycles
        end
        return 0
    end

    local cd = C_Spell.GetSpellCooldown(spellID)
    if cd and cd.isEnabled and cd.isActive and cd.duration > 0 then
        return math.max(0, (cd.startTime + cd.duration) - GetTime())
    end
    return 0
end

-- Trinket slot resolver: resolves which of the two trinket slots the given
-- itemID currently occupies, or nil if it is in neither. Uses the named
-- FrameXML globals (INVSLOT_TRINKET1/2 = 13/14), never bare slot literals --
-- this is the whole "not equipped" mechanism, so it must gate BEFORE any
-- cooldown read (an unequipped itemID reads a 0 cooldown, i.e. "ready",
-- which is indistinguishable from a genuinely ready trinket if read first).
local function ResolveTrinketSlot(itemID)
    if GetInventoryItemID("player", INVSLOT_TRINKET1) == itemID then
        return INVSLOT_TRINKET1
    end
    if GetInventoryItemID("player", INVSLOT_TRINKET2) == itemID then
        return INVSLOT_TRINKET2
    end
    return nil
end

-- Trinket collector: sibling of GetSpellRemaining above, reading
-- readycheck.cooldown.per_spec_items[specID] (the trinket sibling of
-- per_spec -- see Migrations.lua's comment on why it is a separate store)
-- instead of per_spec. Returns one { itemID, spellID, name, remaining }
-- entry per stored itemID that is BOTH currently equipped in slot 13/14 AND
-- has a real on-use spell (C_Item.GetItemSpell's second return is numeric) --
-- a passive/proc trinket or a not-currently-equipped itemID contributes
-- nothing at all, silently. Below the 1.5s shared-cooldown floor the
-- trinket is ready and still reported, with remaining = 0, exactly like a
-- ready enabled spell. Duplicated verbatim in ReadyCheckCooldownComm.lua for
-- the same load-order-independence reason GetSpellRemaining is duplicated
-- there (see that file's comment) -- do NOT introduce a cross-file
-- Private.* helper for this.
local function GetTrinketCooldownEntries(specID)
    local entries = {}

    local rc = WildThingsSavedVars.readycheck
    local perItems = rc and rc.cooldown and rc.cooldown.per_spec_items and rc.cooldown.per_spec_items[specID]
    if not perItems then return entries end

    for itemID, isEnabled in pairs(perItems) do
        if isEnabled then
            local slot = ResolveTrinketSlot(itemID)
            if slot then
                local spellName, spellID = C_Item.GetItemSpell(itemID)
                if type(spellID) == "number" then
                    local start, duration, enable = GetInventoryItemCooldown("player", slot)
                    local remaining = 0
                    if enable == 1 and duration and duration > 1.5 then
                        remaining = math.max(0, (start + duration) - GetTime())
                    end
                    local name = C_Item.GetItemNameByID(itemID) or spellName or ("item:" .. tostring(itemID))
                    table.insert(entries, { itemID = itemID, spellID = spellID, name = name, remaining = remaining })
                end
            end
        end
    end

    return entries
end

-- Presence-guarded dual path: C_PartyInfo.ConfirmReadyCheck (current API)
-- else the legacy global (this client's Interface predates the Patch
-- 12.0.7 deprecation, so the legacy call remains authoritative today).
local function DeclineReadyCheck()
    if C_PartyInfo and C_PartyInfo.ConfirmReadyCheck then
        C_PartyInfo.ConfirmReadyCheck(false)
    else
        ConfirmReadyCheck(nil)
    end
end

-- Fixed-format, addon-constructed message only (no free text) -- stays
-- well inside SendChatMessage's 255-char backstop. Lockdown-guarded per
-- the RosterArrange.lua:1354 codebase convention.
local function PostCooldownChat(name, remainingSeconds)
    if C_ChatInfo and C_ChatInfo.InChatMessagingLockdown and C_ChatInfo.InChatMessagingLockdown() then return end
    local msg = ("Cooldown %s ready in %d seconds"):format(name, math.floor(remainingSeconds))
    if C_ChatInfo and C_ChatInfo.SendChatMessage then
        C_ChatInfo.SendChatMessage(msg, "RAID")
    else
        SendChatMessage(msg, "RAID")
    end
end

-- Pure helper shared by Private:RunCooldownCheck. Resolves the LIVE spec, then
-- covers TWO enabled sets for that spec: the spell set
-- (WildThingsSavedVars.readycheck.cooldown.per_spec[specID][spellID] = true)
-- and the currently-equipped on-use trinket set
-- (readycheck.cooldown.per_spec_items[specID][itemID] = true, via
-- GetTrinketCooldownEntries) -- returning one { onCD = true, name, remaining }
-- entry per enabled spell/trinket whose remaining cooldown exceeds
-- DEFAULT_COOLDOWN_THRESHOLD. A nil live specID (never happens once logged
-- in, but re-resolved fresh after every respec) returns an empty array. The
-- silent-skip invariant now covers TWO cases: nothing enabled fires nothing,
-- AND an enabled-but-currently-unequipped trinket also contributes nothing
-- (GetTrinketCooldownEntries already filters it out) -- neither is an error.
-- A trinket-only selection (empty per_spec, non-empty per_spec_items) still
-- gets a working check: the spell loop below is skipped when per_spec is
-- absent, but the trinket pass always runs.
local function GetCooldownStatuses()
    local statuses = {}

    local specID = select(1, GetSpecializationInfo(GetSpecialization()))
    if not specID then return statuses end

    local rc = WildThingsSavedVars.readycheck
    local perSpec = rc and rc.cooldown and rc.cooldown.per_spec and rc.cooldown.per_spec[specID]
    if perSpec then
        for spellID, isEnabled in pairs(perSpec) do
            if isEnabled then
                -- Per-spell "how many charges count as ready" threshold
                -- (PTW-04): ABSENT defaults to 1 (today's "any charge is
                -- ready" behaviour), unchanged for every existing install
                -- and every spell the user has not explicitly configured.
                -- Mirrors the same defensive three-form lookup used for
                -- `perSpec` directly above.
                local requiredCharges = rc.cooldown.per_spec_required_charges
                    and rc.cooldown.per_spec_required_charges[specID]
                    and rc.cooldown.per_spec_required_charges[specID][spellID]
                    or 1
                local remaining = GetSpellRemaining(spellID, requiredCharges)
                if remaining > DEFAULT_COOLDOWN_THRESHOLD then
                    local name = C_Spell.GetSpellName(spellID) or ("Spell " .. tostring(spellID))
                    table.insert(statuses, { onCD = true, name = name, remaining = remaining })
                end
            end
        end
    end

    for _, entry in ipairs(GetTrinketCooldownEntries(specID)) do
        if entry.remaining > DEFAULT_COOLDOWN_THRESHOLD then
            table.insert(statuses, { onCD = true, name = entry.name, remaining = entry.remaining })
        end
    end

    return statuses
end

-- Once-per-ready-check throttle for the auto-decline/chat side effect --
-- set on post, reset only by Private:ResetCooldownThrottle (Core.lua's
-- READY_CHECK_FINISHED handler).
local cooldownChatPosted = false

--- READY-05 side effect: opt-in auto-decline + throttled raid-chat post
--- PLUS the unconditional broadcast leg for the cooldown window popup.
---
--- Vision-Angleichung: `auto_decline` and `announce_chat` are now
--- independent SavedVars toggles -- previously bundled together as
--- `auto_not_ready`. Auto-decline AND chat post only when both the gate
--- (ShouldRunReadyCheckCooldown) and the relevant user toggle pass.
--- Fires when ANY enabled spell is over threshold; posts exactly ONE chat
--- line, for the worst (max remaining) on-CD spell, staying inside the
--- once-per-check throttle. Does nothing when the throttle is already set.
---
--- WR-01 fix (user clarification 2026-07-26): the broadcast leg fires
--- INDEPENDENTLY of the local chat/decline toggles -- a sender with both
--- `auto_decline=false` AND `announce_chat=false` still broadcasts, and a
--- sender with `trigger_mode="disabled"` still broadcasts (because the
--- `trigger_mode` gate has been hoisted out of Core.lua's READY_CHECK
--- entry and into the chat/decline path here). The chat/decline path
--- below gates on ShouldRunReadyCheckCooldown (trigger_mode + wt_only)
--- AND on auto_decline/announce_chat -- so a sender who has disabled
--- the local chat writing still contributes their cooldown data to the
--- WT-Blackhand raid lead's popup.
function Private:RunCooldownCheck()
    local rc = WildThingsSavedVars.readycheck
    if not (rc and rc.cooldown) then return end

    -- WR-01 fix: broadcast leg fires UNCONDITIONALLY (gated inside
    -- BroadcastAndShowReadyCheckCooldownWindow via
    -- ShouldBroadcastReadyCheckCooldown, which checks IsInRaid +
    -- difficulty + wt_only only). Independent of auto_decline,
    -- announce_chat, trigger_mode, AND auto_popup -- the user's toggles
    -- control only local chat/decline/popup behaviour, never the comm
    -- contribution.
    Private:BroadcastAndShowReadyCheckCooldownWindow()

    -- Chat/decline path: gates on trigger_mode + wt_only (via
    -- ShouldRunReadyCheckCooldown) AND on auto_decline/announce_chat.
    -- A sender with trigger_mode="disabled" OR both toggles off returns
    -- here without writing chat/decline -- but the broadcast above
    -- already fired.
    if not Private:ShouldRunReadyCheckCooldown() then return end
    local cd = rc.cooldown
    if not (cd.auto_decline or cd.announce_chat) then return end

    local statuses = GetCooldownStatuses()
    if #statuses == 0 then return end
    if cooldownChatPosted then return end

    local worst = statuses[1]
    for i = 2, #statuses do
        if statuses[i].remaining > worst.remaining then
            worst = statuses[i]
        end
    end

    if cd.auto_decline then DeclineReadyCheck() end
    if cd.announce_chat then PostCooldownChat(worst.name, worst.remaining) end
    cooldownChatPosted = true
end

--- Resets the once-per-ready-check chat/decline throttle. Called from
--- Core.lua's READY_CHECK_FINISHED handler.
function Private:ResetCooldownThrottle()
    cooldownChatPosted = false
end
