local _XXT_LANCONTROL_VERSION = "v202606281559";
--[[
    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;
    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};
    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};
    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

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_append_payload_value(outt, parts, media, arg, call_line)
    if _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 c, h, r = _XXTLanControl_request("post", {
        url = _XXTLanControl_options["api-url"] .. '/api/devices/send-logs',
        timeout = 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_image(value)
    if type(value) ~= 'table' then
        return false
    end
    for _, item in ipairs(value) do
        if _log_is_inline_image(item) then
            return true
        end
    end
    return false
end

local function _log_payload_from_map_value(id, value, call_line)
    if _log_is_inline_image(value) then
        return _log_payload_from_values(id, {value}, 1, call_line)
    end
    if _log_sequence_has_image(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)
    _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
