-- ApiVault argument builder. Added in v0.5.0.
-- Converts developer-provided string values into conservative Lua values for SafeRunner.
local addonName, ApiVault = ...

ApiVault.ArgumentBuilder = ApiVault.ArgumentBuilder or {}
local Builder = ApiVault.ArgumentBuilder

Builder.metadata = {
	version = "0.5.0",
	interface = 120007,
	policy = "conservative string-to-Lua argument conversion; no loadstring; no table literal execution",
}

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

local function trim(value)
	if ApiVault and ApiVault.StringTrim then
		return ApiVault:StringTrim(value)
	end
	value = tostring(value or "")
	return (value:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function paramName(param)
	return tostring(param and (param.n or param.Name) or "value")
end

local function paramType(param)
	return tostring(param and (param.t or param.Type) or "")
end

function Builder:DetectKind(param)
	local name = lower(paramName(param))
	local typ = lower(paramType(param))
	if typ == "bool" or typ == "boolean" or string.find(typ, "bool", 1, true) then
		return "boolean"
	end
	if typ == "number" or typ == "luaindex" or typ == "fileid" or typ == "time_t" or string.find(typ, "number", 1, true) then
		return "number"
	end
	if string.find(typ, "iteminfo", 1, true) or string.find(name, "item", 1, true) then
		return "item"
	end
	if string.find(typ, "unit", 1, true) or string.find(name, "unit", 1, true) then
		return "unit"
	end
	if string.find(typ, "string", 1, true) or string.find(typ, "cstring", 1, true) then
		return "string"
	end
	if string.find(typ, "table", 1, true) then
		return "table"
	end
	if string.find(name, "id", 1, true) or string.find(name, "index", 1, true) or string.find(typ, "id", 1, true) then
		return "number"
	end
	return "string"
end

local function stripPrefix(raw, prefix)
	local prefixText = prefix .. ":"
	if string.sub(lower(raw), 1, string.len(prefixText)) == prefixText then
		return string.sub(raw, string.len(prefixText) + 1)
	end
	return nil
end

function Builder:Convert(raw, param)
	raw = trim(raw)
	local lowerRaw = lower(raw)
	local report = { warnings = {}, kind = self:DetectKind(param), raw = raw }
	if raw == "" then
		if ApiVault:IsParamNilable(param) then
			report.converted = "nil-empty-nilable"
			return nil, report
		end
		report.converted = "empty-string"
		return "", report
	end
	if lowerRaw == "nil" or lowerRaw == "<nil>" then
		report.converted = "nil-literal"
		return nil, report
	end
	local forcedString = stripPrefix(raw, "str") or stripPrefix(raw, "string") or stripPrefix(raw, "itemlink") or stripPrefix(raw, "unit")
	if forcedString ~= nil then
		report.converted = "forced-string"
		return forcedString, report
	end
	local forcedNumber = stripPrefix(raw, "num") or stripPrefix(raw, "number") or stripPrefix(raw, "itemid") or stripPrefix(raw, "spellid") or stripPrefix(raw, "currencyid") or stripPrefix(raw, "bagid") or stripPrefix(raw, "slot")
	if forcedNumber ~= nil then
		local numeric = tonumber(forcedNumber)
		if numeric == nil then
			report.warnings[#report.warnings + 1] = "number prefix used but value is not numeric"
			report.converted = "number-prefix-invalid-string"
			return forcedNumber, report
		end
		report.converted = "forced-number"
		return numeric, report
	end
	local forcedBool = stripPrefix(raw, "bool") or stripPrefix(raw, "boolean")
	if forcedBool ~= nil then
		local value = lower(forcedBool)
		if value == "true" or value == "1" or value == "yes" then
			report.converted = "forced-boolean"
			return true, report
		elseif value == "false" or value == "0" or value == "no" then
			report.converted = "forced-boolean"
			return false, report
		end
		report.warnings[#report.warnings + 1] = "boolean prefix used but value is not recognized"
		report.converted = "boolean-prefix-invalid-string"
		return forcedBool, report
	end
	if lowerRaw == "true" then
		report.converted = "boolean"
		return true, report
	elseif lowerRaw == "false" then
		report.converted = "boolean"
		return false, report
	end
	if report.kind == "boolean" then
		report.warnings[#report.warnings + 1] = "expected boolean; use true/false or bool:true/bool:false"
		report.converted = "boolean-expected-string"
		return raw, report
	end
	if report.kind == "number" or report.kind == "item" then
		local numeric = tonumber(raw)
		if numeric ~= nil then
			report.converted = "number"
			return numeric, report
		end
		if report.kind == "number" then
			report.warnings[#report.warnings + 1] = "expected number-like argument but input is not numeric"
			report.converted = "number-expected-string"
			return raw, report
		end
		report.converted = "item-string"
		return raw, report
	end
	if report.kind == "table" then
		report.warnings[#report.warnings + 1] = "table literal execution is disabled; value is passed as a string unless nilable"
		report.converted = "table-disabled-string"
		return raw, report
	end
	report.converted = report.kind or "string"
	return raw, report
end

function Builder:Build(functionInfo, rawValues, entry)
	local args = {}
	local reports = {}
	local warnings = {}
	local definitions = functionInfo and functionInfo.args or {}
	for index, param in ipairs(definitions) do
		local value, report = self:Convert(rawValues and rawValues[index] or "", param)
		args[index] = value
		reports[index] = report
		if report and report.warnings then
			for _, warning in ipairs(report.warnings) do
				warnings[#warnings + 1] = "arg" .. tostring(index) .. " " .. paramName(param) .. ": " .. warning
			end
		end
	end
	args.n = #definitions
	return args, {
		entry = entry and entry.display or "?",
		args = reports,
		warnings = warnings,
	}
end

function Builder:Describe(functionInfo)
	local lines = { "Argument builder policy:" }
	for index, param in ipairs(functionInfo and functionInfo.args or {}) do
		lines[#lines + 1] = "- arg" .. tostring(index) .. " " .. paramName(param) .. ": " .. paramType(param) .. " -> " .. self:DetectKind(param)
	end
	if #(functionInfo and functionInfo.args or {}) == 0 then
		lines[#lines + 1] = "- no arguments documented"
	end
	lines[#lines + 1] = "Supported explicit prefixes: str:, string:, number:, num:, bool:, itemid:, itemlink:, unit:, spellid:, currencyid:, bagid:, slot:. Table literal execution is disabled."
	return table.concat(lines, "\n")
end
