-- Core/Deep.lua
-- "Deep investigation" has TWO halves, armed together by this module:
--   1. The engine's source-location tracking, which makes frame:GetSourceLocation() return
--      the file/line that created each frame (so we can name the creating addon for ANY
--      frame, retroactively). Gated by the `enableSourceLocationLookup` CVar, which only
--      takes effect after a /reload and has a perf/memory cost. This half is RETAIL 12.0
--      ONLY (ns.HasSourceLocation, set in Core.lua) — it does not exist on Classic flavors.
--   2. Core/Drivers.lua's secure-driver capture hooks, which attribute driver-proxied
--      Show/Hide/SetAttribute changes. Those install only at FILE LOAD, gated on the
--      persisted `deep.active` flag — so they likewise need a /reload to come into effect.
--      This half is pure debugstack capture and works on EVERY flavor (CVar-independent).
-- CROSS-VERSION: the two halves are decoupled by ns.HasSourceLocation. On Retail, deep is
-- "active" only when the CVar stuck (half 1) — half 2 rides along. On Classic, there is no
-- CVar, so an armed-and-reloaded investigation is active on half 2 alone (driver attribution);
-- frame-creator naming simply degrades to "creator unknown." Every CVar touch / status message
-- branches on ns.HasSourceLocation (see deepLive / deepWhat).
-- Because half 2 needs a reload regardless of the CVar, the no-reload fast path in D.Arm
-- requires ns.Drivers.Installed() AND (the CVar on, or no source-location at all) — see there.
-- The whole thing is opt-in and confirmed. By default it auto-disables after ONE session; the `persist` flag
-- (D.SetPersist / "/twid deep persist") instead keeps it enabled across reloads, with a
-- delayed login reminder so the user never forgets the engine CVar + global driver hooks
-- are still live.
--
-- State lives in ns.db.deep = { active, enabledByUs, wasOnBefore, pending, persist } so we:
--   * check if it was ALREADY enabled before touching it (wasOnBefore),
--   * only ever turn it back OFF if WE turned it on (enabledByUs) — never clobber a
--     user's own setting,
--   * never leave it on forever UNLESS persist is set — otherwise an active investigation
--     auto-disables on the next login/reload after the one it was armed in.

local ADDON, ns = ...

local D = {}
ns.Deep = D

local CVAR = "enableSourceLocationLookup"

local _GetCVar     = (C_CVar and C_CVar.GetCVar) or GetCVar
local _GetCVarBool = (C_CVar and C_CVar.GetCVarBool) or GetCVarBool
local _SetCVar     = (C_CVar and C_CVar.SetCVar) or SetCVar
-- Cross-version reload: C_UI.Reload is the modern form; ReloadUI is the universal global
-- present on every flavor incl. Retail. Defined at file top so armAndReload (below) captures it.
local _Reload      = (C_UI and C_UI.Reload) or ReloadUI

local function cvarOn()
	-- Fall back to the string getter so we never misread "off" on a client missing
	-- GetCVarBool and then wrongly take ownership of a user's pre-existing setting.
	if _GetCVarBool then
		local ok, v = pcall(_GetCVarBool, CVAR)
		if ok then return v and true or false end
	end
	if _GetCVar then
		local ok, v = pcall(_GetCVar, CVAR)
		if ok then return v == "1" end
	end
	return false
end
D.CVarOn = cvarOn

-- Delay (seconds) before the permanent-mode login reminder, so it lands after the flood of
-- other addons' load messages instead of being buried in it.
local PERSIST_HINT_DELAY = 6

-- Refresh the window's deep status line, but only if the window is already open — never force
-- it to build from a slash command the user ran with the window closed.
local function refreshUI()
	if ns.db and ns.db.window and ns.db.window.shown and ns.UI and ns.UI.SetStatus then
		ns.UI.SetStatus(ns.Monitor and ns.Monitor.active)
	end
end

function D.Available()
	-- Available on every flavor: the driver-capture half (Core/Drivers.lua) works anywhere the
	-- secure-driver register globals exist. The source-location half is the part gated by
	-- ns.HasSourceLocation (Retail 12.0). Effectively always true; kept as a defensive guard.
	return ns.HasSourceLocation or type(RegisterStateDriver) == "function"
end

---True when an investigation is live. On Retail this additionally requires the source-location
---CVar to be on (its headline half); on Classic (no source-location) an armed investigation is
---live on the driver-capture half alone — there is no CVar to gate it.
function D.IsActive()
	if not (ns.db and ns.db.deep and ns.db.deep.active) then return false end
	return cvarOn() or not ns.HasSourceLocation
end

---True when permanent (persist-across-reloads) mode is set. For UI affordances.
function D.IsPersistent()
	return (ns.db and ns.db.deep and ns.db.deep.persist) or false
end

local function setCVar(val)
	if _SetCVar then pcall(_SetCVar, CVAR, val) end
end

-- "Did deep actually come up this session?" — Retail needs the source-location CVar to have
-- stuck; Classic (no CVar) needs Core/Drivers.lua's capture hooks to have installed at file load
-- (they install when the persisted deep.active was true). Single capability-aware success check.
local function deepLive()
	if ns.HasSourceLocation then return cvarOn() end
	return (ns.Drivers and ns.Drivers.Installed()) and true or false
end

-- What deep can tell you, by capability — used in status messages so we never promise frame-
-- creator naming on a client that lacks source-location.
local function deepWhat()
	return ns.HasSourceLocation and "frame creators are identifiable"
		or "driver-proxied changes are attributed"
end

-- Persist an armed investigation and reload. The reload is required either to make the
-- source-location CVar take effect OR to let Core/Drivers.lua install its secure-driver
-- capture hooks at the next file load (gated on the persisted deep.active) — deep needs both
-- halves. Ownership is read from the LIVE CVar state so Stop() never switches off a
-- source-lookup setting the user already had on (e.g. arming purely to get the driver hooks).
local function armAndReload()
	-- Only touch the source-location CVar on a client that has it (Retail 12.0). On Classic the
	-- reload exists purely to let Core/Drivers.lua install its driver-capture hooks at the next
	-- file load (gated on the persisted deep.active) — there is no CVar to set or to own.
	local alreadyOn = ns.HasSourceLocation and cvarOn()
	ns.db.deep.wasOnBefore = alreadyOn
	ns.db.deep.enabledByUs = ns.HasSourceLocation and not alreadyOn
	ns.db.deep.active      = true
	ns.db.deep.pending     = true
	if ns.HasSourceLocation and not alreadyOn then setCVar("1") end
	_Reload()
end

-- StaticPopup: the "ask the user" confirmation (a /reload is required).
-- NEVER reassign the global `StaticPopupDialogs` (no `= StaticPopupDialogs or {}`): writing
-- the global VARIABLE from addon code taints it, and every later reader (Blizzard menus,
-- other addons) inherits our taint — which is exactly how Esc → ToggleGameMenu → ClearTarget()
-- became ADDON_ACTION_FORBIDDEN (confirmed in Logs/taint.log). The table is a guaranteed
-- FrameXML global; only ever write our own KEY into it (taints just that entry, read by
-- Blizzard via securecall — safe). See .context/patterns.md Taint Prevention.
StaticPopupDialogs["TWEAKIDENTIFIER_DEEP_ARM"] = {
	-- Chosen at file load: ns.HasSourceLocation (set in Core.lua, file #1) is already known.
	-- Retail gets the full text (creator + driver halves); Classic, which has no source-location,
	-- gets the driver-attribution-only text so we never promise frame-creator naming it can't do.
	text = ns.HasSourceLocation
		and "|cff33ff99TweakIdentifier|r\n\nStart deep investigation?\n\nThis turns on the game's frame source-location tracking and the addon's secure-driver attribution — so it can name which addon CREATED any frame and which addon drives hidden Show/Hide/SetAttribute changes. It needs a UI reload and has a small performance cost, so it auto-disables again on your next reload."
		or "|cff33ff99TweakIdentifier|r\n\nStart deep investigation?\n\nThis turns on the addon's secure-driver attribution — so it can name which addon drives hidden Show/Hide/SetAttribute changes through Blizzard's state/unit drivers. It needs a UI reload, so it auto-disables again on your next reload.",
	button1 = "Reload & Start",
	button2 = CANCEL,
	OnAccept = armAndReload,
	timeout = 0, whileDead = true, hideOnEscape = true, showAlert = true,
	preferredIndex = 3,
}

---Begin deep investigation (confirm + reload if needed).
function D.Arm()
	if not D.Available() then
		ns.Print("|cffff5555Deep investigation is not available on this client.|r")
		return
	end
	if D.IsActive() then
		ns.Print("Deep investigation already active this session.")
		return
	end
	-- No-reload fast path requires BOTH halves of deep to already be live THIS session: the
	-- source-location CVar (frame-creator naming) AND Core/Drivers.lua's secure-driver capture
	-- hooks (driver-proxied Show/Hide/SetAttribute attribution). Those hooks install only at file
	-- load when deep.active was persisted true, so a CVar that merely happens to be on is NOT
	-- enough — without the hooks, driver-proxied changes stay misattributed to Blizzard. When the
	-- hooks are missing we must reload-arm (below) so the next file load installs them; reporting
	-- "active, no reload needed" off the CVar alone is the false-positive this guards against.
	-- On Classic there is no source-location CVar, so "both halves live" reduces to "driver hooks
	-- installed" — the cvarOn() requirement is dropped via `not ns.HasSourceLocation`.
	if (cvarOn() or not ns.HasSourceLocation) and ns.Drivers and ns.Drivers.Installed() then
		-- Both halves already live; do NOT take ownership (on Classic there is nothing to own).
		ns.db.deep.wasOnBefore = ns.HasSourceLocation  -- retail: CVar was already on; classic: n/a
		ns.db.deep.enabledByUs = false
		ns.db.deep.active      = true
		ns.db.deep.pending     = false
		ns.Print(ns.HasSourceLocation
			and "Source lookup and driver hooks already active — deep investigation on now (no reload needed). Frame creators are identifiable."
			or "Driver hooks already active — deep investigation on now (no reload needed). Driver-proxied changes are attributed.")
		refreshUI()
		return
	end
	StaticPopup_Show("TWEAKIDENTIFIER_DEEP_ARM")
end

---Stop the current investigation. Restores the CVar only if WE enabled it. An explicit Stop
---also clears `persist` — "off" means fully off, including permanent mode.
function D.Stop(silent)
	local d = ns.db and ns.db.deep
	if not d then return end
	local owned = d.enabledByUs and not d.wasOnBefore
	if owned then setCVar("0") end
	d.active, d.enabledByUs, d.pending, d.wasOnBefore, d.persist = false, false, false, false, false
	if not silent then
		if not ns.HasSourceLocation then
			-- Classic: no CVar; the driver hooks can't be removed mid-session but won't reinstall.
			ns.Print("Deep investigation stopped (driver attribution ends on your next reload).")
		elseif owned then
			ns.Print("Deep investigation stopped (source lookup will switch off on next reload).")
		else
			ns.Print("Deep investigation stopped (your source-lookup setting left untouched).")
		end
	end
	refreshUI()
end

function D.Toggle()
	if D.IsActive() then D.Stop() else D.Arm() end
end

---Set permanent (persist-across-reloads) mode. When turning it ON while deep is not yet live,
---this also arms deep (which may reload), so the flag is set BEFORE arming and survives.
function D.SetPersist(on)
	local d = ns.db and ns.db.deep
	if not d then return end
	if on then
		if d.persist and D.IsActive() then
			ns.Print("Deep investigation is already permanent.")
		elseif D.IsActive() then
			d.persist = true
			ns.Print("Deep investigation is now |cffffff00permanent|r — it stays enabled across reloads. |cffffff00/twid deep persist off|r to return to one-session, |cffffff00/twid deep off|r to stop.")
		else
			-- Not live yet: set the flag first, then arm (fast path or reload popup). After the
			-- reload, OnLogin sees persist and keeps deep on every session.
			d.persist = true
			ns.Print("Deep investigation set to |cffffff00permanent|r — arming it now.")
			D.Arm()
		end
	else
		if not d.persist then
			ns.Print("Deep investigation is already one-session (not permanent).")
		else
			d.persist = false
			ns.Print("Deep investigation is no longer permanent — it will auto-disable on your next reload.")
		end
	end
	refreshUI()
end

function D.TogglePersist()
	D.SetPersist(not (ns.db and ns.db.deep and ns.db.deep.persist))
end

-- Called from Core on PLAYER_LOGIN.
function D.OnLogin()
	local d = ns.db and ns.db.deep
	if not d then return end
	local remindPermanent = false
	if d.pending then
		d.pending = false
		if deepLive() then
			if d.persist then
				ns.Print(("|cffffff00Deep investigation ACTIVE (permanent)|r this session — %s. It stays on across reloads (|cffffff00/twid deep persist off|r for one-session, |cffffff00/twid deep off|r to stop)."):format(deepWhat()))
			else
				ns.Print(("|cffffff00Deep investigation ACTIVE|r this session — %s. Auto-disables on next reload (|cffffff00/twid deep off|r to stop now)."):format(deepWhat()))
			end
		else
			-- Didn't come up (Retail: CVar locked/protected; Classic: driver hooks failed to
			-- install — very unlikely). Abandon cleanly, persist too (it can't work).
			d.active, d.enabledByUs, d.persist = false, false, false
			ns.Print(ns.HasSourceLocation
				and "|cffff5555Could not enable source lookup (CVar locked).|r"
				or "|cffff5555Could not enable deep investigation (driver hooks unavailable).|r")
		end
	elseif d.active then
		if d.persist then
			-- Permanent: keep deep on across reloads (do NOT auto-disable). On Retail the CVar
			-- persists on its own; on every flavor the driver hooks re-installed at this file load
			-- (deep.active was saved true). When it came up this session, just remind, delayed.
			if deepLive() then
				remindPermanent = true
			else
				-- Unexpectedly not live (e.g. the user cleared the CVar on Retail); can't resume.
				d.active, d.enabledByUs, d.persist = false, false, false
				ns.Print(ns.HasSourceLocation
					and "|cffff5555Permanent deep investigation could not resume — source lookup is off. Run |cffffff00/twid deep|r to re-arm.|r"
					or "|cffff5555Permanent deep investigation could not resume. Run |cffffff00/twid deep|r to re-arm.|r")
			end
		else
			-- A session after the one it was armed in → auto-disable so it never lingers.
			local owned = d.enabledByUs and not d.wasOnBefore
			D.Stop(true)
			if not ns.HasSourceLocation then
				ns.Print("Deep investigation auto-disabled (one session only; driver attribution ends on next reload).")
			elseif owned then
				ns.Print("Deep investigation auto-disabled (one session only; source lookup switches off on next reload).")
			else
				ns.Print("Deep investigation ended (one session only); your source-lookup setting was left untouched.")
			end
		end
	end
	if remindPermanent then
		-- Delayed so it surfaces after the login message flood, not buried in it.
		C_Timer.After(PERSIST_HINT_DELAY, function()
			if ns.db and ns.db.deep and ns.db.deep.persist and D.IsActive() then
				ns.Print(("|cffffff00Deep investigation is PERMANENT|r — enabled across reloads, %s. |cffffff00/twid deep persist off|r for one-session, |cffffff00/twid deep off|r to stop."):format(deepWhat()))
			end
		end)
	end
end

function D.StatusText()
	local d = ns.db and ns.db.deep
	local perm = d and d.persist
	local tag = ns.HasSourceLocation and "" or " (drivers)"  -- Classic: driver-capture only
	if D.IsActive() then
		return perm and ("|cff66ff66deep: on · permanent" .. tag .. "|r") or ("|cff66ff66deep: on" .. tag .. "|r")
	elseif d and d.pending then
		return perm and "|cffffff00deep: pending reload · permanent|r" or "|cffffff00deep: pending reload|r"
	elseif perm then
		-- Flag set but not live yet (e.g. user set persist then cancelled the reload popup).
		return "|cffffaa00deep: permanent · off (reload to start)|r"
	end
	return "|cff888888deep: off|r"
end
