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

-- Shipped (packaged-build) identity primitives (KEYS-01): the single source of
-- truth for name/realm canonicalization and the presser's own canonical key.
-- Byte-identical relocation of RosterParser.lua's CanonicalKey/AsciiLower/
-- StripRealm and RosterArrange.lua's GetSelfRealm/GetSelfKey -- no new logic,
-- no behavioral change. Loads before the dev-only Roster stack (and outside
-- the #@do-not-package@ fence) so every shipped file, including Phase 37/38's
-- timeline page, can derive viewer identity without depending on the
-- battletag-privacy-scoped dev tier. Pure functions only -- no Ace/UI/comm
-- dependency, no SavedVars read/write.

-- Fold A-Z only (bytes 0x41-0x5A) to lowercase; every other byte, including
-- every UTF-8 multibyte sequence (o-umlaut, e-circumflex, curly apostrophe),
-- passes through untouched. Do NOT use string.lower (locale-sensitive) or a
-- positive alphanumeric pattern class (%a/%u/%w are ASCII-only under the WoW
-- Lua C locale and would truncate/corrupt multibyte names) -- this is a
-- byte-for-byte port of generate_rosteralts.py's ascii_lower.
local function AsciiLower(s)
    return (s:gsub("[A-Z]", function(c)
        return string.char(c:byte() + 32)
    end))
end

-- Literal 3-byte UTF-8 encoding of U+2019 (curly apostrophe): 0xE2 0x80 0x99.
local CURLY_APOSTROPHE = "\226\128\153"

-- Strip space, straight apostrophe, curly apostrophe, and hyphen from a realm
-- name only (never the character name). Two passes are required: a single-
-- byte character class handles space/straight-apostrophe/hyphen, then a
-- separate literal three-byte substring removal handles the curly apostrophe
-- -- a single Lua character class matches one byte per class, so folding the
-- curly apostrophe's three continuation bytes into it would strip those
-- bytes wherever they occur, not just this exact glyph. Byte-for-byte port
-- of generate_rosteralts.py's strip_realm.
local function StripRealm(realm)
    realm = realm:gsub("[ '%-]", "")
    realm = realm:gsub(CURLY_APOSTROPHE, "")
    return realm
end

-- Midnight (12.x) hands tainted addon execution SECRET VALUES in place of data
-- the client refuses to expose -- most visibly UnitFullName on a unit the addon
-- may not observe. A secret string cannot be indexed, so `s:gsub(...)` inside
-- AsciiLower raises "attempt to index local 's' (a secret string value, while
-- execution tainted by 'WildThings')" instead of returning anything.
-- issecretvalue itself does not exist on pre-Midnight clients, hence the
-- existence guard (same idiom as Invite.lua's whisper handlers).
local function IsSecret(v)
    return (issecretvalue and issecretvalue(v)) and true or false
end
Private.IsSecretValue = IsSecret

-- Variadic form of the guard above, for event handlers that have to clear a
-- WHOLE payload before touching any part of it. Midnight lets tainted code
-- store, pass on and concatenate a secret, but COMPARING one (`sender == ""`),
-- INDEXING one (`msg:lower()`) or using one as a table KEY (`keywordSet[msg]`)
-- raises immediately -- so a per-field guard placed after the first such use can
-- never fire, which is exactly how Invite.lua:199 shipped. Returns true as soon
-- as any argument is secret; callers then drop the event silently (fail closed).
-- nil is not secret, so this never replaces the existing nil/empty checks -- it
-- only has to run before them.
function Private.HasSecretValue(...)
    if not issecretvalue then return false end
    for i = 1, select("#", ...) do
        -- Extra parentheses truncate select's multiple returns to one value.
        if issecretvalue((select(i, ...))) then return true end
    end
    return false
end

-- Byte-identical port of generate_rosteralts.py's canonical_key:
--   ascii_lower(name) .. "-" .. ascii_lower(strip_realm(realm))
-- Pure function of its two arguments -- no globals besides Private, no
-- Ace/UI/comm dependency, no live game-state read. Nil/empty name or realm
-- (e.g. a realm-less token) coerces to an empty string rather than raising a
-- Lua error.
--
-- A SECRET name/realm degrades to that same empty string. This is a last-ditch
-- guard, not a feature: two secret names both collapse onto the key "-" and
-- would false-match each other. Callers that must know WHICH unit is which may
-- not route live unit data through here at all -- they compare unit tokens with
-- UnitIsUnit, which stays readable under taint (see SummonScreen's
-- RefreshTargetRing and SummonComm's ResolveClaimableTargetKey).
function Private:CanonicalKey(name, realm)
    if not name or IsSecret(name) then name = "" end
    if not realm or IsSecret(realm) then realm = "" end
    return AsciiLower(name) .. "-" .. AsciiLower(StripRealm(realm))
end

-- Lazily-resolved local player's own (normalized) realm, used to complete a
-- bare "Name" (same-realm) token/live-roster entry into "Name-Realm" before
-- canonicalizing. Promoted from RosterArrange.lua's file-local selfRealm/
-- GetSelfRealm to a Private: method so every shipped file (RosterArrange's
-- CanonicalizeLiveName, Invite.lua, and the future timeline page) can share
-- one cache instead of each keeping its own.
local selfRealm
function Private:GetSelfRealm()
    if not selfRealm then
        selfRealm = GetNormalizedRealmName() or ""
    end
    return selfRealm
end

-- The presser's own CanonicalKey -- never invite/self-target the presser.
function Private:GetSelfKey()
    local name, realm = UnitFullName("player")
    if not realm or realm == "" then
        realm = Private:GetSelfRealm()
    end
    return Private:CanonicalKey(name, realm)
end
