local _XXT_LANCONTROL_VERSION = "v202608221455";
--[[
    XXTLanControl 模块

    -- 引入模块
    local LCC = require "XXTLanControl"
--]]

if sys.xtversion():compare_version("1.3.8-20250809000738") < 0 then
    error("需要更新版本的 XXT 才能使用 XXTLanControl.lua 中控模块", 2)
    return {os.exit()}
end

local toast_has_allow_screenshot = sys.xtversion():compare_version("1.3.8-20250919092207") >= 0

local XXTLanControl = {
    _VERSION = _XXT_LANCONTROL_VERSION or "dev";
    _AUTHOR = "havonz";
}

XXTLanControl.web_file = {}
XXTLanControl.assist = {}
local _XXTLanControl_options = {}

local _check_value = functor.argth.check_value
local _opt_value = functor.argth.opt_value

local _orig_toast = sys.toast
local _toast = toast_has_allow_screenshot and function(ctx, orien)
    _orig_toast(ctx, {orien = orien, allow_screenshot = true})
end or _orig_toast

local _ENV = {
    error = error;
    setmetatable = setmetatable;
    select = select;
    tostring = tostring;
    tonumber = tonumber;
    type = type;
    pcall = pcall;
    pairs = pairs;
    ipairs = ipairs;
    load = load;
    require = require;
    package = package;
    proc_get = proc_get;
    proc_put = proc_put;
    utf8 = {len = utf8.len};
    string = {
        gsub = string.gsub;
        match = string.match;
        lower = string.lower;
        sub = string.sub;
        find = string.find;
        format = string.format;
        compare_version = string.compare_version;
        pack = string.pack;
        base64_encode = string.base64_encode;
        base64_decode = string.base64_decode;
    };
    math = {huge = math.huge; floor = math.floor};
    http = {get = http.get, post = http.post, head = http.head, put = http.put, delete = http.delete};
    json = {encode = json.encode, decode = json.decode};
    file = {
        reads = file.reads;
        writes = file.writes;
        remove = file.remove;
        exists = file.exists;
        move = file.move;
        path = {
            normalize = file.path.normalize;
            resolve_symlinks = file.path.resolve_symlinks;
        };
    };
    utils = {gen_uuid = utils.gen_uuid};
    os = {exit = os.exit, date = os.date};
    debug = {getinfo = debug.getinfo, traceback = debug.traceback};
    sys = {toast = _toast, alert = sys.alert, msleep = sys.msleep, mtime = sys.mtime};
    device = {udid = device.udid; name = device.name; type = device.type};
    screen = {image = screen.image};
    image = {is = image.is};
    xpp = {info = xpp.info, resource_path = xpp.resource_path};
    table = {concat = table.concat; deep_dump = table.deep_dump; remove = table.remove};
    register_atexit = register_atexit;
    XXT_HOME_PATH = XXT_HOME_PATH;
    XXT_SCRIPTS_PATH = XXT_SCRIPTS_PATH;
    XXT_LUA_PATH = XXT_LUA_PATH;
    XXT_RES_PATH = XXT_RES_PATH;
}

local _err_msg = {
	timeout = [[与服务端访问超时
1.请确认中控端正常运行
2.中控端防火墙请确保关闭
3.请确保设备与中控端始终处于同一个局域网
]],
	server_bug = [[服务端出现错误，可检查 VPN 或 HTTP 代理设置，以下为错误原因：]],
	submit_ip = [[提交的服务器IP不正确]],
	not_find = [[未找到中控设备
1.中控端防火墙请确保关闭
2.请由中控端或XXTStudio启动
3.请确保设备与中控端始终处于同一个局域网
]],
	not_init = [[未初始化中控模块
请使用 'XXTLanControl.connect()' 进行初始化操作]]
}

local function _failed_toast_msg(msg, level_plus)
    local level = 5 + (level_plus or 0)
    if level < 1 then level = 1 end

    local debug_info
    for i = level, 1, -1 do
        debug_info = debug.getinfo(i)
        if type(debug_info) == 'table' and debug_info.currentline > 0 then break end
    end

    if not debug_info then
        return msg
    end

    local line = debug_info.currentline
    local short_src = debug_info.short_src
    return msg .. "\n\n现在正在 " .. tostring(short_src) .. " 第 " .. tostring(line) .. " 行"
end

local _XXTLanControl_retry_count = 0
local _XXTLanControl_assist_open_tasks = {}

local function _XXTLanControl_request(method, options)
    options.timeout = options.timeout or 60
    local retry_count = 0
    while true do
        local c, h, r = http[method](options)
        if c == -1 then
            sys.toast(_failed_toast_msg(_err_msg.timeout .. tostring(r or ""), options.level_plus or 0))
        elseif c >= 500 then
            sys.toast(_failed_toast_msg(_err_msg.server_bug .. tostring(r or ""), options.level_plus or 0))
        else
            return c, h, r
        end
        if _XXTLanControl_retry_count > 0 then
            retry_count = retry_count + 1
            if retry_count >= _XXTLanControl_retry_count then
                return c, h, r
            end
        end
        sys.msleep(1000)
    end
end

-- 设置对中控连接的重试次数，设置为 0 是无限次重试，默认所有的对中控的请求都是无限次重试，直到中控返回成功或失败
---@param retry_count integer 重试次数，0 为无限次
-- 例子
--   LCC.set_retry_count(0)
function XXTLanControl.set_retry_count(retry_count)
    retry_count = _check_value(1, "integer", retry_count)
    if retry_count < 1 then
        retry_count = 0
    end
    _XXTLanControl_retry_count = retry_count
end

-- 设置中控访问地址，如果不是中控启动需要用这个
---@param url string 中控地址
-- 例子
--   LCC.set_api_url("http://192.168.11.193:46990")
function XXTLanControl.set_api_url(url)
    url = _check_value(1, "string", url)
    local lcc_config_json = proc_get("xxtouch.lancontrol.config")
    local lcc_config = json.decode(lcc_config_json)
    if type(lcc_config) == 'table' then
        _XXTLanControl_options = lcc_config
    end
    _XXTLanControl_options["api-url"] = url
    proc_put("xxtouch.lancontrol.config", json.encode(_XXTLanControl_options))
end

-- 设置日志列别名（对应 "日志 #1" ~ "日志 #6"）
---@param aliases string[] 别名数组，顺序对应日志列
function XXTLanControl.set_log_aliases(aliases)
    aliases = _check_value(1, "table", aliases)
    local t = {}
    for i, v in ipairs(aliases) do
        if i > 6 then
            break
        end
        if type(v) ~= 'string' or v == '' then
            goto continue
        end
        t['log'..i] = v
        ::continue::
    end
    local lcc_config_json = proc_get("xxtouch.lancontrol.config")
    local lcc_config = json.decode(lcc_config_json)
    if type(lcc_config) == 'table' then
        _XXTLanControl_options = lcc_config
    end
    _XXTLanControl_options["log-column-aliases"] = t
    proc_put("xxtouch.lancontrol.config", json.encode(_XXTLanControl_options))
end

-- 连接中控，连接成功返回 true
---@return boolean success 连接成败
-- 例子
--   ok = LCC.connect()
function XXTLanControl.connect()
    local lcc_config = proc_get("xxtouch.lancontrol.config")
    -- sys.toast(lcc_config)
    if lcc_config == '' then
        return false
    end
    lcc_config = json.decode(lcc_config)
    if type(lcc_config) ~= 'table' or lcc_config["api-url"] == nil then
        return false
    end
    local api_url = lcc_config["api-url"]
    local c, h, r = _XXTLanControl_request("post", {
        url = api_url .. "/api/devices/run-state",
        timeout = 5,
        json = {udid = device.udid(), is_running = true, time = sys.mtime(), version = XXTLanControl._VERSION},
    })
    if c == 200 then
        r = json.decode(r)
        if type(r) ~= 'table' or r.code ~= 0 then
            sys.alert("XXTLanControl 模块版本不匹配\n\n 10 秒后自动尝试下载新版 XXTLanControl 模块", 10)
            local c, h, r = _XXTLanControl_request("get", {
                url = api_url .. "/api/down/XXTLanControl.lua",
                timeout = 10,
            })
            if c == 200 then
                r = tostring(r)
                local func = load(r, XXT_SCRIPTS_PATH.."/XXTLanControl.lua")
                if type(func) == 'function' then
                    file.remove(XXT_LUA_PATH.."/XXTLanControl.lua")
                    file.remove(XXT_LUA_PATH.."/XXTLanControl.xxt")
                    file.remove(XXT_SCRIPTS_PATH.."/XXTLanControl.xxt")
                    file.writes(XXT_SCRIPTS_PATH.."/XXTLanControl.lua", r)
                    package.loaded["XXTLanControl"] = nil
                    package.preload["XXTLanControl"] = func
                    local new_XXTLCC = require("XXTLanControl")
                    for k, v in pairs(new_XXTLCC) do
                        XXTLanControl[k] = v
                    end
                    new_XXTLCC.set_retry_count(_XXTLanControl_retry_count)
                    return new_XXTLCC.connect()
                end
            end
            return false
        end
        for k, v in pairs(lcc_config) do
            _XXTLanControl_options[k] = v
        end
        if type(register_atexit) == 'function' then
            register_atexit("xxtouch.lancontrol.run-state", function()
                XXTLanControl.assist.cancel_all(3)
                http.post({
                    url = api_url .. "/api/devices/run-state",
                    timeout = 3,
                    json = {udid = device.udid(), is_running = false, time = sys.mtime(), version = XXTLanControl._VERSION},
                })
            end)
        end
        return true
    end
    sys.toast(table.deep_dump({c, h, r}))
    return false
end

local function _assist_task_kind(request_type)
    request_type = string.lower(tostring(request_type or 'text'))
    if request_type == 'text' or request_type == 'text-reply' then
        return 'text-reply'
    elseif request_type == 'point' or request_type == 'pick-point' then
        return 'pick-point'
    elseif request_type == 'control' or request_type == 'device-control' then
        return 'device-control'
    end
    return 'text-reply'
end

local function _assist_request_type(kind)
    if kind == 'pick-point' then
        return 'point'
    elseif kind == 'device-control' then
        return 'control'
    end
    return 'text'
end

local function _assist_safe_filename(path)
    local name = string.match(tostring(path or ''), "[^/\\]+$")
    if type(name) ~= 'string' or name == '' then
        name = 'screenshot.png'
    end
    name = string.gsub(name, "[^%w%._%-]", "_")
    if name == '' then
        return 'screenshot.png'
    end
    return name
end

local function _assist_image_data_mime(data, fallback)
    if type(fallback) == 'string' and fallback ~= '' then
        return fallback
    end
    if type(data) ~= 'string' then
        return nil
    end
    if string.sub(data, 1, 8) == "\137PNG\r\n\26\n" then
        return 'image/png'
    end
    if string.sub(data, 1, 3) == "\255\216\255" then
        return 'image/jpeg'
    end
    return nil
end

local function _assist_image_data_url(data, mime_type)
    if type(data) ~= 'string' or data == '' then
        return nil, 'invalid screenshot image data'
    end
    if string.match(data, "^data:image/") then
        return data
    end
    local mime = _assist_image_data_mime(data, mime_type)
    if not mime then
        return nil, 'unsupported screenshot image data'
    end
    return 'data:' .. mime .. ';base64,' .. data:base64_encode()
end

local function _assist_is_image_object(value)
    return type(image) == 'table' and type(image.is) == 'function' and image.is(value)
end

local function _assist_image_object_data_url(img, format, quality)
    if not _assist_is_image_object(img) then
        return nil, 'invalid screenshot image object'
    end
    local image_format = string.lower(tostring(format or 'jpeg'))
    if image_format == 'png' then
        local ok, data = pcall(function()
            return img:png_data()
        end)
        if not ok then
            return nil, tostring(data or 'export screenshot image failed')
        end
        return _assist_image_data_url(data, 'image/png')
    end

    local image_quality = type(quality) == 'number' and quality or 0.7
    if image_quality < 0 then
        image_quality = 0
    elseif image_quality > 1 then
        image_quality = 1
    end
    local ok, data = pcall(function()
        return img:jpeg_data(image_quality)
    end)
    if not ok then
        return nil, tostring(data or 'export screenshot image failed')
    end
    return _assist_image_data_url(data, 'image/jpeg')
end

local function _assist_prepare_inline_image(options, input, screenshot_path)
    if type(input.imageSrc) == 'string' and input.imageSrc ~= '' then
        return nil
    end
    if type(screenshot_path) == 'string' and screenshot_path ~= '' then
        return nil
    end

    local image_src = options.screenshotImageSrc or options.imageSrc
    if type(image_src) == 'string' and image_src ~= '' then
        input.imageSrc = image_src
        return nil
    end

    local screenshot_image = options.screenshotImage or options.image
    if screenshot_image ~= nil then
        local data_url, err = _assist_image_object_data_url(
            screenshot_image,
            options.screenshotImageFormat or options.imageFormat,
            options.screenshotImageQuality or options.imageQuality
        )
        if not data_url then
            return err
        end
        input.imageSrc = data_url
        return nil
    end

    local image_data = options.screenshotImageData or options.imageData
    if image_data ~= nil then
        local data_url, err = _assist_image_data_url(
            image_data,
            options.screenshotImageMimeType or options.imageMimeType or options.mimeType
        )
        if not data_url then
            return err
        end
        input.imageSrc = data_url
    end
    return nil
end

local function _assist_clone_options(options)
    local out = {}
    if type(options) == 'table' then
        for k, v in pairs(options) do
            out[k] = v
        end
    end
    return out
end

local function _assist_parse_task_response(body)
    local obj = json.decode(body)
    if type(obj) == 'table' and type(obj.task) == 'table' then
        return obj.task
    end
    return nil
end

local function _assist_cancel_task(id, timeout)
    if type(id) ~= 'string' or id == '' then
        return false, 'invalid task id'
    end
    _check_XXTLanControl_api_url()
    local c, h, r = http.post({
        url = _XXTLanControl_options["api-url"] .. '/api/assist/tasks/' .. id .. '/cancel',
        timeout = timeout or 10,
        json = {}
    })
    _XXTLanControl_assist_open_tasks[id] = nil
    if c == 200 then
        return true, nil, _assist_parse_task_response(r)
    end
    if c == 404 then
        return false, 'not found'
    end
    return false, tostring(r or '')
end

-- 创建人工辅助请求。
---@param options table 请求配置
---@return table? task 成功返回任务
---@return string? errMsg 失败返回错误信息
function XXTLanControl.assist.create(options)
    options = _check_value(1, "table", options)
    _check_XXTLanControl_api_url()

    local request_type = options.type or 'text'
    local kind = _assist_task_kind(request_type)
    local local_task_id = utils.gen_uuid()
    local screenshot_path = options.screenshotPath
    local timeout = type(options.timeout) == 'number' and options.timeout or 300
    if type(options.screenshotLocalPath) == 'string' and options.screenshotLocalPath ~= '' then
        local filename = _assist_safe_filename(options.screenshotLocalPath)
        screenshot_path = '_temp/assist/lua-script/' .. device.udid() .. '/' .. local_task_id .. '/' .. filename
        local ok, err = XXTLanControl.web_file.upload_file(options.screenshotLocalPath, screenshot_path, 60)
        if not ok then
            return nil, err or 'upload screenshot failed'
        end
    end

    local input = type(options.input) == 'table' and _assist_clone_options(options.input) or {}
    input.requestType = _assist_request_type(kind)
    input.scriptRequestId = local_task_id
    if type(options.imageRect) == 'table' then
        input.imageRect = options.imageRect
    end
    local inline_image_err = _assist_prepare_inline_image(options, input, screenshot_path)
    if inline_image_err then
        return nil, inline_image_err
    end

    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/assist/tasks',
        timeout = 10,
        json = {
            kind = kind;
            title = tostring(options.title or '脚本请求人工辅助');
            prompt = tostring(options.text or options.prompt or '');
            deviceId = device.udid();
            source = 'lua-script';
            screenshotPath = screenshot_path;
            input = input;
            expiresInMs = math.floor(timeout * 1000);
        }
    })
    if c == 200 then
        local task = _assist_parse_task_response(r)
        if type(task) == 'table' and type(task.id) == 'string' then
            _XXTLanControl_assist_open_tasks[task.id] = true
            return task
        end
        return nil, 'invalid assist task response'
    end
    return nil, tostring(r or '')
end

-- 等待人工辅助结果。
---@param id string 辅助请求 id
---@param timeout? number 等待秒数，默认 300
---@param pollIntervalMs? number 轮询间隔毫秒，默认 1000
---@return table? result 成功返回提交结果
---@return string? errMsg 失败返回错误信息
---@return table? task 最后一次任务状态
function XXTLanControl.assist.wait(id, timeout, pollIntervalMs)
    id = _check_value(1, "string", id)
    timeout = _opt_value(2, "number", 300, nil, timeout)
    pollIntervalMs = _opt_value(3, "number", 1000, nil, nil, pollIntervalMs)
    if pollIntervalMs < 250 then
        pollIntervalMs = 250
    end
    _check_XXTLanControl_api_url()

    local deadline = sys.mtime() + timeout * 1000
    local last_task
    while true do
        local c, h, r = _XXTLanControl_request("get", {
            url = _XXTLanControl_options["api-url"] .. '/api/assist/tasks/' .. id,
            timeout = 10,
        })
        if c == 200 then
            local task = _assist_parse_task_response(r)
            if type(task) == 'table' then
                last_task = task
                if task.status == 'submitted' then
                    _XXTLanControl_assist_open_tasks[id] = nil
                    return task.result or {}, nil, task
                elseif task.status == 'cancelled' or task.status == 'expired' then
                    _XXTLanControl_assist_open_tasks[id] = nil
                    return nil, task.status, task
                end
            end
        elseif c == 404 then
            _XXTLanControl_assist_open_tasks[id] = nil
            return nil, 'not found', last_task
        else
            return nil, tostring(r or ''), last_task
        end

        if sys.mtime() >= deadline then
            local ok, err, task = _assist_cancel_task(id, 3)
            return nil, 'timeout', task or last_task
        end
        sys.msleep(pollIntervalMs)
    end
end

-- 发起并等待人工辅助请求。
---@param options table 请求配置
---@return table? result 成功返回提交结果
---@return string? errMsg 失败返回错误信息
---@return table? task 最后一次任务状态
function XXTLanControl.assist.request(options)
    options = _check_value(1, "table", options)
    local task, err = XXTLanControl.assist.create(options)
    if not task then
        return nil, err
    end
    return XXTLanControl.assist.wait(task.id, options.timeout or 300, options.pollIntervalMs or 1000)
end

-- 请求人工输入文本。
function XXTLanControl.assist.ask_text(options)
    options = _assist_clone_options(_check_value(1, "table", options))
    options.type = 'text'
    return XXTLanControl.assist.request(options)
end

-- 请求人工在截图上选择坐标。
function XXTLanControl.assist.pick_point(options)
    options = _assist_clone_options(_check_value(1, "table", options))
    options.type = 'point'
    return XXTLanControl.assist.request(options)
end

-- 请求人工远程控制设备。
function XXTLanControl.assist.request_control(options)
    options = _assist_clone_options(_check_value(1, "table", options))
    options.type = 'control'
    return XXTLanControl.assist.request(options)
end

-- 撤销一个人工辅助请求。
function XXTLanControl.assist.cancel(id, timeout)
    id = _check_value(1, "string", id)
    timeout = _opt_value(2, "number", 10, nil, timeout)
    return _assist_cancel_task(id, timeout)
end

-- 撤销当前脚本创建但尚未完成的人工辅助请求。
function XXTLanControl.assist.cancel_all(timeout)
    if type(timeout) ~= 'number' then
        timeout = 5
    end
    local count = 0
    for id in pairs(_XXTLanControl_assist_open_tasks) do
        local ok = _assist_cancel_task(id, timeout)
        if ok then
            count = count + 1
        end
    end
    return count
end

-- 中控脚本配置界面说明
-- 平铺脚本的 UI 界面和对界面的配置存储在 "lua/scripts/main.json"
-- XPP 脚本包的 UI 界面和对界面的配置存储在包根目录的 "main.json"
-- 中控启动脚本时，会把对应的 main.json 一起发送到设备上
-- 用以下方式读取配置界面的配置的参数
-- 例子
--   conf = LCC.get_ui_config()
function XXTLanControl.get_ui_config()
    local config_path = nil
    local info = xpp.info() or {}
    if info.BundleIdentifier then
        local path = xpp.resource_path("main.json")
        if path then
            config_path = path
        end
    end
    local s = file.reads(config_path or (XXT_SCRIPTS_PATH.."/main.json")) or "{}"
    local main_json = json.decode(s) or {}
    local conf = main_json.Config or {}
    if type(main_json.UI) == 'table' then
        for _, ctrl in ipairs(main_json.UI) do
            if type(ctrl) ~= 'table' or type(ctrl.caption) ~= 'string' then
                goto continue
            end
            if ctrl.text and not conf[ctrl.caption] then
                conf[ctrl.caption] = ctrl.text
                goto continue
            end
            if type(ctrl.item) ~= 'table' then
                goto continue
            end
            if (ctrl.type == 'ComboBox' or ctrl.type == 'RadioGroup') and type(ctrl.select) == 'number' then
                if type(conf[ctrl.caption]) == 'number' and ctrl.item[conf[ctrl.caption]] then
                    conf[ctrl.caption] = ctrl.item[conf[ctrl.caption]]
                else
                    conf[ctrl.caption] = ctrl.item[ctrl.select]
                end
            elseif ctrl.type == 'CheckBoxGroup' and type(ctrl.select) == 'table' then
                if type(conf[ctrl.caption]) ~= 'table' then
                    conf[ctrl.caption] = ctrl.select
                end
                local t = {}
                for _, i in ipairs(conf[ctrl.caption]) do
                    if ctrl.item[i] then
                        local text = ctrl.item[i]
                        t[text] = true
                        if ctrl.orderedSelection == true then
                            t[#t + 1] = text
                        end
                    end
                end
                conf[ctrl.caption] = t
            end
            ::continue::
        end
    end
    return conf
end

local _UI_STATE_MIME_TYPE = "application/vnd.xxtouch.ui-element-capture+zip"
local _UI_STATE_MAX_ARCHIVE_BYTES = 20 * 1024 * 1024
local _UI_STATE_MAX_SCREENSHOT_BYTES = 16 * 1024 * 1024
local _UI_STATE_MAX_SCREENSHOT_DIMENSION = 16384
local _UI_STATE_MAX_SCREENSHOT_PIXELS = 12000000
local _UI_STATE_MAX_ELEMENTS_BYTES = 4 * 1024 * 1024
local _UI_STATE_MAX_ELEMENTS = 5000
local _UI_STATE_MAX_DEPTH = 32
local _UI_STATE_MIN_UI_ELEMENT_VERSION = "0.6.0"
local _UI_STATE_EMPTY_ELEMENTS_JSON = '{"elements":[]}'
local _UI_STATE_EMPTY_SCREENSHOT_PNG = string.base64_decode(
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNgAAIAAAUAAen63NgAAAAASUVORK5CYII="
)

local _ui_state_methods = {}
local _ui_state_values = setmetatable({}, {__mode = "k"})
local _ui_state_metatable = {
    __index = _ui_state_methods;
    __metatable = "XXTLanControl.UIStateObject";
}

local function _ui_state_error_text(value, fallback)
    local text = tostring(value or fallback or "unknown error")
    text = string.gsub(text, "\194[\128-\159]", " ")
    text = string.gsub(text, "[%z\1-\31\127]", " ")
    text = string.gsub(text, "^%s+", "")
    text = string.gsub(text, "%s+$", "")
    if text == "" then
        text = tostring(fallback or "unknown error")
    end
    if #text > 256 then
        text = string.sub(text, 1, 256)
    end
    local _, invalid_offset = utf8.len(text)
    if invalid_offset then
        text = string.sub(text, 1, invalid_offset - 1)
    end
    return text ~= "" and text or "unknown error"
end

local function _ui_state_optional_device_value(getter)
    local value = getter()
    if type(value) ~= "string" then
        return nil
    end
    value = string.gsub(value, "^%s+", "")
    value = string.gsub(value, "%s+$", "")
    return value ~= "" and value or nil
end

local function _ui_state_count_elements(elements)
    local stack = {}
    for index = #elements, 1, -1 do
        stack[#stack + 1] = elements[index]
    end
    local count = 0
    while #stack > 0 do
        local node = stack[#stack]
        stack[#stack] = nil
        count = count + 1
        if count > _UI_STATE_MAX_ELEMENTS then
            return nil, "元素数量超过限制"
        end
        local children = node.children
        if children then
            for index = #children, 1, -1 do
                stack[#stack + 1] = children[index]
            end
        end
    end
    return count
end

local function _ui_state_capture_screenshot()
    local screenshot = screen.image()
    local width, height = screenshot:size()
    local screenshot_png = screenshot:png_data()
    screenshot:destroy()
    if #screenshot_png > _UI_STATE_MAX_SCREENSHOT_BYTES then
        return nil, nil, nil, "截图超过 16 MiB 限制"
    end
    if width > _UI_STATE_MAX_SCREENSHOT_DIMENSION or height > _UI_STATE_MAX_SCREENSHOT_DIMENSION then
        return nil, nil, nil, "截图单边超过 16384 像素限制"
    end
    if width * height > _UI_STATE_MAX_SCREENSHOT_PIXELS then
        return nil, nil, nil, "截图超过 1200 万像素限制"
    end
    return screenshot_png, width, height
end

local function _ui_state_require_ui_element()
    local loaded, ui_element_module = pcall(require, "ui_element")
    local version_supported = false
    if loaded and type(ui_element_module) == "table"
        and type(ui_element_module._VERSION) == "string"
    then
        local compared, result = pcall(
            string.compare_version,
            ui_element_module._VERSION,
            _UI_STATE_MIN_UI_ELEMENT_VERSION
        )
        version_supported = compared and type(result) == "number" and result >= 0
    end
    if not version_supported
        or type(ui_element_module.get_ui_snapshot_once) ~= "function"
    then
        error(
            "当前 XXTouch 版本不支持 LCC.capture_ui_state，请更新 XXTouch 后再使用"
                .. "（需要 ui_element 0.6.0 或更高版本）",
            3
        )
    end
    return ui_element_module
end

local function _ui_state_capture_elements(ui_element_module, hit_test_spacing, force_hit_test)
    local capture_options = {
        include_hit_state = true;
        hit_test_spacing = hit_test_spacing;
        max_level = _UI_STATE_MAX_DEPTH;
        max_elements = _UI_STATE_MAX_ELEMENTS;
    }
    if force_hit_test then
        capture_options.hit_test = true
    else
        capture_options.fallback_hit_test = true
    end
    local called, snapshot, snapshot_error = pcall(
        ui_element_module.get_ui_snapshot_once,
        capture_options
    )
    if not called or snapshot == nil then
        return nil, nil, _ui_state_error_text(snapshot_error or snapshot, "文本元素抓取失败")
    end
    local count, count_error = _ui_state_count_elements(snapshot.elements)
    if count == nil then
        return nil, nil, count_error
    end
    local encoded, elements_json_or_error = pcall(json.encode, snapshot)
    if not encoded or type(elements_json_or_error) ~= "string" then
        return nil, nil, _ui_state_error_text(elements_json_or_error, "元素树无法序列化")
    end
    if #elements_json_or_error > _UI_STATE_MAX_ELEMENTS_BYTES then
        return nil, nil, "元素 JSON 超过 4 MiB 限制"
    end
    return elements_json_or_error, count
end

local function _ui_state_captured_at(timestamp_ms)
    local seconds = math.floor(timestamp_ms / 1000)
    return os.date("!%Y-%m-%dT%H:%M:%S", seconds)
        .. string.format(".%03dZ", math.floor(timestamp_ms - seconds * 1000))
end

local function _ui_state_crc32(zlib, data)
    local checksum = zlib.crc32()
    return checksum(data) & 0xffffffff
end

local function _ui_state_deflate(zlib, data)
    local deflater = zlib.deflate(6, -15)
    local compressed, finished = deflater(data, "finish")
    if type(compressed) ~= "string" or finished ~= true then
        error("Deflate 压缩失败", 2)
    end
    return compressed
end

local function _ui_state_zip_entry(zlib, name, data, compression_method)
    local compressed = compression_method == 0 and data or _ui_state_deflate(zlib, data)
    return {
        name = name;
        data = data;
        compressed = compressed;
        method = compression_method;
        crc32 = _ui_state_crc32(zlib, data);
        offset = 0;
    }
end

local function _ui_state_create_zip(manifest_json, screenshot_png, elements_json)
    local loaded, zlib_or_error = pcall(require, "zlib")
    if not loaded or type(zlib_or_error) ~= "table"
        or type(zlib_or_error.deflate) ~= "function"
        or type(zlib_or_error.crc32) ~= "function"
    then
        error("无法加载 zlib：" .. tostring(zlib_or_error or "unknown error"), 2)
    end
    local entries = {
        _ui_state_zip_entry(zlib_or_error, "manifest.json", manifest_json, 8),
        _ui_state_zip_entry(zlib_or_error, "screenshot.png", screenshot_png, 0),
        _ui_state_zip_entry(zlib_or_error, "elements.json", elements_json, 8),
    }
    local local_records = {}
    local local_size = 0
    for index, entry in ipairs(entries) do
        entry.offset = local_size
        local record = string.pack(
            "<I4I2I2I2I2I2I4I4I4I2I2",
            0x04034b50,
            20,
            0,
            entry.method,
            0,
            0x21,
            entry.crc32,
            #entry.compressed,
            #entry.data,
            #entry.name,
            0
        ) .. entry.name .. entry.compressed
        local_records[index] = record
        local_size = local_size + #record
    end

    local central_records = {}
    local central_size = 0
    for index, entry in ipairs(entries) do
        local record = string.pack(
            "<I4I2I2I2I2I2I2I4I4I4I2I2I2I2I2I4I4",
            0x02014b50,
            20,
            20,
            0,
            entry.method,
            0,
            0x21,
            entry.crc32,
            #entry.compressed,
            #entry.data,
            #entry.name,
            0,
            0,
            0,
            0,
            0,
            entry.offset
        ) .. entry.name
        central_records[index] = record
        central_size = central_size + #record
    end
    local end_record = string.pack(
        "<I4I2I2I2I2I4I4I2",
        0x06054b50,
        0,
        0,
        #entries,
        #entries,
        central_size,
        local_size,
        0
    )
    return table.concat(local_records) .. table.concat(central_records) .. end_record
end

local function _ui_state_status(screenshot_captured, elements_captured)
    if screenshot_captured and elements_captured then
        return "complete"
    elseif screenshot_captured then
        return "screenshot"
    elseif elements_captured then
        return "elements"
    end
    return "unavailable"
end

local function _ui_state_value(value)
    if type(value) ~= "table" then
        return nil
    end
    return _ui_state_values[value]
end

function _ui_state_methods:status()
    local value = _ui_state_value(self)
    if not value then
        return nil, "无效的界面状态对象"
    end
    local snapshot = {
        status = value.status;
        captured_at = value.captured_at;
        capturedAt = value.captured_at;
        screenshot = {
            status = value.screenshot.status;
            width = value.screenshot.width;
            height = value.screenshot.height;
        };
        elements = {
            status = value.elements.status;
            count = value.elements.count;
        };
    }
    if value.screenshot.error then
        snapshot.screenshot.error = value.screenshot.error
    end
    if value.elements.error then
        snapshot.elements.error = value.elements.error
    end
    return snapshot
end

function _ui_state_methods:save_to_file(path)
    local value = _ui_state_value(self)
    if not value then
        return nil, "无效的界面状态对象"
    end
    if value.destroyed then
        return nil, "界面状态对象已销毁"
    end
    if type(path) ~= "string" or not string.find(path, "%S") then
        return nil, "保存路径不能为空"
    end
    local temporary_path = path .. ".xxtlc-ui-state-" .. utils.gen_uuid() .. ".tmp"
    local wrote, write_ok, write_error = pcall(file.writes, temporary_path, value.archive)
    if not wrote or not write_ok then
        pcall(file.remove, temporary_path)
        return nil, _ui_state_error_text(write_error or write_ok, "写入临时文件失败")
    end
    local moved, move_ok, move_error = pcall(file.move, temporary_path, path, "overwrite")
    if not moved or not move_ok then
        pcall(file.remove, temporary_path)
        return nil, _ui_state_error_text(move_error or move_ok, "保存文件失败")
    end
    return true
end

function _ui_state_methods:destroy()
    local value = _ui_state_value(self)
    if not value then
        return nil, "无效的界面状态对象"
    end
    value.archive = nil
    value.destroyed = true
    return true
end

-- 抓取当前截图和原始文本元素树。两项独立尝试，失败项使用规范占位内容，
-- 因而返回对象始终可以保存为标准 .xxtuie 或写入中控日志。
-- 该功能需要 ui_element 0.6.0 或更高版本；依赖不可用时应先更新 XXTouch。
---@class XXTLanControlUIStateOptions
---@field hit_test_spacing? integer 命中网格尺寸，默认 40
---@field force_hit_test? boolean 是否启用强制命中，默认 false
---@class XXTLanControlUIStateScreenshotStatus
---@field status "captured"|"unavailable"
---@field width integer
---@field height integer
---@field error? string
---@class XXTLanControlUIStateElementsStatus
---@field status "captured"|"unavailable"
---@field count integer
---@field error? string
---@class XXTLanControlUIStateStatus
---@field status "complete"|"screenshot"|"elements"|"unavailable"
---@field captured_at string
---@field capturedAt string
---@field screenshot XXTLanControlUIStateScreenshotStatus
---@field elements XXTLanControlUIStateElementsStatus
---@class XXTLanControlUIStateObject
---@field status fun(self: XXTLanControlUIStateObject): XXTLanControlUIStateStatus?, string?
---@field save_to_file fun(self: XXTLanControlUIStateObject, path: string): boolean?, string?
---@field destroy fun(self: XXTLanControlUIStateObject): boolean?, string?
---@param options? XXTLanControlUIStateOptions 抓取选项
---@return XXTLanControlUIStateObject state 界面状态对象
-- 例子
--   local state = LCC.capture_ui_state({hit_test_spacing = 40, force_hit_test = false})
--   LCC.log(1, "提交失败", state)
--   state:save_to_file(XXT_SCRIPTS_PATH.."/提交失败.xxtuie")
--   state:destroy()
function XXTLanControl.capture_ui_state(options)
    if options == nil then
        options = {}
    elseif type(options) ~= "table" then
        error("XXTLanControl.capture_ui_state 的 options 参数必须是表", 2)
    end
    local hit_test_spacing = options.hit_test_spacing
    if hit_test_spacing == nil then
        hit_test_spacing = 40
    elseif type(hit_test_spacing) ~= "number"
        or math.floor(hit_test_spacing) ~= hit_test_spacing
        or hit_test_spacing < 1
    then
        error("XXTLanControl.capture_ui_state 的 hit_test_spacing 必须是正整数", 2)
    end
    local force_hit_test = options.force_hit_test
    if force_hit_test == nil then
        force_hit_test = false
    elseif type(force_hit_test) ~= "boolean" then
        error("XXTLanControl.capture_ui_state 的 force_hit_test 必须是布尔值", 2)
    end

    local ui_element_module = _ui_state_require_ui_element()
    local captured_at = _ui_state_captured_at(sys.mtime())
    local screenshot_png, screenshot_width, screenshot_height, screenshot_error =
        _ui_state_capture_screenshot()
    local elements_json, element_count, elements_error =
        _ui_state_capture_elements(ui_element_module, hit_test_spacing, force_hit_test)
    local screenshot_captured = screenshot_png ~= nil
    local elements_captured = elements_json ~= nil
    if not screenshot_captured then
        screenshot_png = _UI_STATE_EMPTY_SCREENSHOT_PNG
        screenshot_width = 1
        screenshot_height = 1
    end
    if not elements_captured then
        elements_json = _UI_STATE_EMPTY_ELEMENTS_JSON
        element_count = 0
    end

    local device_info = {}
    local device_name = _ui_state_optional_device_value(device.name)
    local device_model = _ui_state_optional_device_value(device.type)
    if device_name then
        device_info.name = device_name
    end
    if device_model then
        device_info.model = device_model
    end
    local screenshot_manifest = {
        path = "screenshot.png";
        mediaType = "image/png";
        width = screenshot_width;
        height = screenshot_height;
        coordinateSpace = "screenshotPixels";
        zoom = 1;
        status = screenshot_captured and "captured" or "unavailable";
    }
    if screenshot_error then
        screenshot_manifest.error = screenshot_error
    end
    local elements_manifest = {
        path = "elements.json";
        mediaType = "application/json";
        count = element_count;
        status = elements_captured and "captured" or "unavailable";
    }
    if elements_error then
        elements_manifest.error = elements_error
    end
    local manifest = {
        format = "xxtlc-ui-element-capture";
        formatVersion = 1;
        capturedAt = captured_at;
        device = device_info;
        captureOptions = {
            hitTestSpacing = hit_test_spacing;
            forceHitTest = force_hit_test;
        };
        screenshot = screenshot_manifest;
        elements = elements_manifest;
    }
    local manifest_encoded, manifest_json_or_error = pcall(json.encode, manifest)
    if not manifest_encoded or type(manifest_json_or_error) ~= "string" then
        error(
            "XXTLanControl.capture_ui_state 创建清单失败："
                .. _ui_state_error_text(manifest_json_or_error, "JSON 编码失败"),
            2
        )
    end
    local manifest_json = manifest_json_or_error .. "\n"
    local created, archive_or_error = pcall(
        _ui_state_create_zip,
        manifest_json,
        screenshot_png,
        elements_json
    )
    if not created or type(archive_or_error) ~= "string" then
        error(
            "XXTLanControl.capture_ui_state 创建归档失败："
                .. _ui_state_error_text(archive_or_error, "ZIP 创建失败"),
            2
        )
    end
    if #archive_or_error > _UI_STATE_MAX_ARCHIVE_BYTES then
        error("XXTLanControl.capture_ui_state 归档超过 20 MiB 限制", 2)
    end

    local object = setmetatable({}, _ui_state_metatable)
    _ui_state_values[object] = {
        archive = archive_or_error;
        status = _ui_state_status(screenshot_captured, elements_captured);
        captured_at = captured_at;
        screenshot = {
            status = screenshot_manifest.status;
            width = screenshot_manifest.width;
            height = screenshot_manifest.height;
            error = screenshot_manifest.error;
        };
        elements = {
            status = elements_manifest.status;
            count = elements_manifest.count;
            error = elements_manifest.error;
        };
        destroyed = false;
    }
    return object
end

function _check_XXTLanControl_api_url()
    if type(_XXTLanControl_options["api-url"]) ~= 'string' then
        error("没有找到中控地址\n\n请从中控启动或使用 set_api_url 设置中控访问地址", 3)
        os.exit()
    end
end

local function _log_text_value(arg, call_line)
    local s
    if type(arg) == "table" then
        s = table.deep_dump(arg)
    else
        s = tostring(arg)
        s = string.gsub(s, "%[DATE%]", os.date("[%Y-%m-%d %H:%M:%S]"))
        if string.find(s, "[LINE]", 1, true) then
            local currentline = type(call_line) == 'number' and call_line or -1
            s = string.gsub(s, "%[LINE%]", "["..tostring(currentline).."]")
        end
    end
    return s
end

local function _log_image_data_url_mime(value)
    if type(value) ~= 'string' then
        return nil
    end
    local mime = string.match(string.lower(string.sub(value, 1, 64)), "^data:(image/%w+);base64,")
    if not mime or #value <= (#"data:" + #mime + #";base64,") then
        return nil
    end
    if mime == 'image/jpg' then
        return 'image/jpeg'
    end
    if mime == 'image/png' or mime == 'image/jpeg' then
        return mime
    end
    return nil
end

local function _log_is_inline_image(value)
    return _assist_is_image_object(value) or _log_image_data_url_mime(value) ~= nil
end

local function _log_is_inline_media(value)
    return _ui_state_value(value) ~= nil or _log_is_inline_image(value)
end

local function _log_append_payload_value(outt, parts, media, arg, call_line)
    local ui_state = _ui_state_value(arg)
    if ui_state then
        if ui_state.destroyed or type(ui_state.archive) ~= "string" then
            error("XXTLanControl.log 无法使用已销毁的界面状态对象", 3)
        end
        media[#media + 1] = {
            id = "state1";
            mime = _UI_STATE_MIME_TYPE;
            data = "data:" .. _UI_STATE_MIME_TYPE .. ";base64,"
                .. string.base64_encode(ui_state.archive);
        }
        parts[#parts + 1] = {
            type = "ui-state";
            mediaId = "state1";
        }
        outt[#outt + 1] = "[界面状态]"
    elseif _assist_is_image_object(arg) then
        local data_url, err = _assist_image_object_data_url(arg, 'png')
        if not data_url then
            error("XXTLanControl.log 图像导出失败：" .. tostring(err or ""), 3)
        end
        local media_id = "img" .. tostring(#media + 1)
        media[#media + 1] = {
            id = media_id;
            mime = "image/png";
            data = data_url;
        }
        parts[#parts + 1] = {
            type = "image";
            mediaId = media_id;
        }
        outt[#outt + 1] = "[图片]"
    else
        local data_url_mime = _log_image_data_url_mime(arg)
        if not data_url_mime then
            local s = _log_text_value(arg, call_line)
            parts[#parts + 1] = {
                type = "text";
                text = s;
            }
            outt[#outt + 1] = s
            return
        end
        local media_id = "img" .. tostring(#media + 1)
        media[#media + 1] = {
            id = media_id;
            mime = data_url_mime;
            data = arg;
        }
        parts[#parts + 1] = {
            type = "image";
            mediaId = media_id;
        }
        outt[#outt + 1] = "[图片]"
    end
end

local function _log_payload_from_values(id, values, argc, call_line)
    local outt = {}
    local parts = {}
    local media = {}
    if argc > 0 then
        for i = 1, argc do
            _log_append_payload_value(outt, parts, media, values[i], call_line)
        end
    end
    local payload = {
        id = id or 1;
        message = table.concat(outt, '\t');
    }
    if #media > 0 then
        payload.parts = parts
        payload.media = media
    end
    return payload
end

local function _log_payload(id, call_line, ...)
    local values = {...}
    return _log_payload_from_values(id, values, select("#", ...), call_line)
end

local function _log_error_message(status, response)
    local msg = "XXTLanControl.log 发送失败"
    if status ~= nil then
        msg = msg .. "（HTTP " .. tostring(status) .. "）"
    end
    local detail = tostring(response or "")
    detail = string.gsub(detail, "[\r\n]+", " ")
    if #detail > 200 then
        detail = string.sub(detail, 1, 200) .. "..."
    end
    if detail ~= "" then
        msg = msg .. "：" .. detail
    end
    return msg
end

local function _log_send_payloads(logs)
    local ui_state_count = 0
    local decoded_bytes = 0
    for _, payload in ipairs(logs) do
        if type(payload.media) == "table" then
            for _, media_item in ipairs(payload.media) do
                if media_item.mime == _UI_STATE_MIME_TYPE then
                    ui_state_count = ui_state_count + 1
                    if ui_state_count > 1 then
                        error("XXTLanControl.log 每次请求最多只能包含一份界面状态", 3)
                    end
                end
                local data = media_item.data
                local comma = type(data) == "string" and string.find(data, ",", 1, true) or nil
                if not comma then
                    error("XXTLanControl.log 媒体数据无效", 3)
                end
                local encoded = string.gsub(string.sub(data, comma + 1), "%s", "")
                local padding = 0
                if string.sub(encoded, -2) == "==" then
                    padding = 2
                elseif string.sub(encoded, -1) == "=" then
                    padding = 1
                end
                decoded_bytes = decoded_bytes + math.floor(#encoded * 3 / 4) - padding
                if decoded_bytes > _UI_STATE_MAX_ARCHIVE_BYTES then
                    error("XXTLanControl.log 图片与界面状态合计超过 20 MiB 限制", 3)
                end
            end
        end
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/devices/send-logs',
        timeout = ui_state_count > 0 and 60 or 10,
        json = {
            udid = device.udid();
            logs = logs;
        }
    })
    if c >= 200 and c < 300 then
        return
    end
    error(_log_error_message(c, r), 3)
end

local function _log_sequence_has_media(value)
    if type(value) ~= 'table' then
        return false
    end
    for _, item in ipairs(value) do
        if _log_is_inline_media(item) then
            return true
        end
    end
    return false
end

local function _log_payload_from_map_value(id, value, call_line)
    if _log_is_inline_media(value) then
        return _log_payload_from_values(id, {value}, 1, call_line)
    end
    if _log_sequence_has_media(value) then
        return _log_payload_from_values(id, value, #value, call_line)
    end
    return _log_payload_from_values(id, {value}, 1, call_line)
end

-- 中控日志，可以输出到 "日志 #1"、"日志 #2"、"日志 #3"、"日志 #4"、"日志 #5"、"日志 #6"
---@param id_or_logs integer|table 日志通道 id（1-6）或日志表
---@param ... any 其它日志内容（可变参数）
-- 例子
--   LCC.log(1, '你好世界1')
--   LCC.log(2, '你好世界2')
--   LCC.log(3, '你好世界3')
--   LCC.log(4, '你好世界4')
--   LCC.log(5, '你好世界5')
--   LCC.log(6, '你好世界6')
--   LCC.log(1, "你好", "data:image/png;base64,...", "世界")
--   LCC.log({["日志别名"] = {"你好", screen.image(100, 100, 200, 200), "世界"}})
function XXTLanControl.log(id_or_logs, ...)
    id_or_logs = _check_value(1, "integer.table", id_or_logs)
    if _ui_state_value(id_or_logs) then
        error("XXTLanControl.log 的界面状态对象不能直接作为第一个参数，请使用 LCC.log(1, state)", 2)
    end
    _check_XXTLanControl_api_url()
    local call_info = debug.getinfo(2)
    local call_line = type(call_info) == 'table' and type(call_info.currentline) == 'number' and call_info.currentline or -1
    if type(id_or_logs) == 'number' then
        if id_or_logs > 6 or id_or_logs < 1 then
            error("XXTLanControl.log 的 id 参数必须在 1 到 6 之间", 2)
            return
        end
        local log_payload = _log_payload(id_or_logs, call_line, ...)
        _log_send_payloads({ log_payload })
    else
        local aliases = _XXTLanControl_options["log-column-aliases"]
        local alias_map = {}
        if type(aliases) == 'table' then
            for k, v in pairs(aliases) do
                local id = tonumber(tostring(k):match("^log([1-6])$"))
                if id and type(v) == 'string' and v ~= '' then
                    alias_map[v] = id
                end
            end
        end
        local logs = {}
        for key, value in pairs(id_or_logs) do
            if alias_map[key] then
                logs[#logs + 1] = _log_payload_from_map_value(alias_map[key], value, call_line)
            end
        end
        if #logs == 0 then
            return
        end
        _log_send_payloads(logs)
    end
end

-- 中控文件列表
---@param path string 路径
---@param recursive? boolean 是否递归
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? list 成功返回列表
---@return string? errMsg 失败返回错误信息
-- 返回列表每项字段: name, path, dir, symlink, size
--   symlink: 条目本体是否为符号链接
--   符号链接条目在目标可访问时, dir/size 会按目标类型与大小返回
-- 用法
--   list, err = LCC.web_file.list(path, recursive, timeout)
-- 例子
--   list, err = LCC.web_file.list("/", true, 60)
function XXTLanControl.web_file.list(path, recursive, timeout)
    path = _check_value(1, "string", path)
    recursive = _opt_value(2, "boolean", false, nil, recursive)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/list',
        timeout = timeout,
        params = {
            path = path;
            recursive = recursive and 'true' or 'false';
        }
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then
            return arr
        end
    end
    return nil, r
end

-- 中控目录创建
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.mkdir(path, timeout)
-- 例子
--   ok, err = LCC.web_file.mkdir("目录名称", 60)
function XXTLanControl.web_file.mkdir(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/mkdir',
        timeout = timeout,
        params = { path = path; }
    })
    if c == 204 then
        return true
    end
    return false, r
end

-- 中控文件删除
---@param path string 路径
---@param force? boolean 是否强制删除非空目录
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 注意: 删除符号链接时只删除链接本体，不会删除链接目标内容
-- 用法
--   ok, err = LCC.web_file.delete(path, force, timeout)
-- 例子
--   ok, err = LCC.web_file.delete("文件名称.txt", false, 60)
function XXTLanControl.web_file.delete(path, force, timeout)
    path = _check_value(1, "string", path)
    force = _opt_value(2, "boolean", false, nil, force)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/delete',
        timeout = timeout,
        params = {
            path = path;
            force = force and 'true' or 'false';
        }
    })
    if c == 204 then
        return true
    end
    if c == 404 then
        return false, 'not found'
    end
    return false, r
end

-- 中控文件判断是否存在
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return "file"|"directory"|false fileType 文件类型或 false
-- 用法
--   typ = LCC.web_file.exists(path, timeout)
-- 例子
--   typ = LCC.web_file.exists("文件名称.txt", 60)
function XXTLanControl.web_file.exists(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/exists',
        timeout = timeout,
        params = {
            path = path;
        }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' and result.exists then
            return result.type == "dir" and "directory" or "file"
        end
    end
    return false
end

-- 中控获取文件尺寸
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? size 成功返回尺寸
---@return string? errMsg 失败返回错误信息
-- 用法
--   size, err = LCC.web_file.size(path, timeout)
-- 例子
--   size, err = LCC.web_file.size("文件名称.txt", 60)
function XXTLanControl.web_file.size(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/exists',
        timeout = timeout,
        params = {
            path = path;
        }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' and result.exists then
            return result.size
        end
    end
    return nil, "not found"
end

-- 中控获取文件 md5
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? md5 成功返回 md5
---@return string? errMsg 失败返回错误信息
-- 用法
--   md5, err = LCC.web_file.md5(path, timeout)
-- 例子
--   md5, err = LCC.web_file.md5("文件名称.txt", 60)
function XXTLanControl.web_file.md5(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/md5',
        timeout = timeout,
        params = { path = path; }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' then
            return result.md5
        end
    end
    return nil, r
end

-- 中控获取文件 sha1
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? sha1 成功返回 sha1
---@return string? errMsg 失败返回错误信息
-- 用法
--   sha1, err = LCC.web_file.sha1(path, timeout)
-- 例子
--   sha1, err = LCC.web_file.sha1("文件名称.txt", 60)
function XXTLanControl.web_file.sha1(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/sha1',
        timeout = timeout,
        params = { path = path; }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' then
            return result.sha1
        end
    end
    return nil, r
end

-- 中控获取文件 crc32
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? crc32 成功返回 crc32
---@return string? errMsg 失败返回错误信息
-- 用法
--   crc32, err = LCC.web_file.crc32(path, timeout)
-- 例子
--   crc32, err = LCC.web_file.crc32("文件名称.txt", 60)
function XXTLanControl.web_file.crc32(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/crc32',
        timeout = timeout,
        params = { path = path; }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' then
            return result.crc32
        end
    end
    return nil, r
end

local function _web_file_reads(path, timeout, level_plus)
    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/ops/read',
        timeout = timeout,
        params = {
            path = path;
        }
    })
    if c == 200 then
        return r
    end
    return nil, r
end

-- 中控文件读取
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? data 成功返回数据
---@return string? errMsg 失败返回错误信息
-- 用法
--   data, err = LCC.web_file.reads(path, timeout)
-- 例子
--   data, err = LCC.web_file.reads("文件名称.txt", 60)
function XXTLanControl.web_file.reads(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_reads(path, timeout)
end

local function _web_file_writes(path, data, timeout, level_plus)
    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/ops/write',
        timeout = timeout,
        headers = {
            ['Content-Type'] = 'application/octet-stream';
        },
        params = {
            path = path;
        },
        data = data
    })
    if c == 204 then
        return true
    end
    return false, r
end

-- 中控文件写入
---@param path string 路径
---@param data string 内容
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.writes(path, data, timeout)
-- 例子
--   ok, err = LCC.web_file.writes("文件名称.txt", '你好世界', 60)
function XXTLanControl.web_file.writes(path, data, timeout)
    path = _check_value(1, "string", path)
    data = _check_value(2, "string", nil, data)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_writes(path, data, timeout)
end

-- 中控文件追加
---@param path string 路径
---@param data string 内容
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.appends(path, data, timeout)
-- 例子
--   ok, err = LCC.web_file.appends("文件名称.txt", '你好世界', 60)
function XXTLanControl.web_file.appends(path, data, timeout)
    path = _check_value(1, "string", path)
    data = _check_value(2, "string", nil, data)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/ops/append',
        timeout = timeout,
        headers = {
            ['Content-Type'] = 'application/octet-stream';
        },
        params = {
            path = path;
        },
        data = data
    })
    if c == 204 then
        return true
    end
    return false, r
end

local function _backup_download_target(path)
    if file.exists(path) ~= 'file' then
        return nil
    end
    local backup_base = path .. '.xxtlc-download-backup-' .. utils.gen_uuid()
    local backup_path = backup_base
    local suffix = 0
    while file.exists(backup_path) do
        suffix = suffix + 1
        backup_path = backup_base .. '-' .. tostring(suffix)
    end
    local ok, err = file.move(path, backup_path)
    if not ok then
        return false, err or 'backup target file failed'
    end
    return backup_path
end

local function _normalize_download_path(path)
    local resolved = file.path.resolve_symlinks(path)
    if type(resolved) == 'string' and resolved ~= '' then
        return resolved
    end
    local normalized = file.path.normalize(path)
    if type(normalized) == 'string' and normalized ~= '' then
        return normalized
    end
    return path
end

local function _is_download_target(response_path, target_path)
    response_path = tostring(response_path or '')
    if response_path == '' or file.exists(target_path) ~= 'file' then
        return false
    end
    return _normalize_download_path(response_path) == _normalize_download_path(target_path)
end

local function _restore_download_backup(backup_path, target_path, err)
    if backup_path then
        local ok, restore_err = file.move(backup_path, target_path, 'overwrite')
        if not ok then
            err = err .. "\nrestore backup failed: " .. tostring(restore_err or "unknown error")
        end
    end
    return err
end

-- 中控文件下载
---@param path string 路径
---@param target_path string 设备上的目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.download_file(path, target_path, timeout)
-- 例子
--   ok, err = LCC.web_file.download_file("文件名称.txt", XXT_SCRIPTS_PATH..'/文件名称.txt', 60)
function XXTLanControl.web_file.download_file(path, target_path, timeout)
    path = _check_value(1, "string", path)
    target_path = _check_value(2, "string", nil, target_path)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local backup_path, backup_err = _backup_download_target(target_path)
    if backup_path == false then
        return false, tostring(backup_err)
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/read',
        timeout = timeout,
        params = {
            path = path;
        },

        download_file = target_path,
    })
    if c == 200 then
        if _is_download_target(r, target_path) then
            if backup_path then
                file.remove(backup_path)
            end
            return true
        end
        r = tostring(r)
        if file.exists(r) == 'file' then
            file.remove(r)
        end
        return false, _restore_download_backup(backup_path, target_path, 'download file was not moved to target path')
    end
    r = tostring(r)
    local err = r ~= '' and r or 'unknown error'
    local err_file = file.exists(r) == 'file' and r or target_path
    if file.exists(err_file) == 'file' then
        err = file.reads(err_file) or err
        file.remove(err_file)
    end
    if c == 404 then
        err = 'not found'
    end
    return false, _restore_download_backup(backup_path, target_path, err)
end

-- 中控文件上传
---@param local_path string 设备上的本地文件路径
---@param remote_path string 中控端文件路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.upload_file(local_path, remote_path, timeout)
-- 例子
--   ok, err = LCC.web_file.upload_file(XXT_SCRIPTS_PATH..'/文件名称.txt', '文件名称.txt', 60)
function XXTLanControl.web_file.upload_file(local_path, remote_path, timeout)
    local_path = _check_value(1, "string", local_path)
    remote_path = _check_value(2, "string", nil, remote_path)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/write',
        timeout = timeout,
        headers = {
            ['Content-Type'] = 'application/octet-stream';
        },
        params = {
            path = remote_path;
        },

        upload_file = local_path,
    })
    if c == 204 then
        return true
    end
    return false, r
end

-- 中控文件行列表读取
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return string[]? lines 成功返回行列表
---@return string? errMsg 失败返回错误信息
-- 用法
--   lines, err = LCC.web_file.get_lines(path, timeout)
-- 例子
--   lines, err = LCC.web_file.get_lines("文件名称.txt", 60)
function XXTLanControl.web_file.get_lines(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local data, err = _web_file_reads(path, timeout, 1)
    if data then
        return data:gsub("\r\n", "\n"):split("\n")
    end
    return nil, err
end

-- 中控文件行数
---@param path string 路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? count 成功返回行数
---@return string? errMsg 失败返回错误信息
-- 用法
--   count, err = LCC.web_file.line_count(path, timeout)
-- 例子
--   count, err = LCC.web_file.line_count("文件名称.txt", 60)
function XXTLanControl.web_file.line_count(path, timeout)
    path = _check_value(1, "string", path)
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("get", {
        url = _XXTLanControl_options["api-url"] .. '/api/files/lines/count',
        timeout = timeout,
        params = {
            path = path;
        }
    })
    if c == 200 then
        local result = json.decode(r)
        if type(result) == 'table' and type(result.lines) == 'number' then
            return result.lines
        end
    end
    return nil, r
end

-- 中控文件行列表写入
---@param path string 路径
---@param lines string[] 行列表
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.set_lines(path, lines, timeout)
-- 例子
--   ok, err = LCC.web_file.set_lines("文件名称.txt", {'你好世界1', '你好世界2'}, 60)
function XXTLanControl.web_file.set_lines(path, lines, timeout)
    path = _check_value(1, "string", path)
    lines = _check_value(2, "table", nil, lines)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local data = table.concat(lines, "\r\n")
    return _web_file_writes(path, data, timeout)
end

local function _parse_lines_response(headers, payload)
    local line_count
    if type(headers) == 'string' then
        local parsed_headers = json.decode(headers)
        if type(parsed_headers) == 'table' then
            headers = parsed_headers
        end
    end
    if type(headers) == 'table' then
        line_count = headers["X-Lines-Count"] or headers["x-lines-count"]
        if line_count == nil then
            for k, v in pairs(headers) do
                if type(k) == 'string' and k:lower() == 'x-lines-count' then
                    line_count = v
                    break
                end
            end
        end
    end
    line_count = tonumber(line_count)
    local text = tostring(payload or ""):gsub("\r\n", "\n")
    if text == "" then
        if type(line_count) == 'number' and line_count > 0 then
            local lines = {}
            for i = 1, line_count do
                lines[i] = ""
            end
            return lines
        end
        return {}
    end
    local lines = text:split("\n")
    if type(line_count) == 'number' then
        while #lines > line_count and lines[#lines] == "" do
            table.remove(lines, #lines)
        end
        for i = #lines + 1, line_count do
            lines[i] = ""
        end
    end
    return lines
end

local function _web_file_get_range_lines(path, start_line, count, timeout, level_plus)
    local params = { path = path }
    if start_line then
        params.start_line = tostring(start_line)
    end
    if count then
        params.count = tostring(count)
    end

    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/lines/get-range',
        timeout = timeout or 60,
        params = params
    })
    if c == 200 then
        return _parse_lines_response(h, r)
    end
    return nil, r
end

-- 中控文件行范围读取
---@param path string 路径
---@param start_line? integer 起始行号（可选，默认 1）
---@param count? integer 获取行数（可选，默认 -1 表示到末尾）
---@param timeout? number 超时秒，可选参数，默认 60
---@return string[]? lines 成功返回行列表
---@return string? errMsg 失败返回错误信息
-- 用法
--   lines, err = LCC.web_file.get_range_lines(path, start_line, count, timeout)
function XXTLanControl.web_file.get_range_lines(path, start_line, count, timeout)
    path = _check_value(1, "string", path)
    start_line = _opt_value(2, "number", 1, nil, start_line)
    count = _opt_value(3, "number", -1, nil, nil, count)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_get_range_lines(path, start_line, count, timeout)
end

-- 中控文件单行获取
---@param path string 路径
---@param line_number integer 行号
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? line 成功返回行
---@return string? errMsg 失败返回错误信息
-- 用法
--   line, err = LCC.web_file.get_line(path, line_number, timeout)
-- 例子
--   line, err = LCC.web_file.get_line("文件名称.txt", 1, 60)
function XXTLanControl.web_file.get_line(path, line_number, timeout)
    path = _check_value(1, "string", path)
    line_number = _check_value(2, "number", nil, line_number)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local lines, err = _web_file_get_range_lines(path, line_number, 1, timeout, 1)
    if type(lines) == 'table' then
        return lines[1] or ""
    end
    return nil, err
end

local function _web_file_set_range_lines(path, start_line, lines, timeout, level_plus)
    local params = { path = path }
    if start_line then
        params.start_line = tostring(start_line)
    end
    if type(lines) == 'table' then
        lines = table.concat(lines, "\r\n")
    end

    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/lines/set-range',
        timeout = timeout or 60,
        headers = {
            ['Content-Type'] = 'application/octet-stream';
        },
        params = params,
        data = lines
    })
    if c == 200 then
        return true, _parse_lines_response(h, r)
    end
    return false, r
end

-- 中控文件行范围写入
---@param path string 路径
---@param start_line integer 起始行号
---@param lines string[] 行列表
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string[]|string linesOrErr 成功返回写入后的行列表，失败返回错误信息
-- 用法
--   ok, lines_or_err = LCC.web_file.set_range_lines(path, start_line, lines, timeout)
function XXTLanControl.web_file.set_range_lines(path, start_line, lines, timeout)
    path = _check_value(1, "string", path)
    start_line = _check_value(2, "number", nil, start_line)
    lines = _check_value(3, "table", nil, nil, lines)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_set_range_lines(path, start_line, lines, timeout)
end

-- 中控文件单行写入
---@param path string 路径
---@param line_number integer 行号
---@param line string 内容
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? lineOrErr 成功返回行内容，失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.set_line(path, line_number, line, timeout)
-- 例子
--   ok, err = LCC.web_file.set_line("文件名称.txt", 1, '你好世界', 60)
function XXTLanControl.web_file.set_line(path, line_number, line, timeout)
    path = _check_value(1, "string", path)
    line_number = _check_value(2, "number", nil, line_number)
    line = _check_value(3, "string", nil, nil, line)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local ok, lines = _web_file_set_range_lines(path, line_number, { line }, timeout, 1)
    if ok and type(lines) == 'table' then
        return true, lines[1]
    end
    return false, lines
end

local function _web_file_insert_lines(path, start_line, lines, timeout, level_plus)
    local params = { path = path }
    if start_line then
        params.start_line = tostring(start_line)
    end
    if type(lines) == 'table' then
        local s = table.concat(lines, "\r\n")
        lines = (#lines > 0) and (s .. "\r\n") or ""
    end

    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/lines/insert',
        timeout = timeout or 60,
        headers = {
            ['Content-Type'] = 'application/octet-stream';
        },
        params = params,
        data = lines
    })
    if c == 204 then
        return true
    end
    return false, r
end

-- 中控文件插入多行列表
---@param path string 路径
---@param start_line integer 行号
---@param lines string[] 行列表
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.insert_lines(path, start_line, lines, timeout)
-- 例子
--   ok, err = LCC.web_file.insert_lines("文件名称.txt", 1, {'你好世界1', '你好世界2'}, 60)
function XXTLanControl.web_file.insert_lines(path, start_line, lines, timeout)
    path = _check_value(1, "string", path)
    start_line = _check_value(2, "number", nil, start_line)
    lines = _check_value(3, "table", nil, nil, lines)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_insert_lines(path, start_line, lines, timeout)
end

-- 中控文件单行插入
---@param path string 路径
---@param line_number integer 行号
---@param line string 内容
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.insert_line(path, line_number, line, timeout)
-- 例子
--   ok, err = LCC.web_file.insert_line("文件名称.txt", 1, '你好世界', 60)
function XXTLanControl.web_file.insert_line(path, line_number, line, timeout)
    path = _check_value(1, "string", path)
    line_number = _check_value(2, "number", nil, line_number)
    line = _check_value(3, "string", nil, nil, line)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_insert_lines(path, line_number, { line }, timeout)
end

local function _web_file_remove_lines(path, start_line, count, timeout, level_plus)
    local params = { path = path }
    if start_line then
        params.start_line = tostring(start_line)
    end
    if count then
        params.count = tostring(count)
    end

    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/lines/remove',
        timeout = timeout or 60,
        params = params
    })
    if c == 200 then
        return _parse_lines_response(h, r)
    end
    return nil, r
end

-- 中控文件删除多行
---@param path string 路径
---@param start_line integer 起始行号
---@param count? integer 删除行数，可选参数，默认 1
---@param timeout? number 超时秒，可选参数，默认 60
---@return string[]? lines 成功返回被删除的行列表
---@return string? errMsg 失败返回错误信息
-- 用法
--   lines, err = LCC.web_file.remove_lines(path, start_line, count, timeout)
function XXTLanControl.web_file.remove_lines(path, start_line, count, timeout)
    path = _check_value(1, "string", path)
    start_line = _check_value(2, "number", nil, start_line)
    count = _opt_value(3, "number", 1, nil, nil, count)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_remove_lines(path, start_line, count, timeout)
end

-- 中控文件单行删除
---@param path string 路径
---@param line integer 行号
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? line 成功返回删除的文件行
---@return string? errMsg 失败返回错误信息
-- 用法
--   line, err = LCC.web_file.remove_line(path, line, timeout)
-- 例子
--   line, err = LCC.web_file.remove_line("文件名称.txt", 1, 60)
function XXTLanControl.web_file.remove_line(path, line, timeout)
    path = _check_value(1, "string", path)
    line = _check_value(2, "number", nil, line)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local lines, err = _web_file_remove_lines(path, line, 1, timeout, 1)
    if lines then
        return lines[1]
    end
    return nil, err
end

local function _web_file_copy(src, target, mode, timeout, level_plus)
    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/copy',
        timeout = timeout or 60,
        params = {
            src = src;
            target = target;
            mode = mode;
        }
    })
    if c == 204 then
        return true
    end
    return nil, r
    
end

-- 中控文件拷贝
---@param src string 源路径
---@param target string 目标路径
---@param mode? string 模式，可选 "normal" | "overwrite" | "merge" | "overwrite-merge"，默认 "normal"
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 注意: 当 src 为符号链接时，复制的是符号链接本体（链接目标路径），不是目标内容
-- 用法
--   ok, err = LCC.web_file.copy(src, target, mode, timeout)
-- 例子
--   ok, err = LCC.web_file.copy("文件名称1.txt", '文件名称2.txt', "normal", 60)
function XXTLanControl.web_file.copy(src, target, mode, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    mode = _opt_value(3, "string", "normal", nil, nil, mode)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_copy(src, target, mode, timeout)
end

-- 中控文件拷贝（覆盖，会删除原来的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.copy_overwrite(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.copy_overwrite("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.copy_overwrite(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_copy(src, target, 'overwrite', timeout)
end

-- 中控文件拷贝（合并，会合并目标目录中没有的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.copy_merge(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.copy_merge("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.copy_merge(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_copy(src, target, 'merge', timeout)
end

-- 中控文件拷贝（覆盖合并，会强行覆盖所有源路径有的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.copy_overwrite_merge(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.copy_overwrite_merge("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.copy_overwrite_merge(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_copy(src, target, 'overwrite-merge', timeout)
end

local function _web_file_move(src, target, mode, timeout, level_plus)
    local c, h, r = _XXTLanControl_request("post", {
        level_plus = level_plus or 0,
        url = _XXTLanControl_options["api-url"] .. '/api/files/move',
        timeout = timeout or 60,
        params = {
            src = src;
            target = target;
            mode = mode;
        }
    })
    if c == 204 then
        return true
    end
    return nil, r
end

-- 中控文件移动
---@param src string 源路径
---@param target string 目标路径
---@param mode? string 模式，可选 "normal" | "overwrite" | "merge" | "overwrite-merge"，默认 "normal"
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 注意: 当 src 为符号链接时，移动的是符号链接本体，不会改写或删除其目标内容
-- 用法
--   ok, err = LCC.web_file.move(src, target, mode, timeout)
-- 例子
--   ok, err = LCC.web_file.move("文件名称1.txt", '文件名称2.txt', "normal", 60)
function XXTLanControl.web_file.move(src, target, mode, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    mode = _opt_value(3, "string", "normal", nil, nil, mode)
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_move(src, target, mode, timeout)
end

-- 中控文件移动（覆盖，会删除原来的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.move_overwrite(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.move_overwrite("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.move_overwrite(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_move(src, target, 'overwrite', timeout)
end

-- 中控文件移动（合并，会合并目标目录中没有的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.move_merge(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.move_merge("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.move_merge(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_move(src, target, 'merge', timeout)
end

-- 中控文件移动（覆盖合并，会强行覆盖所有源路径有的文件）
---@param src string 源路径
---@param target string 目标路径
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean? ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 用法
--   ok, err = LCC.web_file.move_overwrite_merge(src, target, timeout)
-- 例子
--   ok, err = LCC.web_file.move_overwrite_merge("文件名称1.txt", '文件名称2.txt', 60)
function XXTLanControl.web_file.move_overwrite_merge(src, target, timeout)
    src = _check_value(1, "string", src)
    target = _check_value(2, "string", nil, target)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    return _web_file_move(src, target, 'overwrite-merge', timeout)
end

-- KVDB 封装
XXTLanControl.kvdb = {}
XXTLanControl.kvdb.queue = {}
XXTLanControl.kvdb.dict = {}

-- KVDB 表名验证（与 Go 后端保持一致）
local function _check_kvdb_name(name)
    if #name == 0 then
        error("KVDB name cannot be empty", 3)
    end
    -- 十六进制编码后的长度限制：原始字符串最大 256 字节
    if #name > 256 then
        error("KVDB name too long (max 256 bytes)", 3)
    end
    return name
end

local function _check_kvdb_key(key)
    if #key == 0 then
        error("KVDB key cannot be empty", 3)
    end
    if not utf8.len(key) then
        error("KVDB key must be valid UTF-8", 3)
    end
    return key
end

local function _check_kvdb_value(value)
    if not utf8.len(value) then
        error("KVDB value must be valid UTF-8", 3)
    end
    return value
end

local function _check_kvdb_value_list(values)
    if type(values) ~= 'table' then
        error("KVDB values must be a table", 3)
    end
    local count = 0
    local max_index = 0
    for k, v in pairs(values) do
        if type(k) ~= 'number' or k < 1 or k ~= math.floor(k) then
            error("KVDB values must be a sequence", 3)
        end
        if type(v) ~= 'string' then
            error("KVDB values must be strings", 3)
        end
        count = count + 1
        if k > max_index then max_index = k end
    end
    if count ~= max_index then
        error("KVDB values must be a sequence", 3)
    end
    local result = {}
    for i = 1, max_index do
        result[i] = _check_kvdb_value(values[i])
    end
    return result
end

local function _check_kvdb_queue_pop_options(options)
    if type(options) ~= 'table' then
        error("KVDB queue pop options must be a table", 3)
    end
    local name = _check_kvdb_name(_check_value(1, "string", options.name))
    local max = options.max
    if type(max) ~= 'number' or max ~= math.floor(max) or max < 1 then
        error("KVDB queue pop max must be a positive integer", 3)
    end
    max = math.floor(max)
    return name, max
end

-- 中控 KVDB 推入队列（前端）
---@param name string 队列名称
---@param value string|string[] 值，或按顺序批量推入的值列表
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? size 成功返回队列尺寸
---@return string? errMsg 失败返回错误信息
-- 用法
--   size, err = LCC.kvdb.queue.push_front(name, value, timeout)
-- 例子
--   size, err = LCC.kvdb.queue.push_front("表名", "值", 60)
function XXTLanControl.kvdb.queue.push_front(name, value, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    local is_batch = type(value) == 'table'
    if is_batch then
        value = _check_kvdb_value_list(value)
    else
        value = _check_kvdb_value(_check_value(2, "string", nil, value))
    end
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    if is_batch then
        if #value == 0 then
            return XXTLanControl.kvdb.queue.size(name, timeout)
        end
        local c, h, r = _XXTLanControl_request("post", {
            url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/push-front-batch',
            timeout = timeout,
            params = { name = name; },
            json = value,
        })
        if c == 200 then
            local obj = json.decode(r)
            if type(obj) == 'table' then
                return tonumber(obj.size)
            end
        end
        return nil, r
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/push-front',
        timeout = timeout,
        headers = { ['Content-Type'] = 'application/octet-stream' },
        params = { name = name; },
        data = value,
    })
    if c == 200 then
        return tonumber(r)
    end
    return nil, r
end

-- 中控 KVDB 推入队列（后端）
---@param name string 队列名称
---@param value string|string[] 值，或按顺序批量推入的值列表
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? size 成功返回队列尺寸
---@return string? errMsg 失败返回错误信息
-- 用法
--   size, err = LCC.kvdb.queue.push_back(name, value, timeout)
function XXTLanControl.kvdb.queue.push_back(name, value, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    local is_batch = type(value) == 'table'
    if is_batch then
        value = _check_kvdb_value_list(value)
    else
        value = _check_kvdb_value(_check_value(2, "string", nil, value))
    end
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    if is_batch then
        if #value == 0 then
            return XXTLanControl.kvdb.queue.size(name, timeout)
        end
        local c, h, r = _XXTLanControl_request("post", {
            url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/push-back-batch',
            timeout = timeout,
            params = { name = name; },
            json = value,
        })
        if c == 200 then
            local obj = json.decode(r)
            if type(obj) == 'table' then
                return tonumber(obj.size)
            end
        end
        return nil, r
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/push-back',
        timeout = timeout,
        headers = { ['Content-Type'] = 'application/octet-stream' },
        params = { name = name; },
        data = value,
    })
    if c == 200 then
        return tonumber(r)
    end
    return nil, r
end

-- 中控 KVDB 弹出队列（前端）
---@param name string|{name:string,max:integer} 队列名称；传入 table 时批量弹出
---@param timeout? number 超时秒，可选参数，默认 60
---@return string|string[]|nil value 成功返回被弹出的值；批量模式返回列表
---@return string? errMsg 失败返回错误信息
-- 用法
--   value, err = LCC.kvdb.queue.pop_front(name, timeout)
-- 例子
--   value, err = LCC.kvdb.queue.pop_front("表名", 60)
function XXTLanControl.kvdb.queue.pop_front(name, timeout)
    local is_batch = type(name) == 'table'
    local max
    if is_batch then
        name, max = _check_kvdb_queue_pop_options(name)
        timeout = _opt_value(2, "number", 60, nil, timeout)
    else
        name = _check_kvdb_name(_check_value(1, "string", name))
        timeout = _opt_value(2, "number", 60, nil, timeout)
    end
    _check_XXTLanControl_api_url()
    if is_batch then
        local c, h, r = _XXTLanControl_request("post", {
            url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/pop-front-batch',
            timeout = timeout,
            params = { name = name; max = tostring(max); },
        })
        if c == 200 then
            local arr = json.decode(r)
            if type(arr) == 'table' then
                return arr
            end
        end
        return nil, r
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/pop-front',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        return r
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- 中控 KVDB 弹出队列（后端）
---@param name string|{name:string,max:integer} 队列名称；传入 table 时批量弹出
---@param timeout? number 超时秒，可选参数，默认 60
---@return string|string[]|nil value 成功返回被弹出的值；批量模式返回列表
---@return string? errMsg 失败返回错误信息
-- 用法
--   value, err = LCC.kvdb.queue.pop_back(name, timeout)
-- 例子
--   value, err = LCC.kvdb.queue.pop_back("表名", 60)
function XXTLanControl.kvdb.queue.pop_back(name, timeout)
    local is_batch = type(name) == 'table'
    local max
    if is_batch then
        name, max = _check_kvdb_queue_pop_options(name)
        timeout = _opt_value(2, "number", 60, nil, timeout)
    else
        name = _check_kvdb_name(_check_value(1, "string", name))
        timeout = _opt_value(2, "number", 60, nil, timeout)
    end
    _check_XXTLanControl_api_url()
    if is_batch then
        local c, h, r = _XXTLanControl_request("post", {
            url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/pop-back-batch',
            timeout = timeout,
            params = { name = name; max = tostring(max); },
        })
        if c == 200 then
            local arr = json.decode(r)
            if type(arr) == 'table' then
                return arr
            end
        end
        return nil, r
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/pop-back',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        return r
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- 中控 KVDB 移动队列元素
---@param from_name string 源队列名称
---@param pop string 源队列弹出方向（front/back）
---@param to_name string 目标队列名称
---@param push string 目标队列推入方向（front/back）
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? value 成功返回被弹出的值
---@return string? errMsg 失败返回错误信息
-- 用法
--   val, err = LCC.kvdb.queue.move(from_name, pop, to_name, push, timeout)
-- 例子
--   val, err = LCC.kvdb.queue.move("表名", "front", "表名", "back", 60)
function XXTLanControl.kvdb.queue.move(from_name, pop, to_name, push, timeout)
    from_name = _check_kvdb_name(_check_value(1, "string", from_name))
    pop = _opt_value(2, "string", "front", nil, pop)
    to_name = _check_kvdb_name(_check_value(3, "string", nil, nil, to_name))
    push = _opt_value(4, "string", "back", nil, nil, nil, push)
    timeout = _opt_value(5, "number", 60, nil, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/move',
        timeout = timeout,
        params = { fromName = from_name; toName = to_name; pop = pop; push = push; },
    })
    if c == 200 then
        return r
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- 中控 KVDB 按值删除并返回删除数量
---@param name string 队列名称
---@param value string 值
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? count 成功返回删除数量
---@return string? errMsg 失败返回错误信息
-- 用法
--   count, err = LCC.kvdb.queue.pop_values(name, value, timeout)
function XXTLanControl.kvdb.queue.pop_values(name, value, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    value = _check_kvdb_value(_check_value(2, "string", nil, value))
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/pop-values',
        timeout = timeout,
        params = { name = name; value = value; },
    })
    if c == 200 then
        return tonumber(r)
    end
    return nil, r
end

-- 中控 KVDB 列出队列
---@param name string 队列名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return string[]? list 成功返回队列
---@return string? errMsg 失败返回错误信息
-- 用法
--   list, err = LCC.kvdb.queue.list(name, timeout)
-- 例子
--   list, err = LCC.kvdb.queue.list("表名", 60)
function XXTLanControl.kvdb.queue.list(name, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/list',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then
            return arr
        end
    end
    return nil, r
end

-- 中控 KVDB 清空队列
---@param name string 队列名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return string[]? list 成功返回清空的队列
---@return string? errMsg 失败返回错误信息
-- 用法
--   list, err = LCC.kvdb.queue.clear(name, timeout)
-- 例子
--   list, err = LCC.kvdb.queue.clear("表名", 60)
function XXTLanControl.kvdb.queue.clear(name, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/clear-all',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then
            return arr
        end
    end
    return nil, r
end

-- 中控 KVDB 统计队列某值个数
---@param name string 队列名称
---@param value string 值
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? count 成功返回该值的个数
---@return string? errMsg 失败返回错误信息
-- 用法
--   count, err = LCC.kvdb.queue.count(name, value, timeout)
-- 例子
--   count, err = LCC.kvdb.queue.count("表名", "值", 60)
function XXTLanControl.kvdb.queue.count(name, value, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    value = _check_kvdb_value(_check_value(2, "string", nil, value))
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/count',
        timeout = timeout,
        params = { name = name; value = value; },
    })
    if c == 200 then
        return tonumber(r)
    end
    return nil, r
end

-- 中控 KVDB 获取队列尺寸
---@param name string 队列名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? size 成功返回队列尺寸
---@return string? errMsg 失败返回错误信息
-- 用法
--   size, err = LCC.kvdb.queue.size(name, timeout)
function XXTLanControl.kvdb.queue.size(name, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/queue/size',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        return tonumber(r)
    end
    return nil, r
end

-- 中控 KVDB 设置词典值
---@param name string 词典名称
---@param key string 键
---@param value string 值
---@param timeout? number 超时秒，可选参数，默认 60
---@return string|false|nil originalValue 成功返回原值
---@return string? errMsg 失败返回错误信息
-- 用法
--   original_value, err = LCC.kvdb.dict.put(name, key, value, timeout)
-- 例子
--   original_value, err = LCC.kvdb.dict.put("表名", "键", "值", 60)
function XXTLanControl.kvdb.dict.put(name, key, value, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    key = _check_kvdb_key(_check_value(2, "string", nil, key))
    value = _check_kvdb_value(_check_value(3, "string", nil, nil, value))
    timeout = _opt_value(4, "number", 60, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/put',
        timeout = timeout,
        headers = { ['Content-Type'] = 'application/octet-stream' },
        params = { name = name; key = key; },
        data = value,
    })
    if c == 200 then
        return r
    end
    if c == 204 then
        return nil, nil
    end
    return false, r
end

-- 中控 KVDB 词典值移动
---@param src_name string 源词典名称
---@param src_key string 源键
---@param dst_name? string 目标词典名称
---@param dst_key? string 目标键
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 移动成败
---@return string? dstValueOrErr 被移动替换的目标位置的原值或错误信息
-- @note src_name == dst_name 并且 src_key == dst_key 时，将抛出错误
-- 用法
--   ok, dst_value_or_err = LCC.kvdb.dict.move_value(src_name, src_key, dst_name, dst_key, timeout)
-- 例子
--   ok, dst_value_or_err = LCC.kvdb.dict.move_value("表名1", "键1", "表名2", "键2", 60)
function XXTLanControl.kvdb.dict.move_value(src_name, src_key, dst_name, dst_key, timeout)
    src_name = _check_kvdb_name(_check_value(1, "string", src_name))
    src_key = _check_kvdb_key(_check_value(2, "string", nil, src_key))
    dst_name = _check_kvdb_name(_opt_value(3, "string", src_name, nil, nil, dst_name))
    dst_key = _check_kvdb_key(_opt_value(4, "string", src_key, nil, nil, nil, dst_key))
    timeout = _opt_value(5, "number", 60, nil, nil, nil, nil, timeout)
    if dst_name == src_name and dst_key == src_key then
        error("same name and key", 2)
        return false, "same name and key"
    end
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/move-value',
        timeout = timeout,
        params = { srcName = src_name; srcKey = src_key; dstName = dst_name; dstKey = dst_key; },
    })
    if c == 200 then
        return true, r
    end
    if c == 204 then
        return true, nil
    end
    return false, r
end

-- 中控 KVDB 词典值复制
---@param src_name string 源词典名称
---@param src_key string 源键
---@param dst_name? string 目标词典名称
---@param dst_key? string 目标键
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 复制成败
---@return string? dstValueOrErr 被复制替换的目标位置的原值或错误信息
-- @note src_name == dst_name 并且 src_key == dst_key 时，将返回 true, 原值，并且无状态变更
-- 用法
--   ok, dst_value_or_err = LCC.kvdb.dict.copy_value(src_name, src_key, dst_name, dst_key, timeout)
-- 例子
--   ok, dst_value_or_err = LCC.kvdb.dict.copy_value("表名1", "键1", "表名2", "键2", 60)
function XXTLanControl.kvdb.dict.copy_value(src_name, src_key, dst_name, dst_key, timeout)
    src_name = _check_kvdb_name(_check_value(1, "string", src_name))
    src_key = _check_kvdb_key(_check_value(2, "string", nil, src_key))
    dst_name = _check_kvdb_name(_opt_value(3, "string", src_name, nil, nil, dst_name))
    dst_key = _check_kvdb_key(_opt_value(4, "string", src_key, nil, nil, nil, dst_key))
    timeout = _opt_value(5, "number", 60, nil, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/copy-value',
        timeout = timeout,
        params = { srcName = src_name; srcKey = src_key; dstName = dst_name; dstKey = dst_key; },
    })
    if c == 200 then
        return true, r
    end
    if c == 204 then
        return true, nil
    end
    return false, r
end

-- 中控 KVDB 原子整数增减
---@param name string 词典名称
---@param key string 键
---@param by? integer 增减量（可以为负数）
---@param init? integer 初始值，如果键不存在则以 init 作为基值，可选参数，默认 0
---@param min? integer 最小值，如果自增后的范围小于这个值，会被钳制为该值，可选参数
---@param max? integer 最大值，如果自增后的范围大于这个值，会被钳制为该值，可选参数
---@param coerce? boolean 是否强制将不是整数的旧值作为 init 然后自增，可选参数，默认 false，如果旧值不是整数则失败
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? newValue 成功返回新值
---@return table|string? metaOrErr 成功返回元信息，失败返回错误信息
-- 用法
--   new_value, err = LCC.kvdb.dict.incr(name, key, by, init, min, max, coerce, timeout)
-- 例子
--   new_value, err = LCC.kvdb.dict.incr("表名", "键", 1, 0, -100, 100, true, 60)
function XXTLanControl.kvdb.dict.incr(name, key, by, init, min, max, coerce, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    key = _check_kvdb_key(_check_value(2, "string", nil, key))
    by = _opt_value(3, "integer", 1, nil, nil, by)
    init = _opt_value(4, "integer", 0, nil, nil, nil, init)
    -- 第 5、6 参数为可选边界
    if min ~= nil then min = _check_value(5, "integer", nil, nil, nil, nil, min) end
    if max ~= nil then max = _check_value(6, "integer", nil, nil, nil, nil, nil, max) end
    coerce = _opt_value(7, "boolean", false, nil, nil, nil, nil, nil, nil, coerce)
    timeout = _opt_value(8, "number", 60, nil, nil, nil, nil, nil, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local params = { name = name; key = key; by = tostring(by); init = tostring(init); coerce = coerce and 'true' or 'false'; }
    if min ~= nil then params.min = tostring(min) end
    if max ~= nil then params.max = tostring(max) end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/incr',
        timeout = timeout,
        params = params,
    })
    if c == 200 then
        h = json.decode(h) or {}
        return tonumber(r), {
            created = h['X-Kvdb-Existed'] == 'false',
            coerced = h['X-Kvdb-Coerced'] == 'true',
            clamped = h['X-Kvdb-Clamped'] == 'true',
            clamp_reason = h['X-Kvdb-Clamp-Reason'],
            original_value = tonumber(h['X-Kvdb-Original-Value']),
            computed_value = tonumber(h['X-Kvdb-Computed-Value']),
            result_value = tonumber(h['X-Kvdb-Result-Value']),
        }
    end
    return nil, r
end

-- 中控 KVDB 获取词典值
---@param name string 词典名称
---@param key string 键
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? value 成功返回值
---@return string? errMsg 失败返回错误信息
-- 用法
--   value, err = LCC.kvdb.dict.get(name, key, timeout)
-- 例子
--   value, err = LCC.kvdb.dict.get("表名", "键", 60)
function XXTLanControl.kvdb.dict.get(name, key, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    key = _check_kvdb_key(_check_value(2, "string", nil, key))
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/get',
        timeout = timeout,
        params = { name = name; key = key; },
    })
    if c == 200 then
        return r
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- 中控 KVDB 获取词典所有值
---@param name string 词典名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? dict 成功返回值
---@return string? errMsg 失败返回错误信息
-- 用法
--   list, err = LCC.kvdb.dict.get_all(name, timeout)
-- 例子
--   list, err = LCC.kvdb.dict.get_all("表名", 60)
function XXTLanControl.kvdb.dict.get_all(name, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/get-all',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        local obj = json.decode(r)
        if type(obj) == 'table' then
            return obj
        end
    end
    return nil, r
end

-- 中控 KVDB 删除词典值
---@param name string 词典名称
---@param key string 键
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? value 成功返回值
---@return string? errMsg 失败返回错误信息
-- 用法
--   value, err = LCC.kvdb.dict.delete(name, key, timeout)
-- 例子
--   value, err = LCC.kvdb.dict.delete("表名", "键", 60)
function XXTLanControl.kvdb.dict.delete(name, key, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    key = _check_kvdb_key(_check_value(2, "string", nil, key))
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/delete-key',
        timeout = timeout,
        params = { name = name; key = key; },
    })
    if c == 200 then
        return r
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- 中控 KVDB 清空词典
---@param name string 词典名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? dict 成功返回值
---@return string? errMsg 失败返回错误信息
-- 用法
--   list, err = LCC.kvdb.dict.clear(name, timeout)
-- 例子
--   list, err = LCC.kvdb.dict.clear("表名", 60)
function XXTLanControl.kvdb.dict.clear(name, timeout)
    name = _check_kvdb_name(_check_value(1, "string", name))
    timeout = _opt_value(2, "number", 60, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/kvdb/dict/clear-all',
        timeout = timeout,
        params = { name = name; },
    })
    if c == 200 then
        local obj = json.decode(r)
        if type(obj) == 'table' then
            return obj
        end
    end
    if c == 404 then
        return nil, 'not found'
    end
    return nil, r
end

-- TableDB 封装
XXTLanControl.db = {}
-- 注意事项：
-- 1. 所有表都自动包含 'id' 列作为自增主键，不可手动赋值或修改
-- 2. 所有用户定义的列类型均为 TEXT
-- 3. 条件匹配仅支持等值 AND 条件
-- 4. fetch_one 是原子操作，适用于"领取任务"等并发场景
-- 5. 返回的行对象包含 id 和所有定义的列（值为字符串或 nil）

-- 名称校验（与 KVDB 风格保持一致，允许中文；内部由服务端做安全映射）
local function _check_tabledb_name(name)
    if #name == 0 then error("Table name cannot be empty", 3) end
    if #name > 256 then error("Table name too long (max 256 bytes)", 3) end
    return name
end

local function _check_tabledb_columns(cols)
    if type(cols) ~= 'table' or #cols == 0 then
        error("invalid columns", 3)
    end
    for i, c in ipairs(cols) do
        if type(c) ~= 'string' or #c == 0 then
            error("invalid column at index "..tostring(i), 3)
        end
        if c == 'id' then
            error("column name 'id' is reserved", 3)
        end
        if not utf8.len(c) then
            error("column name must be valid UTF-8", 3)
        end
    end
    return cols
end

local function _normalize_tabledb_id(id)
    if type(id) == 'string' then
        id = tonumber(id)
    end
    if type(id) ~= 'number'
        or id ~= id
        or id == math.huge
        or id == -math.huge
        or id < 1
        or id ~= math.floor(id)
    then
        return nil
    end
    return math.floor(id)
end

local function _check_tabledb_id(id, level, message)
    id = _normalize_tabledb_id(id)
    if id == nil then
        error(message or "invalid id", level or 3)
    end
    return id
end

-- 创建数据表
---@param name string 表名称（任意非空字符串）
---@param columns string[] 列名称数组（不允许包含 'id'）
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 例子
--   ok, err = LCC.db.create('任务表', { '名称', '状态', '备注' }, timeout)
function XXTLanControl.db.create(name, columns, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    columns = _check_tabledb_columns(_check_value(2, "table", nil, columns))
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/create',
        timeout = timeout,
        params = { name = name; },
        json = { columns = columns }
    })
    if c == 204 then return true end
    if c == 409 then return false, 'table already exists' end
    if c == 400 then
        -- 返回具体的错误信息
        return false, r
    end
    return false, r
end

-- 批量新增行
---@param name string 表名称
---@param rows table[] 行数据数组，每行为一个表对象
---@param timeout? number 超时秒，可选参数，默认 60
---@return boolean ok 成功返回 true
---@return string? errMsg 失败返回错误信息
-- 例子
--   ok, err = LCC.db.add('任务表', {
--       { 名称 = '任务A', 状态 = '空闲' },
--       { 名称 = '任务B', 状态 = '空闲', 备注 = '高优先级' }
--   }, timeout)
function XXTLanControl.db.add(name, rows, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    rows = _check_value(2, "table", nil, rows)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/add',
        timeout = timeout,
        params = { name = name; },
        json = { rows = rows }
    })
    if c == 204 then return true end
    if c == 404 then return false, 'unknown table' end
    if c == 400 then
        -- 可能的错误: empty payload, id is readonly, unknown column
        return false, r
    end
    return false, r
end

-- 批量删除行（按 id）
---@param name string 表名称
---@param id_rows table[] 包含 id 的对象数组
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? rows 成功返回被删除的行列表
---@return string? errMsg 失败返回错误信息
-- 例子
--   list, err = LCC.db.del('任务表', {
--       { id = 6 },
--       { id = 7 }
--   }, timeout)
function XXTLanControl.db.del(name, id_rows, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    id_rows = _check_value(2, "table", nil, id_rows)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    -- 允许传入 { {id=1}, {id=2} } 或 {1,2}
    local ids = {}
    for _, v in ipairs(id_rows) do
        if type(v) == 'table' then v = v.id end
        ids[#ids+1] = _check_tabledb_id(v)
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/del',
        timeout = timeout,
        params = { name = name; },
        json = { ids = ids }
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then return arr end
    end
    if c == 404 then return nil, 'unknown table' end
    if c == 400 then
        -- 保留后端返回的参数错误描述
        return nil, r
    end
    return nil, r
end

-- 批量编辑行（按 id）
---@param name string 表名称
---@param rows table[] 包含 id 和要更新字段的对象数组
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? rows 成功返回编辑前的行列表
---@return string? errMsg 失败返回错误信息
-- 例子
--   list, err = LCC.db.edit('任务表', {
--       { id = 6, 名称 = '新名称', 状态 = '进行中' },
--       { id = 7, 备注 = '已更新' }
--   }, timeout)
function XXTLanControl.db.edit(name, rows, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    rows = _check_value(2, "table", nil, rows)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    for i, r0 in ipairs(rows) do
        if type(r0) ~= 'table' then
            error("invalid id at row "..tostring(i), 2)
        end
        r0.id = _check_tabledb_id(r0.id, 3, "invalid id at row "..tostring(i))
    end
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/edit',
        timeout = timeout,
        params = { name = name; },
        json = { rows = rows }
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then return arr end
    end
    if c == 404 then return nil, 'unknown table' end
    if c == 400 then
        -- 可能的错误: invalid id, id is readonly, unknown column
        return nil, r
    end
    return nil, r
end

-- 列出行（支持条件筛选、排序、分页）
---@param name string 表名称
---@param opts? table 查询选项表
--   - conditions: 等值条件表（AND 组合）
--   - order_by: 排序字段，默认 'id'
--   - desc: 是否降序，默认 false
--   - limit: 限制返回行数
--   - offset: 偏移量，默认 0
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? rows 成功返回行列表
---@return string? errMsg 失败返回错误信息
-- 例子
--   list, err = LCC.db.list('任务表', {
--       conditions = { 状态 = '空闲' },
--       order_by = 'id',
--       desc = false,
--       limit = 10,
--       offset = 0
--   }, timeout)
function XXTLanControl.db.list(name, opts, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    opts = _opt_value(2, "table", {}, nil, opts)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local body = {
        conditions = opts and opts.conditions or nil,
        order_by = opts and opts.order_by or 'id',
        desc = opts and (opts.desc and true or false) or false,
        limit = opts and opts.limit or nil,
        offset = opts and opts.offset or 0,
    }
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/list',
        timeout = timeout,
        params = { name = name; },
        json = body
    })
    if c == 200 then
        local arr = json.decode(r)
        if type(arr) == 'table' then return arr end
    end
    if c == 404 then return nil, 'unknown table' end
    if c == 400 then
        -- 可能的错误: unknown column
        return nil, r
    end
    return nil, r
end

-- 统计符合条件的行数
---@param name string 表名称
---@param conditions? table 查询条件（可选，等值条件表，AND 组合）
---@param timeout? number 超时秒，可选参数，默认 60
---@return integer? count 成功返回数量
---@return string? errMsg 失败返回错误信息
-- 例子
--   count, err = LCC.db.count('任务表', { 状态 = '空闲' }, timeout)
-- 统计所有行
--   total, err = LCC.db.count('任务表', nil, timeout)
function XXTLanControl.db.count(name, conditions, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    conditions = _opt_value(2, "table", nil, nil, conditions)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local body = {
        conditions = conditions or {}
    }
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/count',
        timeout = timeout,
        params = { name = name; },
        json = body
    })
    if c == 200 then
        local obj = json.decode(r)
        if type(obj) == 'table' and obj.count then
            return obj.count
        end
    end
    if c == 404 then return nil, 'unknown table' end
    if c == 400 then
        -- 可能的错误: unknown column
        return nil, r
    end
    return nil, r
end

-- 原子获取并更新一条数据
---@param name string 表名称
---@param opts table 操作选项表
--   - conditions: 查询条件
--   - update: 要更新的字段
--   - pick: 选择策略 'min' | 'max' | 'random'（基于 id；random 随机一条），默认 'min'
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? row 成功返回更新前的行对象
---@return string? errMsg 失败返回错误信息
-- 例子
--   line, err = LCC.db.fetch_one('任务表', {
--       conditions = { 状态 = '空闲' },
--       update = { 状态 = '进行中' },
--       pick = 'min'
--   }, timeout)
function XXTLanControl.db.fetch_one(name, opts, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    opts = _check_value(2, "table", nil, opts)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local body = {
        conditions = opts.conditions or {},
        update = opts.update or {},
        pick = opts.pick or 'min',
    }
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/fetch-one',
        timeout = timeout,
        params = { name = name; },
        json = body
    })
    if c == 200 then
        local obj = json.decode(r)
        if type(obj) == 'table' then return obj end
    end
    if c == 404 then
        -- 区分 "unknown table" 和 "not found"
        if r == 'unknown table' then
            return nil, 'unknown table'
        else
            return nil, 'not found'
        end
    end
    if c == 400 then
        -- 可能的错误: unknown column
        return nil, r
    end
    return nil, r
end

-- 获取单行（按 id）
---@param name string 表名称
---@param id integer 行 id
---@param timeout? number 超时秒，可选参数，默认 60
---@return table? row 成功返回行对象
---@return string? errMsg 失败返回错误信息
-- 例子
--   line, err = LCC.db.get('任务表', 123, timeout)
function XXTLanControl.db.get(name, id, timeout)
    name = _check_tabledb_name(_check_value(1, "string", name))
    id = _check_tabledb_id(id)
    timeout = _opt_value(3, "number", 60, nil, nil, timeout)
    _check_XXTLanControl_api_url()
    local c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/tabledb/get',
        timeout = timeout,
        params = { name = name; },
        json = { id = id }
    })
    if c == 200 then
        local obj = json.decode(r)
        if type(obj) == 'table' then return obj end
    end
    if c == 404 then
        -- 区分 "unknown table" 和 "not found"
        if r == 'unknown table' then
            return nil, 'unknown table'
        else
            return nil, 'not found'
        end
    end
    if c == 400 then
        -- 可能的错误: invalid name or id
        return nil, r
    end
    return nil, r
end

return XXTLanControl
