-- Core/Drivers.lua
-- Proxy attribution for secure state / unit / attribute drivers.
--
-- RegisterStateDriver / RegisterUnitWatch / RegisterAttributeDriver hand a frame to
-- Blizzard's SecureStateDriverManager, which performs the actual Show / Hide /
-- SetAttribute LATER, from its own throttled OnUpdate (every 0.2s) and event handlers
-- (Blizzard_RestrictedAddOnEnvironment/SecureStateDriver.lua). At that moment the live
-- Lua stack names ONLY the Blizzard manager — the addon that registered the driver is
-- not on the stack — so Monitor's runtime debugstack attribution (ns.GetCaller) blames
-- Blizzard_RestrictedAddOnEnvironment.
--
-- We recover the real culprit by capturing attribution at REGISTRATION time (when the
-- addon IS on the stack) and keying it by the frame. Monitor consults this on proxied
-- mutations. There is no API to enumerate already-registered drivers, so this only
-- catches registrations made AFTER our hooks install — which is why the addon ships as
-- !!!TweakIdentifier (early load) so it sees other addons' driver registrations.

local ADDON, ns = ...

local D = {}
ns.Drivers = D

local hooksecurefunc = hooksecurefunc

-- The Blizzard addon folder whose SecureStateDriverManager runs proxied mutations.
local PROXY_FOLDER = "Blizzard_RestrictedAddOnEnvironment"
D.PROXY_FOLDER = PROXY_FOLDER

-- frame -> { ext, extTitle, culprit, culpritTitle, chain, stack, kind }
-- Weak keys: never keep a frame alive on the driver registry's account.
local registrants = setmetatable({}, { __mode = "k" })

local function capture(kind, frame)
	-- frame is a real frame reference (an addon's own frame); never secret here.
	if not ns.IsWidget(frame) then return end
	local culprit, culpritTitle, ext, extTitle, chain, stack = ns.GetCaller()
	-- Only record a real, external registrant. A Blizzard-registered driver (ext == nil)
	-- has nothing to attribute to and must not shadow the honest Blizzard fallback.
	if not ext then return end
	registrants[frame] = {
		ext = ext, extTitle = extTitle,
		culprit = culprit, culpritTitle = culpritTitle,
		chain = chain, stack = stack, kind = kind,
	}
end

-- Install is GATED BY DEEP INVESTIGATION and reload-armed. These are global-function hooks
-- that fire for EVERY driver registration in the game for the whole session, so (like the
-- source-location CVar in Core/Deep.lua) they are opt-in, not always-on. The gate is decided
-- at the BOTTOM of this file, during file load — the earliest moment our code runs, before
-- any addon that loads after us has registered its drivers (matching the original
-- unconditional install's timing, rather than relying on ADDON_LOADED ordering). Reading the
-- gate that early is only possible because `## LoadSavedVariablesFirst: 1` in the .toc loads
-- TweakIdentifierDB before our script files. When deep is off, nothing is hooked and D.Lookup
-- returns nil for every frame, so Monitor's driver-proxy recovery degrades cleanly to the
-- honest Blizzard fallback.
local installed = false

---Install the global register hooks. Idempotent; safe to call more than once.
function D.Install()
	if installed then return end
	installed = true
	-- Post-hooks are taint-safe and only read debugstack / write a Lua table — they never
	-- touch the frame or secure state. These globals are defined by
	-- Blizzard_RestrictedAddOnEnvironment (a hard FrameXML dependency loaded at startup), so
	-- they exist by ADDON_LOADED; the type guard is flavour safety, NOT a forbidden C_*
	-- call-site existence guard.
	if type(RegisterStateDriver) == "function" then
		hooksecurefunc("RegisterStateDriver", function(frame) capture("RegisterStateDriver", frame) end)
	end
	if type(RegisterUnitWatch) == "function" then
		hooksecurefunc("RegisterUnitWatch", function(frame) capture("RegisterUnitWatch", frame) end)
	end
	if type(RegisterAttributeDriver) == "function" then
		hooksecurefunc("RegisterAttributeDriver", function(frame) capture("RegisterAttributeDriver", frame) end)
	end
	-- Clearing on full unregister keeps stale attributions from lingering. Per-attribute
	-- state/attribute unregisters are intentionally left as-is (the frame may still carry
	-- other drivers, and "last registrant wins" is the attribution we want).
	if type(UnregisterUnitWatch) == "function" then
		hooksecurefunc("UnregisterUnitWatch", function(frame)
			if ns.IsWidget(frame) then registrants[frame] = nil end
		end)
	end
end

---True once the global register hooks have been installed this session (i.e. deep was active
---at file load). Monitor uses this to decide whether an unattributed secured-environment change
---is worth hinting `/twid deep` for: if the hooks are NOT installed, enabling deep would capture
---the registrant; if they ARE installed and we still recovered nothing, it is a Blizzard or
---pre-hook registration that deep cannot help with.
function D.Installed()
	return installed
end

---Attribution captured when a driver was registered on this frame, or nil.
function D.Lookup(frame)
	if ns.IsSecret(frame) then return nil end
	return registrants[frame]
end

---True when `culprit` (the first addon folder on the live stack) is the secure-driver proxy.
function D.IsProxyCulprit(culprit)
	return culprit == PROXY_FOLDER
end

-- The deep gate, decided at file load. `## LoadSavedVariablesFirst: 1` means the raw saved
-- table is populated before this chunk runs (ns.db / defaults are not applied until our
-- ADDON_LOADED, later — so read the global directly here, fully nil-guarded). We install iff
-- a deep investigation is active (armed on a prior reload, or its one-session grace). The
-- CVar is intentionally NOT consulted: the driver hooks are pure debugstack capture and work
-- regardless of whether source-location lookup actually stuck.
do
	local sv = TweakIdentifierDB
	if sv and sv.deep and sv.deep.active then
		D.Install()
	end
end
