#!/opt/lnxall_app/bin/lua

local invoker = require "invoker"
local gfmt = string.format
local ltuidir = "/usr/lib/lua/5.x"
if invoker.statfile(ltuidir) ~= "directory" then
        io.stderr:write(gfmt("Error, LTUI module directory not found: %s\n", ltuidir))
        io.stderr:flush()
        os.exit(1)
end
package.path  = gfmt("%s;%s/?.lua", package.path,  ltuidir)
package.cpath = gfmt("%s;%s/?.so",  package.cpath, ltuidir)
ltuidir = nil
local ltui = require("ltui")

local MODULE_DIR = "/app/sysdiag_modules"
local HEADER_TEXT = "方向键移动, Enter 执行或进入菜单, Esc 返回, ? 查看说明, / 搜索, Ctrl-C 退出"

local function sorted_keys(tab)
	local keys = {}
	for key, _ in pairs(tab) do
		keys[#keys + 1] = key
	end
	table.sort(keys)
	return keys
end

local function display_width(text)
	local width, idx, len = 0, 1, #text
	while idx <= len do
		local byte = string.byte(text, idx)
		if byte >= 240 then
			idx = idx + 4
			width = width + 2
		elseif byte >= 224 then
			idx = idx + 3
			width = width + 2
		elseif byte >= 192 then
			idx = idx + 2
			width = width + 1
		else
			idx = idx + 1
			width = width + 1
		end
	end
	return width
end

local function center_text(text, width)
	local pad = math.floor(math.max(0, width - display_width(text)) / 2)
	return string.rep(" ", pad) .. text
end

local function update_header(dialog)
	dialog:text():text_set(center_text(HEADER_TEXT, dialog:width()))
end

local function configs_width(configs)
	local width = 0
	for _, config in ipairs(configs or {}) do
		local text = tostring(config)
		width = math.max(width, display_width(text), #text)
	end
	return width
end

local function layout_menuconf(dialog)
	local panel = dialog:box():panel()
	local menuconf = dialog:menuconf()
	local panel_width = panel:width()
	local panel_height = panel:height()
	local content_width = configs_width(menuconf._CONFIGS)
	if content_width <= 0 then
		content_width = panel_width
	end
	content_width = math.min(panel_width, content_width)
	local x = math.floor(math.max(0, panel_width - content_width) / 2)
	menuconf:bounds_set(ltui.rect:new(x, 0, content_width, panel_height))
	for _, item in ipairs(menuconf:_items() or {}) do
		local bounds = item:bounds()
		item:bounds_set(ltui.rect:new(bounds.sx, bounds.sy, content_width, 1))
	end
end

local function menuconf_select_index(menuconf, index)
	local items = menuconf:_items() or {}
	local total = #items
	if total == 0 then return end
	index = math.max(1, math.min(index or 1, total))

	local height = math.max(1, menuconf:height())
	local startindex = menuconf._STARTINDEX or 1
	if index < startindex or index >= startindex + height then
		startindex = index
		local maxstart = math.max(1, total - height + 1)
		if startindex > maxstart then startindex = maxstart end
		menuconf._STARTINDEX = startindex
		menuconf:clear()
		for idx = startindex, startindex + height - 1 do
			local item = items[idx]
			if item then
				item:bounds_set(ltui.rect:new(0, idx - startindex, menuconf:width(), 1))
				menuconf:insert(item)
			else
				break
			end
		end
	end

	local item = items[index]
	if item and item:parent() == menuconf then
		menuconf:select(item)
		menuconf:invalidate()
	end
end

local function restore_menuconf_selection(menuconf)
	local configs = menuconf._CONFIGS
	if not configs then return end
	local index = configs._SYSDIAG_SELECTED_INDEX
	if index then
		menuconf_select_index(menuconf, index)
	end
end

local function shell_quote(text)
	text = tostring(text or "")
	return "'" .. text:gsub("'", "'\\''") .. "'"
end

local function list_module_files(mdir)
	local files = {}
	local cmd = "find " .. shell_quote(mdir) .. " -maxdepth 1 -type f -name '*.lua' 2>/dev/null | sort"
	local fp = io.popen(cmd, "r")
	if not fp then return files end
	for line in fp:lines() do
		files[#files + 1] = line
	end
	fp:close()
	return files
end

local function basename(path)
	return string.match(path, "([^/]+)%.lua$") or path
end

local function safe_call(fn, ...)
	if type(fn) ~= "function" then return false, "not a function" end
	return pcall(fn, ...)
end

local function table_to_text(value, indent, visited)
	indent = indent or ""
	visited = visited or {}
	if type(value) ~= "table" then
		return tostring(value)
	end
	if visited[value] then
		return "<cycle>"
	end
	visited[value] = true

	local lines = {}
	local numeric = true
	local maxn = 0
	for key, _ in pairs(value) do
		if type(key) ~= "number" then numeric = false end
		if type(key) == "number" and key > maxn then maxn = key end
	end

	if numeric then
		for i = 1, maxn do
			if value[i] ~= nil then
				local item = value[i]
				if type(item) == "table" then
					lines[#lines + 1] = indent .. tostring(i) .. ":"
					lines[#lines + 1] = table_to_text(item, indent .. "  ", visited)
				else
					lines[#lines + 1] = indent .. tostring(i) .. ": " .. tostring(item)
				end
			end
		end
	else
		for _, key in ipairs(sorted_keys(value)) do
			local item = value[key]
			if type(item) == "table" then
				lines[#lines + 1] = indent .. tostring(key) .. ":"
				lines[#lines + 1] = table_to_text(item, indent .. "  ", visited)
			else
				lines[#lines + 1] = indent .. tostring(key) .. ": " .. tostring(item)
			end
		end
	end
	visited[value] = nil
	return #lines > 0 and table.concat(lines, "\n") or indent .. "<empty>"
end

local function check_module_api(id, mod)
	local apis = { "checkplat", "category", "description", "detailed", "suggestion", "step" }
	for _, api in ipairs(apis) do
		if type(mod[api]) ~= "function" then
			return false, id .. ": missing api " .. api
		end
	end
	return true
end

local function load_modules()
	local modules, errors = {}, {}
	for _, path in ipairs(list_module_files(MODULE_DIR)) do
		local id = basename(path)
		local loader, lerr = loadfile(path)
		if not loader then
			errors[#errors + 1] = id .. ": " .. tostring(lerr)
		else
			local ok, mod = pcall(loader)
			if not ok or type(mod) ~= "table" then
				errors[#errors + 1] = id .. ": failed to load module"
			else
				local valid, verr = check_module_api(id, mod)
				if not valid then
					errors[#errors + 1] = verr
				else
					local pok, matched = safe_call(mod.checkplat)
					if not pok then
						errors[#errors + 1] = id .. ": checkplat failed: " .. tostring(matched)
					elseif matched then
						modules[#modules + 1] = { id = id, path = path, handle = mod }
					end
				end
			end
		end
	end
	table.sort(modules, function (a, b) return a.id < b.id end)
	return modules, errors
end

local function get_text(mod, api, fallback)
	local ok, text = safe_call(mod[api])
	if ok and type(text) == "string" and #text > 0 then
		return text
	end
	return fallback
end

local function run_module(entry)
	local mod = entry.handle
	local ok, result, haserr = safe_call(mod.step)
	if not ok then
		return "模块执行失败\n\n" .. tostring(result)
	end

	local lines = {}
	lines[#lines + 1] = "模块: " .. entry.id
	lines[#lines + 1] = "类别: " .. get_text(mod, "category", "Unknown")
	lines[#lines + 1] = "结果: " .. (haserr and "发现异常" or "未发现异常")
	lines[#lines + 1] = ""
	lines[#lines + 1] = "诊断数据:"
	lines[#lines + 1] = table_to_text(result or {})
	if haserr then
		lines[#lines + 1] = ""
		lines[#lines + 1] = "处理建议:"
		lines[#lines + 1] = get_text(mod, "suggestion", "无处理建议")
	end
	return table.concat(lines, "\n")
end

local function build_help(entry)
	local mod = entry.handle
	return table.concat({
		get_text(mod, "detailed", "无详细说明"),
		"",
		"处理建议:",
		get_text(mod, "suggestion", "无处理建议"),
		"",
		"模块文件:",
		entry.path,
	}, "\n")
end

local function build_configs(modules, errors)
	local groups = {}
	for _, entry in ipairs(modules) do
		local category = get_text(entry.handle, "category", "Unknown")
		local description = get_text(entry.handle, "description", entry.id)
		entry.description = description
		entry.category = category
		entry.help = build_help(entry)
		groups[category] = groups[category] or {}
		table.insert(groups[category], entry)
	end

	local function item_prompt(text)
		return function ()
			return text
		end
	end

	local function menu_prompt(text)
		return function ()
			return text
		end
	end

	local configs = {}
	for _, category in ipairs(sorted_keys(groups)) do
		local items = {}
		for _, entry in ipairs(groups[category]) do
			items[#items + 1] = ltui.menuconf.config {
				description = { entry.description, "", entry.help },
				prompt = item_prompt(entry.description),
				module = entry,
				new = false
			}
		end
		configs[#configs + 1] = ltui.menuconf.menu {
			description = category,
			prompt = menu_prompt(category),
			configs = items,
			new = false
		}
	end

	if #errors > 0 then
		local items = {}
		for i, err in ipairs(errors) do
			items[#items + 1] = ltui.menuconf.config {
				description = "错误 " .. tostring(i),
				prompt = item_prompt("错误 " .. tostring(i)),
				result_text = err,
				new = false
			}
		end
		configs[#configs + 1] = ltui.menuconf.menu {
			description = "加载错误",
			prompt = menu_prompt("加载错误"),
			configs = items,
			new = false
		}
	end

	if #configs == 0 then
		configs[#configs + 1] = ltui.menuconf.config {
			description = "未发现可用诊断模块",
			prompt = item_prompt("未发现可用诊断模块"),
			result_text = "未在 " .. MODULE_DIR .. " 下发现可用诊断模块",
			new = false
		}
	end
	return configs
end

local app = ltui.application()

function app:init(name, argv)
	ltui.application.init(self, name, argv)
	local modules, errors = load_modules()
	local dialog = ltui.mconfdialog:new("sysdiag.dialog", ltui.rect {0, 0, self:width(), self:height()}, "系统诊断")
	update_header(dialog)
	dialog:button("exit"):action_set(ltui.action.ac_on_enter, function ()
		self:quit()
		return true
	end)
	dialog:box():panel():action_add(ltui.action.ac_on_resized, function ()
		layout_menuconf(dialog)
	end)
	dialog:menuconf():action_add(ltui.action.ac_on_load, function (v)
		layout_menuconf(dialog)
		restore_menuconf_selection(v)
	end)
	dialog:menuconf():action_set(ltui.action.ac_on_selected, function (v, config)
		if config.module then
			dialog:show_result(run_module(config.module))
			return true
		elseif config.result_text then
			dialog:show_result(config.result_text)
			return true
		elseif config.kind == "menu" and config.configs and #config.configs > 0 then
			local current = v:current()
			if current and v._CONFIGS then
				v._CONFIGS._SYSDIAG_SELECTED_INDEX = current:extra("index")
			end
		end
	end)
	dialog:load(build_configs(modules, errors))
	self:insert(dialog)
	self._SYSDIAG_DIALOG = dialog
	self._SYSDIAG_MODULES = modules
	self._SYSDIAG_ERRORS = errors
end

function app:on_resize()
	if self._SYSDIAG_DIALOG then
		self._SYSDIAG_DIALOG:bounds_set(ltui.rect {0, 0, self:width(), self:height()})
		ltui.application.on_resize(self)
		update_header(self._SYSDIAG_DIALOG)
		layout_menuconf(self._SYSDIAG_DIALOG)
		return
	end
	ltui.application.on_resize(self)
end

app:run("sysdiag-tui", arg)
