-- Core/Attribution.lua
-- Maps execution / frames to the responsible addon.
--
-- Two complementary mechanisms:
--   * RUNTIME alterations (who is calling SetPoint/Hide/… right now)  → debugstack
--     parsing inside the secure post-hook (see Monitor).
--   * CREATION (who built this frame)                                → the engine's
--     own frame:GetSourceLocation(), gated by the enableSourceLocationLookup CVar
--     (see Core/Deep.lua). This is exact and works retroactively on any frame.
--
-- In the live client source paths use BACKSLASHES; named chunks render with an "@"
-- prefix and [string "@Interface\AddOns\<Folder>\<File>.lua"]:NN. GetSourceLocation
-- returns the "@Interface/AddOns/..." form (forward slashes on Mac). One normalizer
-- + case-insensitive pattern covers every form.

local ADDON, ns = ...

local debugstack = debugstack
local strmatch   = string.match
local strgmatch  = string.gmatch
local MY_FOLDER  = ADDON

-- ---------------------------------------------------------------------------
-- Addon folder -> Title cache (built once, lazily, after addons are known)
-- ---------------------------------------------------------------------------

local addonCache, cacheBuilt = {}, false

local function buildCache()
	if cacheBuilt then return end
	local getCount = (C_AddOns and C_AddOns.GetNumAddOns) or GetNumAddOns
	local getInfo  = (C_AddOns and C_AddOns.GetAddOnInfo)  or GetAddOnInfo
	if not (getCount and getInfo) then return end
	local n = getCount() or 0
	for i = 1, n do
		local ok, folder, title = pcall(getInfo, i)
		if ok and folder then
			addonCache[folder:lower()] = { folder = folder, title = (title and title ~= "") and title or folder }
		end
	end
	cacheBuilt = true
end

function ns.AddonTitle(folder)
	if not folder then return nil end
	buildCache()
	local e = addonCache[folder:lower()]
	return e and e.title or folder
end

---Resolve a taint string from `issecurevariable` (the addon that wrote a field) to a real addon.
---The engine reports either the folder or the Title; match both (Title may carry |c colour codes).
---Returns {folder, title} for a known addon, or nil if it matches none — callers must NOT persist an
---unresolved taint string as evidence (it could be a macro "", a renamed value, or non-folder text).
function ns.ResolveTaint(taintName)
	if type(taintName) ~= "string" or taintName == "" then return nil end
	buildCache()
	local low = taintName:lower()
	local e = addonCache[low]
	if e then return { folder = e.folder, title = e.title } end
	for _, ent in pairs(addonCache) do
		local title = ent.title
		if title then
			if title:lower() == low then return { folder = ent.folder, title = title } end
			local stripped = title:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
			if stripped:lower() == low then return { folder = ent.folder, title = title } end
		end
	end
	return nil
end

local function isBlizzard(folder)
	return folder ~= nil and strmatch(folder, "^[Bb]lizzard_") ~= nil
end
ns.IsBlizzardFolder = isBlizzard

-- ---------------------------------------------------------------------------
-- Source-location creator attribution (frame:GetSourceLocation)
-- ---------------------------------------------------------------------------

local function normalizePath(loc)
	if type(loc) ~= "string" then return nil end
	return (loc:gsub("^@", ""):gsub("/", "\\"))
end
ns.NormalizePath = normalizePath

---Parse a source-location string into {type, folder, title, file, line, raw}.
---type is "addon" | "blizzard" | "unknown".
function ns.ParseLocation(loc)
	local norm = normalizePath(loc)
	if not norm then return nil end
	local folder, file, line = norm:match("[Ii]nterface\\[Aa]dd[Oo]ns\\([^\\]+)\\(.+):(%d+)")
	if folder then
		if isBlizzard(folder) then
			return { type = "blizzard", folder = folder, title = "Blizzard UI", file = file, line = line, raw = loc }
		end
		return { type = "addon", folder = folder, title = ns.AddonTitle(folder), file = file, line = line, raw = loc }
	end
	return { type = "unknown", raw = loc }
end

---Creator of a frame via the engine. Returns a ParseLocation table or nil.
---Requires deep mode (enableSourceLocationLookup) to be on; otherwise the engine
---returns nothing. Secret-safe.
function ns.SourceOf(frame)
	if ns.IsSecret(frame) or type(frame) ~= "table" or type(frame.GetSourceLocation) ~= "function" then return nil end
	local ok, loc = pcall(frame.GetSourceLocation, frame)
	if not ok or loc == nil or ns.IsSecret(loc) or type(loc) ~= "string" or loc == "" then return nil end
	return ns.ParseLocation(loc)
end

---Fallback: guess the addon from a frame's name prefix (longest cache match).
function ns.GuessFromName(name)
	if type(name) ~= "string" or name == "" then return nil end
	buildCache()
	local low, best, bestLen = name:lower(), nil, 0
	for key, e in pairs(addonCache) do
		if #key > bestLen and low:sub(1, #key) == key then
			best, bestLen = e.title, #key
		end
	end
	return best
end

-- ---------------------------------------------------------------------------
-- Runtime caller attribution (debugstack)
-- ---------------------------------------------------------------------------

local ADDON_PATTERN = "[Aa][Dd][Dd][Oo][Nn][Ss][\\/]([^\\/]+)"

local function folderFromLine(line)
	return strmatch(line, ADDON_PATTERN)
end
ns.FolderFromLine = folderFromLine

---Identify the caller from inside a hooksecurefunc callback.
---Returns: culpritFolder, culpritTitle, externalFolder, externalTitle, chain, stack
---Our own hook/capture frames are always the top of the stack — pure boilerplate that
---confuses the reader — so they are dropped from BOTH the returned stack string and the
---chain. The displayed stack then starts at the real boundary ([C] method → caller).
function ns.GetCaller(maxTop)
	local raw = debugstack(2, maxTop or 30, 0) or ""
	local culprit, culpritTitle, ext, extTitle
	local chain, seen, kept = {}, {}, {}
	for line in strgmatch(raw, "[^\r\n]+") do
		local folder = folderFromLine(line)
		if folder == MY_FOLDER then
			-- our hook/capture machinery — never part of the answer; drop it entirely.
		else
			kept[#kept + 1] = line
			if folder and not seen[folder] then
				seen[folder] = true
				local title = ns.AddonTitle(folder)
				local bliz  = isBlizzard(folder)
				chain[#chain + 1] = { folder = folder, title = title, blizzard = bliz }
				if not culprit then culprit, culpritTitle = folder, title end
				if not ext and not bliz then ext, extTitle = folder, title end
			end
		end
	end
	return culprit, culpritTitle, ext, extTitle, chain, table.concat(kept, "\n")
end
