local _XXT_LANCONTROL_VERSION = "v202605100945";
--[[
    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 = {}
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;
    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};
    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};
    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};
    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 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
        t['log'..i] = v
    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
                local func = load(tostring(r), XXT_SCRIPTS_PATH.."/XXTLanControl.lua")
                if type(func) == 'function' then
                    file.remove(XXT_LUA_PATH.."/XXTLanControl.lua")
                    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()
                http.post(api_url .. "/api/devices/run-state", 3, {
                    ['Content-Type'] = 'application/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

-- 中控脚本配置界面说明
-- 平铺脚本是指带 "lua/scripts/main.lua" 的脚本，例如 "我的脚本/lua/scripts/main.lua" 这样的，"我的脚本" 就是一个平铺脚本
-- 中控的文件管理中，可以对平铺脚本点击右键，选择 "编辑脚本 UI 配置" 来为平铺脚本创建配置界面
-- 脚本的 UI 界面和对界面的配置都存储在平铺脚本中的 "lua/scripts/main.json" 文件中
-- 使用对设备启动脚本时，"lua/scripts/main.json" 这个文件也会同时发送到设备上
-- 用以下方式读取配置界面的配置的参数
-- 例子
--   conf = LCC.get_ui_config()
function XXTLanControl.get_ui_config()
    local s = file.reads(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
                        t[ctrl.item[i]] = true
                    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

-- 中控日志，可以输出到 "日志 #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')
function XXTLanControl.log(id_or_logs, ...)
    id_or_logs = _check_value(1, "integer.table", id_or_logs)
    _check_XXTLanControl_api_url()
    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 outt = {}
        local argc = select("#", ...)
        if argc > 0 then
            for i = 1, argc do
                local arg = select(i, ...)
                if type(arg) == "table" then
                    outt[#outt + 1] = table.deep_dump(arg)
                else
                    local s = tostring(arg)
                    s = string.gsub(s, "%[DATE%]", os.date("[%Y-%m-%d %H:%M:%S]"))
                    s = string.gsub(s, "%[LINE%]", "["..tostring(debug.getinfo(2).currentline).."]")
                    outt[#outt + 1] = s
                end
            end
        end
        local ok = false
        local send_failed_toast = false
        repeat
            local c, h, r = _XXTLanControl_request("post", {
                url = _XXTLanControl_options["api-url"] .. '/api/devices/send-logs',
                timeout = 10,
                json = {
                    udid = device.udid();
                    logs = {
                        {
                            id = id_or_logs or 1;
                            message = table.concat(outt, '\t');
                        }
                    }
                }
            })
            if c >= 200 and c < 300 then
                ok = true
                if send_failed_toast then
                    sys.toast("", -1)
                end
            else
                sys.toast("XXTLanControl.log 发送失败，准备重试...")
                send_failed_toast = true
                sys.msleep(1000)
            end
        until ok
    else
        local aliases = _XXTLanControl_options["log-column-aliases"]
        local alias_map = {}
        if type(aliases) == 'table' then
            local count = 1
            for k, v in pairs(aliases) do
                alias_map[v] = tonumber(k:sub(-1, -1)) or (count%6)
                count = count + 1
            end
        end
        local logs = {}
        for key, value in pairs(id_or_logs) do
            if alias_map[key] then
                if type(value) == 'table' then
                    logs[#logs + 1] = {id = alias_map[key], message = table.deep_dump(value)}
                else
                    logs[#logs + 1] = {id = alias_map[key], message = tostring(value)}
                end
            end
        end
        local ok = false
        local send_failed_toast = false
        repeat
            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
                ok = true
                if send_failed_toast then
                    sys.toast("", -1)
                end
            else
                sys.toast("XXTLanControl.log 发送失败，准备重试...")
                send_failed_toast = true
                sys.msleep(1000)
            end
        until ok
    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

-- 中控文件下载
---@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 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
        return true
    end
    return false, r
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) == '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

-- 中控 KVDB 推入队列（前端）
---@param name string 队列名称
---@param value 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))
    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/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 值
---@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))
    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/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 队列名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? 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)
    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/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 队列名称
---@param timeout? number 超时秒，可选参数，默认 60
---@return string? 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)
    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/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, "not found"
    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
