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

-- Shipped guild-rank capability layer (ACCESS-01, cosmetic half only this
-- plan). Private.CAPABILITIES is a hardcoded, version-pinned threshold table
-- -- NOT a SavedVar, since a per-client SavedVar cannot deliver a guild-wide-
-- consistent trust value (SEED-006 Sec.7.1, locked). Private:ViewerHasCapability
-- reads only the local player's own GetGuildInfo("player") -- a UI/rendering
-- decision, bypassable by design, and provides ZERO security on its own. The
-- authoritative receiver-side trust boundary is Private:SenderHasCapability,
-- built in Plan 36-04 alongside the guild-event re-eval hook -- do not treat
-- ViewerHasCapability as sufficient to gate anything but local UI visibility.

-- rankIndex <= 2 == GM (0) + two officer ranks, the documented default per
-- CONTEXT.md's locked decision. Flagged for an in-game phase-1 rank spot-check
-- to confirm this guild's actual officer rank indices; adjust the constant
-- (not the gating logic) if the spot-check finds a mismatch.
Private.CAPABILITIES = {
    ["timeline.share"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
    ["inspect.refresh"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
    ["roster.view"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
    ["bank.view"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
    ["devtools.view"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
    ["readycheck.cooldownwindow"] = { maxRank = 2 }, -- rankIndex <= 2 (GM=0 + two officer ranks) -- SPOT-CHECK in-game
}

-- Hardcoded home-guild name AND realm(s). This addon is built for a single
-- specific guild, which spans a connected-realm cluster ("a ~25-27 player
-- guild" per CLAUDE.md) -- confirmed by real roster entries in
-- RosterData.lua on BOTH "Blackhand" and "Mal'Ganis".
-- "Wild Things" alone is not
-- a unique identifier -- a same-named guild on an unrelated realm/region
-- must not pass. Without the realm check, a rank-0-2 officer of any other
-- same-named guild anywhere would also pass ViewerHasCapability/
-- SenderHasCapability's rank test -- guild rank alone is not a trust
-- boundary across different guilds/realms. A single hardcoded realm broke
-- every member logged in on the OTHER realm of the cluster (found 2026-07-26
-- via a rank-2 Mal'Ganis viewer missing all capabilities) -- so this is a
-- whitelist of the cluster's realms, not one string. GetNormalizedRealmName()
-- (not GetGuildInfo's own realm field, which returns nil for "player" in
-- practice) is the authoritative source for the viewer's own login realm.
local WT_GUILD_NAME = "Wild Things"
local WT_GUILD_REALMS = {
    ["Blackhand"] = true,
    ["Mal'Ganis"] = true,
}

-- Cosmetic viewer-side gate: fails closed on an unknown capability string, a
-- viewer not currently in a guild, a viewer on the wrong realm, a viewer in
-- a DIFFERENT guild than WT_GUILD_NAME, or a not-yet-resolved (nil) own rank
-- index (e.g. the brief window right after login before GetGuildInfo
-- settles -- ACCESS-03's guild-event re-eval hook, Plan 36-04, re-checks
-- this later).
function Private:ViewerHasCapability(cap)
    local entry = Private.CAPABILITIES[cap]
    if not entry then return false end -- fail-closed: unknown/typo'd capability
    if not IsInGuild() then return false end
    if not WT_GUILD_REALMS[GetNormalizedRealmName()] then return false end -- fail-closed: not one of OUR realms
    local myGuild, _, myRank = GetGuildInfo("player")
    if myGuild ~= WT_GUILD_NAME then return false end -- fail-closed: not OUR guild
    if myRank == nil then return false end -- fail-closed: rank not yet resolved
    return myRank <= entry.maxRank
end

-- AUTHORITATIVE TRUST BOUNDARY (ACCESS-02). Every receiving client independently
-- re-validates a remote sender through this function before honoring any
-- comm-driven action -- unlike ViewerHasCapability above (cosmetic/local-UI-only,
-- bypassable by design), SenderHasCapability's rank is NEVER taken from a
-- self-asserted claim (a payload field, a parameter, anything the sender
-- controls). Its only authority is this client's OWN GetGuildInfo(sender)
-- lookup. Fails closed, in order, on: unknown capability; viewer not in a
-- guild; viewer not on one of WT_GUILD_REALMS; viewer not in WT_GUILD_NAME;
-- sender not in the SAME guild by name; sender not on a known WT realm
-- (rejects a same-named player on an unrelated realm, T-36-07); and an
-- unresolved (nil) sender rank (e.g. out-of-proximity/not-yet-cached --
-- deny, never trust, T-36-10); and a malformed (nil/non-string) sender, since
-- GetGuildInfo requires a string unit token and errors rather than returning
-- nil otherwise (WR-01 review fix -- hardens ahead of Phase 37's comm-driven
-- caller, which will pass an untrusted comm-supplied sender value).
--
-- senderRealm check: GetGuildInfo's realm return is nil when the queried unit
-- is on the VIEWER's own physical realm, and a realm string only when it's on
-- a DIFFERENT realm within the connected cluster. Comparing it against
-- myRealm (always nil for "player") instead of the WT_GUILD_REALMS whitelist
-- rejected every legitimate sender logged in on the cluster's other realm --
-- found 2026-07-26 alongside the ViewerHasCapability realm bug.
function Private:SenderHasCapability(sender, cap)
    local entry = Private.CAPABILITIES[cap]
    if not entry then return false end -- fail-closed: unknown/typo'd capability
    if type(sender) ~= "string" or sender == "" then return false end -- fail-closed: malformed sender
    if not WT_GUILD_REALMS[GetNormalizedRealmName()] then return false end -- fail-closed: not one of OUR realms

    local myGuild = GetGuildInfo("player")
    if myGuild == nil then return false end -- fail-closed: viewer not in a guild
    if myGuild ~= WT_GUILD_NAME then return false end -- fail-closed: not OUR guild (two same-guild-but-not-Wild-Things clients must not trust each other)

    local senderGuild, _, senderRank, senderRealm = GetGuildInfo(sender)
    if senderGuild ~= myGuild then return false end -- fail-closed: different guild
    if senderRealm ~= nil and not WT_GUILD_REALMS[senderRealm] then return false end -- fail-closed: cross-realm same-name collision (T-36-07) -- nil means "viewer's own (already-whitelisted) realm"
    if senderRank == nil then return false end -- fail-closed: unresolved sender rank (T-36-10)

    return senderRank <= entry.maxRank
end

-- ACCESS-03: live own-rank re-evaluation, cache-gated so unrelated
-- GUILD_ROSTER_UPDATE floods don't churn the nav relayout (T-36-11).
-- cachedOwnRank stays nil until the first resolved read, distinguishing
-- "never checked" from "rank 0" -- so an unresolved-both-times call
-- (nil == nil) correctly no-ops instead of appearing to "change."
local cachedOwnRank

-- Wired from Core.lua's mainFrame:PLAYER_GUILD_UPDATE / GUILD_ROSTER_UPDATE
-- trampolines (Access.lua cannot register events itself -- mainFrame is
-- file-local to Core.lua). Re-reads GetGuildInfo("player") fresh; only on an
-- actual rankIndex change does it update the cache and relayout the nav,
-- guarded on Private.rosterShell existing (the shell may not be open yet).
function Private:ReEvaluateOwnRank()
    local _, _, rankIndex = GetGuildInfo("player")
    if rankIndex == cachedOwnRank then return end -- no change: no relayout
    cachedOwnRank = rankIndex
    if Private.rosterShell then
        Private.rosterShell:RelayoutNav()
    end
end
