-- ApiVault Bug Advisor. Added in v1.1.0.
-- Classifies error records supplied as data. Does not read source files or execute code.
local addonName, ApiVault = ...

ApiVault.BugAdvisor = ApiVault.BugAdvisor or {}
local Advisor = ApiVault.BugAdvisor

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

local function findAPIs(text)
	local out, seen = {}, {}
	for namespace, name in string.gmatch(tostring(text or ""), "(C_[%w_]+)%.([%w_]+)") do
		local api = namespace .. "." .. name
		if not seen[api] then
			seen[api] = true
			table.insert(out, api)
		end
	end
	return out
end

local function findEvents(text)
	local out, seen = {}, {}
	for token in string.gmatch(tostring(text or ""), "%f[%u]([A-Z][A-Z0-9_]+)%f[%A]") do
		if string.find(token, "_", 1, true) and #token >= 5 and not seen[token] then
			seen[token] = true
			table.insert(out, token)
		end
	end
	return out
end

local SECURE_PATTERNS = {
	"SetAttribute",
	"GetAttribute",
	"RegisterStateDriver",
	"UnregisterStateDriver",
	"SecureActionButtonTemplate",
	"SecureHandler",
	"SecureUnitButtonTemplate",
	"SecureFrameTemplate",
	"InCombatLockdown",
	"hooksecurefunc",
}

local function findSecure(text)
	local out = {}
	local hay = tostring(text or "")
	for _, pattern in ipairs(SECURE_PATTERNS) do
		if string.find(hay, pattern, 1, true) then
			table.insert(out, pattern)
		end
	end
	return out
end

function Advisor:Classify(message, stack)
	local text = lower(tostring(message or "") .. "\n" .. tostring(stack or ""))
	if string.find(text, "attempt to index", 1, true) or string.find(text, "nil value", 1, true) then
		return "nil-value/default-initialization"
	end
	if string.find(text, "attempt to call", 1, true) and string.find(text, "nil", 1, true) then
		return "call-nil/missing-method-or-load-order"
	end
	if string.find(text, "bad argument", 1, true) or string.find(text, "usage:", 1, true) then
		return "bad-argument/api-signature"
	end
	if string.find(text, "action blocked", 1, true) or string.find(text, "taint", 1, true) or string.find(text, "protected", 1, true) then
		return "protected/taint"
	end
	if string.find(text, "setattribute", 1, true) or string.find(text, "incombatlockdown", 1, true) then
		return "secure-ui/combat-sensitive"
	end
	if string.find(text, "template", 1, true) and (string.find(text, "not found", 1, true) or string.find(text, "unknown", 1, true)) then
		return "xml-template/missing-template"
	end
	if string.find(text, "registerevent", 1, true) or string.find(text, "event", 1, true) then
		return "event-registration/payload"
	end
	if string.find(text, "savedvariables", 1, true) or string.find(text, "db", 1, true) then
		return "savedvariables/schema"
	end
	return "uncategorized"
end

function Advisor:BuildSuggestions(category)
	local suggestions = {
		["nil-value/default-initialization"] = {
			"Check whether the table is initialized before use.",
			"Check ADDON_LOADED order and SavedVariables defaults.",
			"Add defensive default creation before indexing nested fields.",
			"Use ApiVault's SavedVariables checklist and addon audit mode.",
		},
		["call-nil/missing-method-or-load-order"] = {
			"Check whether the function/method exists in this build and load state.",
			"Check TOC file order and dependencies.",
			"If the function belongs to another module/addon, verify that module is loaded first.",
			"Use Symbol Browser and Loaded Addon Inspector to compare source/runtime availability.",
		},
		["bad-argument/api-signature"] = {
			"Compare the call with documented ApiVault API arguments/returns.",
			"Use Test Lab with safe arguments only.",
			"Add type checks before the API call.",
			"Mark cache/context-dependent APIs as requiring in-game validation.",
		},
		["protected/taint"] = {
			"Treat this as secure-sensitive.",
			"Do not fix by forcing protected execution.",
			"Separate secure click paths from normal UI state.",
			"Test out-of-combat and in combat.",
		},
		["secure-ui/combat-sensitive"] = {
			"Create/update secure attributes only through valid secure patterns.",
			"Do not call SetAttribute automatically from ApiVault.",
			"Create secure frames out of combat where applicable.",
			"Use Secure Pattern Reference Index for inspect-only guidance.",
		},
		["xml-template/missing-template"] = {
			"Check XML Template Catalog for exact template name.",
			"Check whether the Blizzard module containing the template is loaded.",
			"Check TOC dependencies and file order.",
			"Use a safe fallback template or create a simple frame if the template is optional.",
		},
		["event-registration/payload"] = {
			"Check exact event literal spelling.",
			"Compare payloads with API documentation.",
			"Use Event Lab to record real payloads.",
			"Cap event logs and avoid registering all events.",
		},
		["savedvariables/schema"] = {
			"Check .toc SavedVariables declaration.",
			"Initialize defaults after ADDON_LOADED.",
			"Add schema version and migration guard.",
			"Do not assume existing user data has new fields.",
		},
		uncategorized = {
			"Inspect stack trace and linked symbols.",
			"Search ApiVault for referenced APIs, templates and modules.",
			"Add defensive logging around the failing code path.",
			"Mark diagnosis as UNCERTAIN until source context is available.",
		},
	}
	return suggestions[category] or suggestions.uncategorized
end

function Advisor:BuildChecklist(category)
	local base = {
		"[ ] Confirm the exact error message and first stack frame.",
		"[ ] Confirm addon load order and relevant TOC dependencies.",
		"[ ] Search ApiVault for referenced APIs/templates/modules/symbols.",
		"[ ] Check whether the issue changes after /reload.",
	}
	if category == "protected/taint" or category == "secure-ui/combat-sensitive" then
		table.insert(base, "[ ] Test out of combat.")
		table.insert(base, "[ ] Test in combat.")
		table.insert(base, "[ ] Verify no insecure UI state touches secure frames.")
	end
	if category == "bad-argument/api-signature" then
		table.insert(base, "[ ] Compare arguments with documented API metadata.")
		table.insert(base, "[ ] Test safe read-only call with controlled arguments.")
	end
	if category == "savedvariables/schema" or category == "nil-value/default-initialization" then
		table.insert(base, "[ ] Verify SavedVariables are declared and initialized.")
		table.insert(base, "[ ] Verify defaults/migrations before nested table access.")
	end
	return base
end

function Advisor:Analyze(errorRecord)
	errorRecord = errorRecord or {}
	local message = tostring(errorRecord.message or errorRecord.error or "")
	local stack = tostring(errorRecord.stack or "")
	local combined = message .. "\n" .. stack
	local category = self:Classify(message, stack)
	return {
		category = category,
		apis = findAPIs(combined),
		events = findEvents(combined),
		securePatterns = findSecure(combined),
		suggestions = self:BuildSuggestions(category),
		checklist = self:BuildChecklist(category),
		uncertainNotes = "Generated from error text only. Full source context is required for a definitive fix.",
	}
end

function Advisor:FormatAdvice(errorRecord)
	local advice = self:Analyze(errorRecord)
	local lines = {}
	table.insert(lines, "ApiVault Bug Advisor")
	table.insert(lines, "category: " .. tostring(advice.category))
	table.insert(lines, "message: " .. tostring(errorRecord and errorRecord.message or ""))
	table.insert(lines, "")
	if #advice.apis > 0 then
		table.insert(lines, "linked APIs: " .. table.concat(advice.apis, ", "))
	end
	if #advice.events > 0 then
		table.insert(lines, "linked events: " .. table.concat(advice.events, ", "))
	end
	if #advice.securePatterns > 0 then
		table.insert(lines, "linked secure patterns: " .. table.concat(advice.securePatterns, ", "))
	end
	table.insert(lines, "")
	table.insert(lines, "suggestions:")
	for _, suggestion in ipairs(advice.suggestions or {}) do
		table.insert(lines, "- " .. tostring(suggestion))
	end
	table.insert(lines, "")
	table.insert(lines, "checklist:")
	for _, item in ipairs(advice.checklist or {}) do
		table.insert(lines, item)
	end
	table.insert(lines, "")
	table.insert(lines, "UNCERTAIN: " .. tostring(advice.uncertainNotes))
	return table.concat(lines, "\n")
end


Advisor.errors = Advisor.errors or {}
Advisor.maxErrors = Advisor.maxErrors or 200
Advisor.nextId = Advisor.nextId or 1

local function clampBugString(value, limit)
	value = tostring(value or "")
	limit = limit or 12000
	if #value > limit then
		return string.sub(value, 1, limit) .. "\n<truncated by ApiVault BugAdvisor>"
	end
	return value
end

local function extractTemplates(text)
	local out, seen = {}, {}
	for token in string.gmatch(tostring(text or ""), "([A-Za-z_][A-Za-z0-9_]*Template)") do
		if not seen[token] then
			seen[token] = true
			table.insert(out, token)
		end
	end
	return out
end

local function extractModulesFromPaths(text)
	local out, seen = {}, {}
	for module in string.gmatch(tostring(text or ""), "Interface/AddOns/([^/%s:]+)/") do
		if not seen[module] then
			seen[module] = true
			table.insert(out, module)
		end
	end
	for module in string.gmatch(tostring(text or ""), "ApiVault/Examples/Interface/AddOns/([^/%s:]+)/") do
		if not seen[module] then
			seen[module] = true
			table.insert(out, module)
		end
	end
	return out
end

function Advisor:ParseStack(stack, fallbackFile, fallbackLine)
	local frames = {}
	stack = tostring(stack or "")
	for file, line in string.gmatch(stack, "([%w_%-%.%/\\]+%.lua):(%d+)") do
		table.insert(frames, {
			file = file,
			line = tonumber(line),
			module = string.match(file, "Interface/AddOns/([^/%s:]+)/") or string.match(file, "AddOns/([^/%s:]+)/") or "",
		})
		if #frames >= 20 then break end
	end
	if #frames == 0 and fallbackFile and fallbackFile ~= "" then
		table.insert(frames, {
			file = tostring(fallbackFile),
			line = tonumber(fallbackLine),
			module = string.match(tostring(fallbackFile), "Interface/AddOns/([^/%s:]+)/") or string.match(tostring(fallbackFile), "AddOns/([^/%s:]+)/") or "",
		})
	end
	return frames
end

function Advisor:ResolveLinks(errorRecord, analysis)
	errorRecord = errorRecord or {}
	analysis = analysis or {}
	local text = tostring(errorRecord.message or "") .. "\n" .. tostring(errorRecord.stack or "") .. "\n" .. tostring(errorRecord.file or "") .. "\n" .. tostring(errorRecord.context or "")
	local links = {
		apis = analysis.apis or findAPIs(text),
		events = analysis.events or findEvents(text),
		securePatterns = analysis.securePatterns or findSecure(text),
		templates = extractTemplates(text),
		modules = extractModulesFromPaths(text),
		stackFrames = self:ParseStack(errorRecord.stack, errorRecord.file, errorRecord.line),
	}
	links.summary = {
		apis = #links.apis,
		events = #links.events,
		securePatterns = #links.securePatterns,
		templates = #links.templates,
		modules = #links.modules,
		stackFrames = #links.stackFrames,
	}
	return links
end

function Advisor:BuildFixChecklist(category, links)
	local checklist = self:BuildChecklist(category)
	links = links or {}
	if links.summary and links.summary.apis and links.summary.apis > 0 then
		table.insert(checklist, "[ ] Open ApiVault API detail for linked API names.")
	end
	if links.summary and links.summary.templates and links.summary.templates > 0 then
		table.insert(checklist, "[ ] Open XML Template Catalog for linked template names.")
	end
	if links.summary and links.summary.securePatterns and links.summary.securePatterns > 0 then
		table.insert(checklist, "[ ] Open Secure Pattern Reference and verify inspect-only policy.")
	end
	if links.summary and links.summary.stackFrames and links.summary.stackFrames > 0 then
		table.insert(checklist, "[ ] Inspect the first stack frame and confirm whether line number matches current source.")
	end
	return checklist
end

function Advisor:IngestError(errorRecord)
	errorRecord = errorRecord or {}
	local record = {
		id = self.nextId or 1,
		addon = clampBugString(errorRecord.addon or errorRecord.addonName or errorRecord.owner or "UNKNOWN_ADDON", 256),
		message = clampBugString(errorRecord.message or errorRecord.error or "unknown error", 12000),
		stack = clampBugString(errorRecord.stack or "", 24000),
		source = clampBugString(errorRecord.source or "External", 256),
		file = clampBugString(errorRecord.file or "", 1024),
		line = tonumber(errorRecord.line) or nil,
		context = clampBugString(errorRecord.context or "", 2000),
		time = errorRecord.timestamp or (GetTime and GetTime() or 0),
		wallTime = errorRecord.wallTime or (date and date("%Y-%m-%d %H:%M:%S") or tostring(GetTime and GetTime() or 0)),
	}
	self.nextId = record.id + 1

	record.advice = self:Analyze(record)
	record.links = self:ResolveLinks(record, record.advice)
	record.fixChecklist = self:BuildFixChecklist(record.advice.category, record.links)
	record.copyableReport = self:FormatBugReport(record)

	if ApiVault.ErrorInbox and ApiVault.ErrorInbox.Ingest and not record._fromErrorInbox then
		ApiVault.ErrorInbox:Ingest(record)
	end
	table.insert(self.errors, 1, record)
	while #self.errors > (self.maxErrors or 200) do
		table.remove(self.errors)
	end
	return record, "ingested error #" .. tostring(record.id)
end

function Advisor:GetErrors()
	return self.errors or {}
end

function Advisor:GetErrorById(id)
	id = tonumber(id)
	for _, record in ipairs(self.errors or {}) do
		if record.id == id then return record end
	end
end

function Advisor:SearchErrors(query, limit)
	query = lower(query or "")
	limit = limit or 100
	local results = {}
	for _, record in ipairs(self.errors or {}) do
		local hay = lower((record.addon or "") .. " " .. (record.message or "") .. " " .. (record.stack or "") .. " " .. (record.source or ""))
		if query == "" or string.find(hay, query, 1, true) then
			table.insert(results, record)
			if #results >= limit then break end
		end
	end
	return results
end

function Advisor:FormatBugReport(record)
	if not record then
		return "No bug record selected."
	end
	local advice = record.advice or self:Analyze(record)
	local links = record.links or self:ResolveLinks(record, advice)
	local checklist = record.fixChecklist or self:BuildFixChecklist(advice.category, links)
	local lines = {}
	table.insert(lines, "ApiVault Bug Report")
	table.insert(lines, "id: " .. tostring(record.id or ""))
	table.insert(lines, "addon: " .. tostring(record.addon or ""))
	table.insert(lines, "source: " .. tostring(record.source or ""))
	table.insert(lines, "category: " .. tostring(advice.category or ""))
	table.insert(lines, "message: " .. tostring(record.message or ""))
	table.insert(lines, "file: " .. tostring(record.file or ""))
	table.insert(lines, "line: " .. tostring(record.line or ""))
	table.insert(lines, "time: " .. tostring(record.wallTime or ""))
	table.insert(lines, "")
	if links.summary then
		table.insert(lines, "linked counts: APIs=" .. tostring(links.summary.apis) .. ", events=" .. tostring(links.summary.events) .. ", templates=" .. tostring(links.summary.templates) .. ", secure=" .. tostring(links.summary.securePatterns) .. ", modules=" .. tostring(links.summary.modules))
	end
	if links.apis and #links.apis > 0 then table.insert(lines, "linked APIs: " .. table.concat(links.apis, ", ")) end
	if links.events and #links.events > 0 then table.insert(lines, "linked events: " .. table.concat(links.events, ", ")) end
	if links.templates and #links.templates > 0 then table.insert(lines, "linked templates: " .. table.concat(links.templates, ", ")) end
	if links.modules and #links.modules > 0 then table.insert(lines, "linked modules: " .. table.concat(links.modules, ", ")) end
	if links.securePatterns and #links.securePatterns > 0 then table.insert(lines, "linked secure patterns: " .. table.concat(links.securePatterns, ", ")) end
	table.insert(lines, "")
	table.insert(lines, "suggested fixes:")
	for _, suggestion in ipairs(advice.suggestions or {}) do
		table.insert(lines, "- " .. tostring(suggestion))
	end
	table.insert(lines, "")
	table.insert(lines, "checklist:")
	for _, item in ipairs(checklist or {}) do
		table.insert(lines, tostring(item))
	end
	table.insert(lines, "")
	table.insert(lines, "stack:")
	table.insert(lines, tostring(record.stack or ""))
	table.insert(lines, "")
	table.insert(lines, "UNCERTAIN: Generated from error text and stack metadata. Confirm against the addon source before changing code.")
	return table.concat(lines, "\n")
end

function Advisor:FormatErrorList(query)
	local list = self:SearchErrors(query or "", 100)
	local lines = {}
	table.insert(lines, "ApiVault Bug Advisor - Ingested Errors")
	table.insert(lines, "count: " .. tostring(#(self.errors or {})))
	table.insert(lines, "filtered: " .. tostring(#list))
	table.insert(lines, "")
	for _, record in ipairs(list) do
		local category = record.advice and record.advice.category or "uncategorized"
		table.insert(lines, string.format("#%s [%s] %s — %s", tostring(record.id), tostring(record.source), tostring(category), tostring(record.message)))
	end
	if #list == 0 then
		table.insert(lines, "No errors ingested yet. Use ApiVault.BugAdvisor:IngestError({...}) or /av bugadvisor sample.")
	end
	return table.concat(lines, "\n")
end

function Advisor:GetStats()
	return 8, #SECURE_PATTERNS, #(self.errors or {})
end
