-- ApiVault Addon Audit Mode. Added in v0.6.0.
-- Metadata/checklist scanner only. No runtime source file reads and no automatic code rewriting.
local addonName, ApiVault = ...

ApiVault.AddonAudit = ApiVault.AddonAudit or {}
local Audit = ApiVault.AddonAudit

Audit.metadata = {
	version = "0.6.0",
	interface = 120007,
	policy = "paste-based metadata audit; no runtime file reads; no code rewrite; no protected/secure execution",
	rules = 18,
}

local SEVERITY_SCORE = { critical = 1, high = 2, medium = 3, low = 4, info = 5 }

local function lower(value)
	return string.lower(tostring(value or ""))
end

local function contains(haystack, needle)
	haystack = lower(haystack)
	needle = lower(needle)
	return needle == "" or string.find(haystack, needle, 1, true) ~= nil
end

local function splitLines(text)
	local lines = {}
	text = tostring(text or "")
	text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
	text = text .. "\n"
	for line in string.gmatch(text, "(.-)\n") do
		table.insert(lines, line)
	end
	return lines
end

local function trim(value)
	value = tostring(value or "")
	return (value:gsub("^%s+", ""):gsub("%s+$", ""))
end

function Audit:ResetLookups()
	self.apiByName = nil
	self.eventByName = nil
end

function Audit:EnsureLookups()
	if self.apiByName and self.eventByName then
		return
	end
	self.apiByName = {}
	self.eventByName = {}
	if ApiVault.Index and ApiVault.Index.Build then
		local entries = ApiVault.Index:Build() or {}
		for _, entry in ipairs(entries) do
			if entry.kind == "Function" then
				self.apiByName[entry.display] = entry
			elseif entry.kind == "Event" then
				self.eventByName[entry.display] = entry
			end
		end
	end
end

function Audit:GetStats()
	local count = (self.metadata and self.metadata.rules) or 0
	local last = self.lastReport and #(self.lastReport.issues or {}) or 0
	return count, last, self.lastReport and self.lastReport.fileType or "none"
end

function Audit:DetectFileType(text, explicit)
	if explicit and explicit ~= "auto" and explicit ~= "Auto" then
		return lower(explicit)
	end
	text = tostring(text or "")
	if string.find(text, "##%s*Interface%s*:") or string.find(text, "##%s*Title%s*:") then
		return "toc"
	end
	if string.find(text, "<Ui") or string.find(text, "<Frame") or string.find(text, "<Button") or string.find(text, "<Script") then
		return "xml"
	end
	return "lua"
end

function Audit:AddIssue(report, severity, category, line, title, detail, recommendation, sourceBacked)
	table.insert(report.issues, {
		severity = severity or "info",
		category = category or "general",
		line = line or 0,
		title = title or "Audit note",
		detail = detail or "",
		recommendation = recommendation or "Review manually.",
		sourceBacked = sourceBacked ~= false,
	})
	report.counts[severity or "info"] = (report.counts[severity or "info"] or 0) + 1
end

function Audit:CheckAPICall(report, apiName, lineNumber)
	self:EnsureLookups()
	report.apiCalls[apiName] = (report.apiCalls[apiName] or 0) + 1
	local entry = self.apiByName and self.apiByName[apiName]
	if not entry then
		self:AddIssue(report, "medium", "api-compatibility", lineNumber,
			"API not found in documented API index",
			apiName .. " was referenced but was not found in Blizzard_APIDocumentationGenerated for this ApiVault build.",
			"Verify the API name against the target Interface build or add a fallback/check before calling it.", true)
		return
	end
	if entry.risk == "protected" or entry.risk == "restricted" or entry.risk == "review" then
		self:AddIssue(report, entry.risk == "protected" and "critical" or "high", "api-risk", lineNumber,
			"API requires manual review: " .. apiName,
			"ApiVault risk classification: " .. tostring(entry.risk) .. " / " .. tostring(entry.riskReason),
			"Do not assume this call is safe in combat or without the correct runtime context. Test OOC/combat as appropriate.", true)
	end
end

function Audit:CheckEvent(report, eventName, lineNumber, unitEvent)
	self:EnsureLookups()
	report.events[eventName] = (report.events[eventName] or 0) + 1
	if not (self.eventByName and self.eventByName[eventName]) then
		self:AddIssue(report, "low", "event", lineNumber,
			"Event not found in documented event index",
			tostring(eventName) .. " was registered but was not found in the generated event documentation.",
			"Verify spelling and runtime availability. Some internal or dynamic events may be undocumented; mark as UNCERTAIN until tested.", true)
	end
	if unitEvent then
		self:AddIssue(report, "info", "event", lineNumber,
			"RegisterUnitEvent detected",
			"Event " .. tostring(eventName) .. " is registered with unit filtering.",
			"Verify the unit token list and expected payload in-game.", true)
	end
end

function Audit:CheckTemplate(report, templateName, lineNumber)
	report.templates[templateName] = (report.templates[templateName] or 0) + 1
	if ApiVault.XMLCatalog and ApiVault.XMLCatalog.GetTemplate then
		local template = ApiVault.XMLCatalog:GetTemplate(templateName)
		if not template then
			self:AddIssue(report, "medium", "xml-template", lineNumber,
				"Template not found in XML catalog",
				tostring(templateName) .. " was referenced as a template but not found in the generated XML Template Catalog.",
				"Verify template name, dependencies and LoadOnDemand module availability.", true)
		end
	else
		self:AddIssue(report, "info", "xml-template", lineNumber,
			"XML catalog unavailable",
			"Template reference " .. tostring(templateName) .. " could not be checked because XMLCatalog is unavailable.",
			"Re-test after the XML catalog is loaded.", false)
	end
end

function Audit:ScanTOCLine(report, line, lineNumber)
	local key, value = string.match(line, "^##%s*([^:]+)%s*:%s*(.-)%s*$")
	if not key then return end
	key = trim(key)
	value = trim(value)
	report.toc[key] = value
	local keyLower = lower(key)
	if keyLower == "interface" then
		if tonumber(value) ~= 120007 then
			self:AddIssue(report, "high", "toc", lineNumber,
				"TOC Interface does not match target",
				"Found Interface: " .. tostring(value) .. "; ApiVault target is 120007.",
				"Use Interface: 120007 for this Midnight target unless intentionally building for another client.", true)
		end
	elseif keyLower == "savedvariables" or keyLower == "savedvariablespercharacter" then
		self:AddIssue(report, "info", "savedvariables", lineNumber,
			"SavedVariables declaration detected",
			key .. ": " .. value,
			"Verify defaults, validation and migration/rollback behavior.", true)
	elseif keyLower == "dependencies" or keyLower == "requireddep" or keyLower == "optionaldeps" then
		self:AddIssue(report, "info", "toc-dependency", lineNumber,
			"TOC dependency metadata detected",
			key .. ": " .. value,
			"Verify dependency/load order in-game. Behavior is UNCERTAIN unless the target modules are present and loaded.", true)
	elseif keyLower == "usesecureenvironment" or keyLower == "secure" then
		self:AddIssue(report, "high", "toc-secure", lineNumber,
			"Secure TOC metadata detected",
			key .. ": " .. value,
			"Review secure environment implications. Do not mix insecure UI updates into protected paths.", true)
	end
end

function Audit:ScanLuaLine(report, line, lineNumber)
	for namespace, method in string.gmatch(line, "(C_[%w_]+)%.([%w_]+)%s*%(") do
		self:CheckAPICall(report, namespace .. "." .. method, lineNumber)
	end
	for eventName in string.gmatch(line, "RegisterEvent%s*%(%s*[\"']([A-Z0-9_]+)[\"']") do
		self:CheckEvent(report, eventName, lineNumber, false)
	end
	for eventName in string.gmatch(line, "RegisterUnitEvent%s*%(%s*[\"']([A-Z0-9_]+)[\"']") do
		self:CheckEvent(report, eventName, lineNumber, true)
	end
	for templateName in string.gmatch(line, "CreateFrame%s*%([^,]+,[^,]*,[^,]*,%s*[\"']([^\"']+)[\"']") do
		self:CheckTemplate(report, templateName, lineNumber)
	end
	if string.find(line, "SetAttribute", 1, true) then
		self:AddIssue(report, "critical", "secure-ui", lineNumber, "SetAttribute detected", "SetAttribute is secure-sensitive and can taint protected UI paths.", "Only set secure attributes in a verified OOC/user-driven secure pattern. Test OOC and in combat.", true)
	end
	if string.find(line, "RegisterStateDriver", 1, true) or string.find(line, "UnregisterStateDriver", 1, true) then
		self:AddIssue(report, "high", "secure-ui", lineNumber, "State driver usage detected", "State driver changes are secure-sensitive.", "Verify combat behavior and secure template use.", true)
	end
	if string.find(line, "SecureActionButtonTemplate", 1, true) or string.find(line, "SecureHandler", 1, true) then
		self:AddIssue(report, "high", "secure-ui", lineNumber, "Secure template reference detected", "Secure templates require a strict secure/insecure boundary.", "Keep normal UI state away from secure click paths. Test OOC/combat.", true)
	end
	if string.find(line, "InCombatLockdown", 1, true) then
		self:AddIssue(report, "info", "combat", lineNumber, "Combat lockdown check detected", "Code checks combat lockdown.", "Verify every protected mutation is gated and has an OOC retry path if needed.", true)
	end
	if string.find(line, "SetScript%s*%(%s*[\"']OnUpdate[\"']") or string.find(line, "OnUpdate", 1, true) then
		self:AddIssue(report, "medium", "performance", lineNumber, "OnUpdate usage detected", "OnUpdate can be expensive if it runs continuously.", "Throttle work, avoid allocations, and disable the handler when idle.", true)
	end
	if string.find(line, "C_Timer.NewTicker", 1, true) then
		self:AddIssue(report, "medium", "performance", lineNumber, "C_Timer.NewTicker detected", "Repeating timers can accumulate work.", "Store/cancel ticker handles and avoid high-frequency polling.", true)
	end
	if string.find(line, "loadstring", 1, true) or string.find(line, "RunScript", 1, true) then
		self:AddIssue(report, "critical", "execution", lineNumber, "Dynamic code execution pattern detected", "Dynamic code execution is unsafe for addon audit targets.", "Remove or strictly isolate this path. Do not use it for user input.", true)
	end
end

function Audit:ScanXMLLine(report, line, lineNumber)
	for templateName in string.gmatch(line, "inherits%s*=%s*[\"']([^\"']+)[\"']") do
		self:CheckTemplate(report, templateName, lineNumber)
	end
	for scriptName in string.gmatch(line, "<(%w+)%s*>") do
		if string.sub(scriptName, 1, 2) == "On" then
			report.xmlScripts[scriptName] = (report.xmlScripts[scriptName] or 0) + 1
			if scriptName == "OnUpdate" then
				self:AddIssue(report, "medium", "performance", lineNumber, "XML OnUpdate handler detected", "OnUpdate can be expensive when always active.", "Throttle work and ensure the handler is disabled when not needed.", true)
			end
		end
	end
	if string.find(line, "SecureActionButtonTemplate", 1, true) or string.find(line, "SecureHandler", 1, true) then
		self:AddIssue(report, "high", "secure-ui", lineNumber, "Secure XML template reference detected", "Secure XML templates are inspect-only in ApiVault.", "Verify secure/insecure boundaries and combat behavior in-game.", true)
	end
end

function Audit:ScanText(text, fileType, label)
	local detectedType = self:DetectFileType(text, fileType or "auto")
	local report = {
		label = label or "Pasted addon text",
		fileType = detectedType,
		generatedAt = type(date) == "function" and date("%Y-%m-%d %H:%M:%S") or "runtime",
		policy = "metadata/checklist audit only; no code rewrite; no runtime file reads",
		issues = {},
		counts = {},
		apiCalls = {},
		events = {},
		templates = {},
		xmlScripts = {},
		toc = {},
	}
	local lines = splitLines(text)
	report.lineCount = #lines
	for index, line in ipairs(lines) do
		if detectedType == "toc" then
			self:ScanTOCLine(report, line, index)
		elseif detectedType == "xml" then
			self:ScanXMLLine(report, line, index)
		else
			self:ScanLuaLine(report, line, index)
		end
	end
	if #report.issues == 0 then
		self:AddIssue(report, "info", "summary", 0, "No issues found by current rules", "The current paste did not match high-risk ApiVault audit rules.", "Still test in-game and review manually; this is not proof of correctness.", true)
	end
	table.sort(report.issues, function(left, right)
		local ls = SEVERITY_SCORE[left.severity] or 9
		local rs = SEVERITY_SCORE[right.severity] or 9
		if ls == rs then return (left.line or 0) < (right.line or 0) end
		return ls < rs
	end)
	self.lastReport = report
	return report
end

local function countMap(map)
	local count = 0
	for _ in pairs(map or {}) do count = count + 1 end
	return count
end

local function appendMapSummary(lines, label, map, maxItems)
	local values = {}
	for key, count in pairs(map or {}) do table.insert(values, { key = key, count = count }) end
	table.sort(values, function(a, b) return tostring(a.key) < tostring(b.key) end)
	table.insert(lines, label .. ": " .. tostring(#values))
	for index, item in ipairs(values) do
		if index > (maxItems or 20) then
			table.insert(lines, "  ...")
			break
		end
		table.insert(lines, "  - " .. tostring(item.key) .. " (" .. tostring(item.count) .. ")")
	end
end

function Audit:FormatReport(report)
	report = report or self.lastReport
	if not report then
		return "No audit report available."
	end
	local lines = {}
	table.insert(lines, "ApiVault Addon Audit Report")
	table.insert(lines, "===========================")
	table.insert(lines, "label: " .. tostring(report.label))
	table.insert(lines, "fileType: " .. tostring(report.fileType))
	table.insert(lines, "lineCount: " .. tostring(report.lineCount or 0))
	table.insert(lines, "policy: " .. tostring(report.policy))
	table.insert(lines, "")
	table.insert(lines, "Issue summary:")
	for _, severity in ipairs({ "critical", "high", "medium", "low", "info" }) do
		table.insert(lines, "- " .. severity .. ": " .. tostring(report.counts[severity] or 0))
	end
	table.insert(lines, "")
	appendMapSummary(lines, "Documented C_* API references", report.apiCalls, 30)
	appendMapSummary(lines, "Event registrations", report.events, 30)
	appendMapSummary(lines, "Template references", report.templates, 30)
	appendMapSummary(lines, "XML script handlers", report.xmlScripts, 30)
	if report.fileType == "toc" then
		table.insert(lines, "TOC metadata:")
		for key, value in pairs(report.toc or {}) do
			table.insert(lines, "  - " .. tostring(key) .. ": " .. tostring(value))
		end
	end
	table.insert(lines, "")
	table.insert(lines, "Issues:")
	for index, issue in ipairs(report.issues or {}) do
		table.insert(lines, tostring(index) .. ". [" .. tostring(issue.severity) .. "] " .. tostring(issue.category) .. " line " .. tostring(issue.line or 0) .. " — " .. tostring(issue.title))
		if issue.detail and issue.detail ~= "" then table.insert(lines, "   detail: " .. tostring(issue.detail)) end
		if issue.recommendation and issue.recommendation ~= "" then table.insert(lines, "   recommendation: " .. tostring(issue.recommendation)) end
	end
	table.insert(lines, "")
	table.insert(lines, "Notes:")
	table.insert(lines, "- This report is metadata/checklist-based and does not prove runtime correctness.")
	table.insert(lines, "- Secure/protected behavior must be tested in-game OOC and in combat where relevant.")
	table.insert(lines, "- UNCERTAIN findings require manual verification against uploaded source/API metadata.")
	return table.concat(lines, "\n")
end

function Audit:GetLastReportText()
	return self:FormatReport(self.lastReport)
end
