-- ============================================================
-- Asylum Compagnon — Restrictions.lua  (0.38.x, Phase 1)
-- Couche CENTRALE d'état « restrictions Midnight ».
--
-- Rôle : dire au reste de l'addon, de façon fiable, s'il a le droit d'ENVOYER
-- un message d'addon, et exposer un vocabulaire de PROVENANCE des données.
--
-- Détection du blocage, à deux niveaux :
--   1) PRÉVENTIF : InKey() (clé chronométrée active). C'est le SEUL signal fiable —
--      test à 2 joueurs : les canaux passent (code 0) AVANT la clé, tous à 11 EN clé,
--      et InKey suit exactement ça. Les API C_ChatInfo.AreOutgoingAddonChatMessages‑
--      Restricted / InChatMessagingLockdown NE servent PAS de gate : la 1re est un
--      FAUX POSITIF (renvoie « true » en instance même quand l'envoi passe). Elles
--      restent uniquement INFORMATIVES (Snapshot / page Diag).
--   2) AUTORITAIRE : le CODE RETOUR réel de SendAddonMessage. Un « 11 »
--      (AddOnMessageLockdown) arme le verrou LOCKED, relâché au changement de
--      contexte (fin/reset de clé, changement de zone, entrée dans le monde).
--
-- Rien ici ne compare, ne convertit ni ne journalise une valeur secrète.
-- ============================================================

local ADDON_NAME, NS = ...

local R = {}
NS.Restrictions = R

-- ---------- Provenance des données (utilisée progressivement, phases 1→8) ----------
-- Toute donnée affichée doit porter sa provenance ; une source moins fiable ne
-- doit jamais écraser une source plus fiable pour la même action.
NS.SOURCE = {
    EXACT_LOCAL       = "EXACT_LOCAL",        -- action du joueur local, mesurée
    EXACT_API_DISPLAY = "EXACT_API_DISPLAY",  -- valeur exacte poussée direct API→UI (ex. Battle Rez secret)
    EXACT_SYNC        = "EXACT_SYNC",          -- message addon réellement reçu (hors lockdown)
    OBSERVED          = "OBSERVED",            -- événement vu localement (ex. interruption)
    INFERRED_HIGH     = "INFERRED_HIGH",
    INFERRED_MEDIUM   = "INFERRED_MEDIUM",
    UNKNOWN           = "UNKNOWN",
}

-- ---------- Clé Mythique+ CHRONOMÉTRÉE active ? ----------
function R.InKey()
    if C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive then
        local ok, active = pcall(C_ChallengeMode.IsChallengeModeActive)
        return (ok and active) and true or false
    end
    return false
end

-- ---------- API de contexte (INFORMATIVES seulement — pas de gate) ----------
-- Chaque helper renvoie (valeur_bool, api_présente_bool). L'API peut ne pas
-- exister (vieux client / avant 12.0.5) : dans ce cas (false, false).

-- Messages d'addon SORTANTS restreints (12.0.5+). ATTENTION : test à 2 joueurs, elle
-- renvoie « true » DÈS QU'ON EST EN INSTANCE, même quand l'envoi passe (code 0). Donc
-- NE PAS s'en servir comme gate — purement diagnostic.
function R.AddonRestricted()
    if C_ChatInfo and C_ChatInfo.AreOutgoingAddonChatMessagesRestricted then
        local ok, v = pcall(C_ChatInfo.AreOutgoingAddonChatMessagesRestricted)
        if ok then return (v and true) or false, true end
    end
    return false, false
end

-- Verrouillage générique de la messagerie de chat (diagnostic).
function R.ChatLocked()
    if C_ChatInfo and C_ChatInfo.InChatMessagingLockdown then
        local ok, v = pcall(C_ChatInfo.InChatMessagingLockdown)
        if ok then return (v and true) or false, true end
    end
    return false, false
end

-- ---------- Niveau 2 : verrou LOCKED (armé par un code 11 RÉEL) ----------
R.locked       = false     -- verrou runtime : un envoi a réellement retourné 11
R.lockedReason = nil
R.lockedSince  = nil
R.lockHits     = 0         -- nombre de « 11 » réellement encaissés (diag, remis à 0 au changement de contexte)

-- Appelé UNIQUEMENT quand un vrai envoi retourne le code 11.
function R.NoteLockHit(reason)
    if not R.locked then R.lockedSince = GetTime() end
    R.locked       = true
    R.lockedReason = reason or "send"
    R.lockHits     = R.lockHits + 1
end

function R.ClearLocked()
    R.locked       = false
    R.lockedReason = nil
    R.lockedSince  = nil
    R.lockHits     = 0
end

function R.IsLocked()
    return R.locked
end

-- ---------- Verdict combiné : a-t-on le droit de tenter un envoi ? ----------
-- Requête PURE (ne modifie aucun état). true = ne pas tenter.
-- CONSTAT EMPIRIQUE (test à 2 joueurs, 0.38.0) : AreOutgoingAddonChatMessagesRestricted
-- renvoie « true » DÈS QU'ON EST EN INSTANCE, MÊME quand les envois passent (code 0,
-- avant l'activation de la clé) — c'est un FAUX POSITIF. S'en servir comme gate
-- couperait toute la sync en donjon/raid, même hors clé (régression).
-- On gate donc sur InKey() (IsChallengeModeActive), qui suit EXACTEMENT le blocage
-- réel : false + code 0 avant la clé, true + code 11 en clé (vérifié à 2 clients),
-- et jamais déclenché en contenu normal. Le verrou LOCKED (code 11 réel) reste le
-- filet de sécurité. Les 2 API restent dans Snapshot, pour la page Diag seulement.
function R.OutgoingBlocked()
    if R.locked then return true end
    if R.InKey() then return true end
    return false
end

-- ---------- Instantané pour la page Diag (jamais de valeur secrète) ----------
function R.Snapshot()
    local ar, arP = R.AddonRestricted()
    local cl, clP = R.ChatLocked()
    return {
        inKey           = R.InKey(),
        addonRestricted = ar, addonApiPresent = arP,
        chatLocked      = cl, chatApiPresent  = clP,
        locked          = R.locked,
        lockedReason    = R.lockedReason,
        lockHits        = R.lockHits,
    }
end

-- ---------- Relâchement du verrou au changement de contexte ----------
-- Fin/reset de clé, changement de zone, entrée dans le monde : le contexte a
-- changé → on repart d'un état « non verrouillé » (les envois pourront re-tester).
local cf = CreateFrame("Frame")
cf:RegisterEvent("PLAYER_ENTERING_WORLD")
cf:RegisterEvent("ZONE_CHANGED_NEW_AREA")
if pcall(cf.RegisterEvent, cf, "CHALLENGE_MODE_COMPLETED") then end
if pcall(cf.RegisterEvent, cf, "CHALLENGE_MODE_RESET") then end
cf:SetScript("OnEvent", function()
    R.ClearLocked()
end)
