---@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: none 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)
}

-- Gespiegelt aus Access.lua:39-46 (keine Load-Order-Abhängigkeit zwischen den
-- Dateien einführen -- WildThings.toc's load order is the dependency graph,
-- and ReadyCheck.lua already loads at line 81 alongside other Private:*
-- consumers. These literals must be kept in sync with Access.lua:39-46 IF the
-- home guild/realms ever change (an Access.lua repo-wide rename). The guild
-- spans a connected-realm cluster (Blackhand + Mal'Ganis, confirmed by real
-- roster entries in RosterData.lua) -- a single hardcoded realm broke every
-- leader-guild check for a raid leader logged in on the OTHER realm of the
-- cluster (found 2026-07-26 alongside the same bug in Access.lua).
local WT_GUILD_NAME  = "Wild Things"
local WT_GUILD_REALMS = {
    ["Blackhand"] = true,
    ["Mal'Ganis"] = true,
}

-- Iterates the raid roster once and returns true iff the player's raid leader
-- is in the WT_GUILD_NAME guild on one of WT_GUILD_REALMS. Loop short-circuits
-- on the first leader found (raid size caps at 40 -- cheap). Returns false
-- when not in a raid, when no leader is reported, or when GetGuildInfo
-- returns nil. The four-return signature is GetGuildInfo(unit) -> name,
-- rankName, rankIndex, realm (per Access.lua's identical call style);
-- guildRealm is nil when the leader is on the VIEWER's own physical realm.
local function RaidLeaderIsWildThings()
    if not IsInRaid() then return false end
    local numMembers = GetNumGroupMembers()
    for i = 1, numMembers do
        local unit = "raid" .. i
        if UnitExists(unit) and UnitIsGroupLeader(unit) then
            local guildName, _, _, guildRealm = GetGuildInfo(unit)
            if guildName ~= WT_GUILD_NAME then return false end
            return guildRealm == nil or WT_GUILD_REALMS[guildRealm] == true
        end
    end
    return false
end

--- 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 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 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 >=1 charge available is NOT on
-- cooldown (GetSpellCharges takes priority over GetSpellCooldown when the
-- spell IS charge-based).
local function GetSpellRemaining(spellID)
    local chargeInfo = C_Spell.GetSpellCharges(spellID)
    if chargeInfo then
        if chargeInfo.currentCharges >= 1 then
            return 0
        end
        if chargeInfo.isActive and chargeInfo.cooldownDuration > 0 then
            return math.max(0, (chargeInfo.cooldownStartTime + chargeInfo.cooldownDuration) - GetTime())
        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

-- 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
-- iterates the user's enabled set for that spec
-- (WildThingsSavedVars.readycheck.cooldown.per_spec[specID][spellID] = true)
-- and returns one { onCD = true, name, remaining } entry per enabled spell
-- whose remaining cooldown exceeds DEFAULT_COOLDOWN_THRESHOLD. A nil live
-- specID (never happens once logged in, but re-resolved fresh after every
-- respec) or a nil/absent per-spec set both return an empty array -- the
-- silent-skip invariant: nothing enabled, nothing fires.
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 not perSpec then return statuses end

    for spellID, isEnabled in pairs(perSpec) do
        if isEnabled then
            local remaining = GetSpellRemaining(spellID)
            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

    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
