-- ApiVault SavedVariables Schema Inspector. Rebuilt in v1.3.3.
-- Read-only runtime inspector. Does not mutate external addon databases.
-- Important: C_AddOns.GetAddOnMetadata(addon, "SavedVariables") is not authoritative.
-- SavedVariables directives are often not exposed as runtime metadata keys.
local addonName, ApiVault = ...

ApiVault.SavedVariablesSchemaInspector = ApiVault.SavedVariablesSchemaInspector or {}
local Inspector = ApiVault.SavedVariablesSchemaInspector

Inspector.knownDBs = Inspector.knownDBs or {
	ApiVault = { "ApiVaultDB" },
	StockVault = { "StockVaultDB", "StockVaultCharDB" },
	MailVault = { "MailVaultDB", "MailVaultCharDB" },
}

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

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

local function addonRuntime()
	return ApiVault.AddOnRuntime
end

local function addonCount()
	if addonRuntime() then
		return addonRuntime():GetNumAddOns()
	end
	return 0
end

local function addonInfo(token)
	if addonRuntime() then
		return addonRuntime():GetAddOnInfo(token)
	end
	return nil
end

local function metadataByToken(token, key)
	if addonRuntime() then
		return addonRuntime():GetAddOnMetadata(token, key)
	end
	return nil
end

local function findAddon(query)
	query = trim(query)
	if query == "" then return nil end

	local name, title, notes, loadable, reason, security, newVersion = addonInfo(query)
	if name then
		return {
			name = name,
			title = title,
			notes = notes,
			loadable = loadable,
			reason = reason,
			security = security,
			newVersion = newVersion,
			source = "direct",
		}
	end

	local needle = lower(query)
	local count = addonCount()
	for index = 1, count do
		name, title, notes, loadable, reason, security, newVersion = addonInfo(index)
		if name then
			local hay = lower(tostring(name) .. " " .. tostring(title or ""))
			if lower(name) == needle or string.find(hay, needle, 1, true) then
				return {
					index = index,
					name = name,
					title = title,
					notes = notes,
					loadable = loadable,
					reason = reason,
					security = security,
					newVersion = newVersion,
					source = "scan",
				}
			end
		end
	end
	return nil
end

local function addUnique(list, seen, value)
	value = trim(value)
	if value ~= "" and not seen[value] then
		seen[value] = true
		table.insert(list, value)
	end
end

local function sanitizedName(value)
	value = tostring(value or "")
	value = value:gsub("[^%w_]", "")
	return value
end

function Inspector:GetKnownDBNames(query, addonName)
	local out, seen = {}, {}
	query = trim(query)
	addonName = trim(addonName or query)

	if self.knownDBs[addonName] then
		for _, name in ipairs(self.knownDBs[addonName]) do
			addUnique(out, seen, name)
		end
	end

	-- Case-insensitive match for known mappings.
	local needle = lower(addonName)
	for knownAddon, names in pairs(self.knownDBs or {}) do
		if lower(knownAddon) == needle then
			for _, name in ipairs(names) do
				addUnique(out, seen, name)
			end
		end
	end

	-- If the user typed an actual DB global, preserve it.
	if query ~= "" and (string.find(query, "DB", 1, true) or string.find(query, "Saved", 1, true)) then
		addUnique(out, seen, query)
	end

	local base = sanitizedName(addonName)
	if base ~= "" then
		addUnique(out, seen, base .. "DB")
		addUnique(out, seen, base .. "CharDB")
		addUnique(out, seen, base .. "CharacterDB")
		addUnique(out, seen, base .. "SavedVariables")
	end

	return out
end

local function makeRuntimeEntry(globalName, source)
	local value = _G[globalName]
	local valueType = type(value)
	return {
		name = globalName,
		source = source or "runtime-probe",
		exists = value ~= nil,
		type = valueType,
		hasSchemaVersion = valueType == "table" and (value.schemaVersion ~= nil or value.version ~= nil) or false,
		schemaVersion = valueType == "table" and (value.schemaVersion or value.version) or nil,
	}
end

function Inspector:GetDeveloperRecord(addonName)
	if ApiVault.DeveloperAPI and ApiVault.DeveloperAPI.GetAddonRecord then
		local exact = ApiVault.DeveloperAPI:GetAddonRecord(addonName)
		if exact then return exact, addonName end
		local needle = lower(addonName)
		if type(ApiVault.DeveloperAPI.addons) == "table" then
			for name, record in pairs(ApiVault.DeveloperAPI.addons) do
				if lower(name) == needle then
					return record, name
				end
			end
		end
	end
	return nil
end

function Inspector:GetPublicReports(addonName)
	local record = self:GetDeveloperRecord(addonName)
	if record and type(record.savedVariables) == "table" then
		return record.savedVariables
	end
	return {}
end

function Inspector:BuildRuntimeProbeRecord(query)
	query = trim(query)
	local addon = findAddon(query)
	local addonName = addon and addon.name or query
	local version = addonName ~= "" and metadataByToken(addonName, "Version") or nil
	local author = addonName ~= "" and metadataByToken(addonName, "Author") or nil
	local interface = addonName ~= "" and metadataByToken(addonName, "Interface") or nil

	-- Optional/diagnostic only; not authoritative.
	local exposedSV = addonName ~= "" and metadataByToken(addonName, "SavedVariables") or nil
	local exposedSVPC = addonName ~= "" and metadataByToken(addonName, "SavedVariablesPerCharacter") or nil

	local names = self:GetKnownDBNames(query, addonName)
	local runtime = {}
	for _, globalName in ipairs(names) do
		table.insert(runtime, makeRuntimeEntry(globalName, "runtime-global-probe"))
	end

	local reports = addonName ~= "" and self:GetPublicReports(addonName) or {}

	return {
		query = query,
		addon = addonName,
		addonInfo = addon,
		version = version,
		author = author,
		interface = interface,
		exposedSavedVariables = exposedSV,
		exposedSavedVariablesPerCharacter = exposedSVPC,
		inferredNames = names,
		runtime = runtime,
		publicReports = reports,
		expectedFile = addonName ~= "" and ("WTF/Account/<Account>/SavedVariables/" .. tostring(addonName) .. ".lua") or "",
	}
end

function Inspector:BuildKnownOverview()
	local rows = {}
	for addonName in pairs(self.knownDBs or {}) do
		local record = self:BuildRuntimeProbeRecord(addonName)
		local hasEvidence = false
		for _, entry in ipairs(record.runtime or {}) do
			if entry.exists then hasEvidence = true end
		end
		if #(record.publicReports or {}) > 0 then hasEvidence = true end
		if hasEvidence or addonName == "ApiVault" then
			table.insert(rows, record)
		end
	end
	return rows
end

function Inspector:AppendRuntimeEntry(lines, entry, indent)
	indent = indent or "  "
	table.insert(lines, indent .. tostring(entry.name) .. " exists=" .. tostring(entry.exists) .. " type=" .. tostring(entry.type) .. " schemaVersion=" .. tostring(entry.schemaVersion or entry.hasSchemaVersion) .. " source=" .. tostring(entry.source))
end

function Inspector:AppendPublicReport(lines, report, index)
	table.insert(lines, "  Report #" .. tostring(index))
	table.insert(lines, "    name=" .. tostring(report.name or report.dbName or "unknown"))
	table.insert(lines, "    initialized=" .. tostring(report.initialized))
	table.insert(lines, "    scope=" .. tostring(report.scope or "unknown"))
	table.insert(lines, "    schemaVersion=" .. tostring(report.schemaVersion or report.version or ""))
	table.insert(lines, "    source=" .. tostring(report.source or "Public Developer API"))
	table.insert(lines, "    time=" .. tostring(report.wallTime or report.time or ""))
	if report.expectedFile then
		table.insert(lines, "    expectedFile=" .. tostring(report.expectedFile))
	end
end

function Inspector:BuildChecklist(record)
	local lines = {}
	table.insert(lines, "[ ] Do not rely on C_AddOns.GetAddOnMetadata(addon, \"SavedVariables\") as authoritative.")
	table.insert(lines, "[ ] Confirm the DB global exists at runtime, for example /dump type(StockVaultDB).")
	table.insert(lines, "[ ] Initialize DB only after ADDON_LOADED for the addon.")
	table.insert(lines, "[ ] Create nested defaults defensively before indexing.")
	table.insert(lines, "[ ] Add schemaVersion or version field.")
	table.insert(lines, "[ ] Use the addon bridge to report exact SavedVariables names to ApiVault.")

	if record then
		for _, entry in ipairs(record.runtime or {}) do
			if not entry.exists then
				table.insert(lines, "[ ] " .. tostring(entry.name) .. " does not exist at runtime yet; verify DB init timing.")
			elseif entry.type ~= "table" then
				table.insert(lines, "[ ] " .. tostring(entry.name) .. " exists but is " .. tostring(entry.type) .. "; verify expected DB type.")
			elseif not entry.hasSchemaVersion then
				table.insert(lines, "[ ] " .. tostring(entry.name) .. " has no schemaVersion/version marker.")
			end
		end
	end
	return lines
end

function Inspector:AppendProbeReport(lines, record)
	table.insert(lines, "Addon: " .. tostring(record.addon ~= "" and record.addon or record.query))
	if record.addonInfo then
		table.insert(lines, "  runtime addon registry: found (" .. tostring(record.addonInfo.source or "unknown") .. ")")
		if record.addonInfo.title and record.addonInfo.title ~= "" then
			table.insert(lines, "  title: " .. tostring(record.addonInfo.title))
		end
	else
		table.insert(lines, "  runtime addon registry: not found")
	end

	if record.version or record.author or record.interface then
		table.insert(lines, "  metadata: version=" .. tostring(record.version or "") .. " author=" .. tostring(record.author or "") .. " interface=" .. tostring(record.interface or ""))
	end

	table.insert(lines, "")
	table.insert(lines, "TOC SavedVariables metadata through C_AddOns.GetAddOnMetadata:")
	if record.exposedSavedVariables or record.exposedSavedVariablesPerCharacter then
		table.insert(lines, "  SavedVariables=" .. tostring(record.exposedSavedVariables or ""))
		table.insert(lines, "  SavedVariablesPerCharacter=" .. tostring(record.exposedSavedVariablesPerCharacter or ""))
		table.insert(lines, "  Note: this field is optional runtime metadata and is not treated as authoritative.")
	else
		table.insert(lines, "  unavailable/not exposed by runtime metadata API")
		table.insert(lines, "  Note: this is normal for SavedVariables directives; absence here does not mean the addon has no SavedVariables.")
	end

	table.insert(lines, "")
	table.insert(lines, "Known / inferred DB names:")
	if #(record.inferredNames or {}) > 0 then
		for _, name in ipairs(record.inferredNames) do
			table.insert(lines, "  " .. tostring(name))
		end
	else
		table.insert(lines, "  none")
	end

	table.insert(lines, "")
	table.insert(lines, "Runtime global probe:")
	if #(record.runtime or {}) > 0 then
		for _, entry in ipairs(record.runtime) do
			self:AppendRuntimeEntry(lines, entry, "  ")
		end
	else
		table.insert(lines, "  no DB names available to probe")
	end

	table.insert(lines, "")
	table.insert(lines, "Public Developer API SavedVariables reports:")
	if #(record.publicReports or {}) > 0 then
		for index, report in ipairs(record.publicReports) do
			self:AppendPublicReport(lines, report, index)
			if index >= 12 then
				table.insert(lines, "  <additional reports omitted>")
				break
			end
		end
	else
		table.insert(lines, "  none")
		table.insert(lines, "  Recommended: have the addon call AV:ReportSavedVariables({ name = \"...\", initialized = ..., schemaVersion = ... }).")
	end

	table.insert(lines, "")
	table.insert(lines, "Expected SavedVariables file convention:")
	table.insert(lines, "  " .. tostring(record.expectedFile or "unknown"))

	table.insert(lines, "")
	table.insert(lines, "Conclusion:")
	local exists, tableExists = false, false
	for _, entry in ipairs(record.runtime or {}) do
		if entry.exists then exists = true end
		if entry.type == "table" then tableExists = true end
	end
	if tableExists then
		table.insert(lines, "  Runtime SavedVariables table found.")
	elseif exists then
		table.insert(lines, "  Runtime global found, but it is not a table.")
	elseif #(record.publicReports or {}) > 0 then
		table.insert(lines, "  No runtime DB table found by probe, but the addon has reported SavedVariables through ApiVault Public API.")
	else
		table.insert(lines, "  No runtime DB table found by current probes. This may be normal before the addon initializes defaults or if the DB name is unknown.")
	end

	table.insert(lines, "")
	table.insert(lines, "Checklist:")
	for _, item in ipairs(self:BuildChecklist(record)) do
		table.insert(lines, "  " .. item)
	end
	table.insert(lines, "")
end

function Inspector:Scan()
	local overview = self:BuildKnownOverview()
	self.records = overview
	self.lastScanCount = addonCount()
	return overview
end

function Inspector:FormatReport(addonFilter)
	local filter = trim(addonFilter or "")
	local lines = {}
	table.insert(lines, "ApiVault SavedVariables Schema Inspector")
	table.insert(lines, "addons scanned through runtime registry: " .. tostring(addonCount()))
	table.insert(lines, "primary source: runtime global probes + Public Developer API reports")
	table.insert(lines, "note: C_AddOns.GetAddOnMetadata(addon, \"SavedVariables\") is not authoritative and may be unavailable")
	table.insert(lines, "")

	if filter ~= "" then
		self:AppendProbeReport(lines, self:BuildRuntimeProbeRecord(filter))
		return table.concat(lines, "\n")
	end

	table.insert(lines, "No addon filter entered.")
	table.insert(lines, "Type an addon or DB name, for example:")
	table.insert(lines, "  StockVault")
	table.insert(lines, "  StockVaultDB")
	table.insert(lines, "  MailVault")
	table.insert(lines, "  ApiVault")
	table.insert(lines, "")
	table.insert(lines, "Known/runtime overview:")
	local overview = self:Scan()
	if #overview == 0 then
		table.insert(lines, "  No known runtime DB tables or Public API SavedVariables reports found yet.")
	else
		for _, record in ipairs(overview) do
			local summary = {}
			for _, entry in ipairs(record.runtime or {}) do
				if entry.exists then
					table.insert(summary, entry.name .. "=" .. entry.type)
				end
			end
			table.insert(lines, "  " .. tostring(record.addon) .. ": " .. (#summary > 0 and table.concat(summary, ", ") or "no runtime DB table detected"))
		end
	end

	return table.concat(lines, "\n")
end

function Inspector:GetStats()
	local records = self.records or self:Scan()
	return #records, self.lastScanCount or addonCount()
end
