<?php

use think\Db;

define('CLICD_DEBUG', false);

function clicd_debug($message, $data = null)
{
    if (!CLICD_DEBUG) {
        return;
    }
    $line = '[CLICD-DEBUG] ' . $message;
    if ($data !== null) {
        $line .= ' | ' . json_encode($data, JSON_UNESCAPED_UNICODE);
    }
    error_log($line);
}

function clicd_debug_entry($message, $data = null)
{
    return [
        'time'    => date('Y-m-d H:i:s'),
        'message' => $message,
        'data'    => $data,
    ];
}

function clicd_json_response($payload)
{
    if (!headers_sent()) {
        header('Content-Type: application/json; charset=utf-8');
    }
    echo json_encode($payload, JSON_UNESCAPED_UNICODE);
    exit;
}

function clicd_MetaData()
{
    return [
        'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5 and DeepSeek V4',
        'APIVersion'  => '1.1',
        'HelpDoc'     => 'https://github.com/MengMengCode/CLICD',
        'version'     => '1.0.11',
    ];
}

function clicd_ConfigOptions()
{
    return [
        ['type' => 'dropdown', 'name' => '虚拟化类型', 'description' => 'lxc 或 kvm', 'default' => 'lxc', 'key' => 'virtualization', 'options' => ['lxc' => 'LXC', 'kvm' => 'KVM']],
        ['type' => 'text', 'name' => '镜像/模板 ID', 'description' => 'CLICD 模板 ID，例如 alpine-3.21、debian-bookworm、ubuntu-jammy 或已启用的 KVM 镜像 ID', 'default' => 'alpine-3.21', 'key' => 'template_id'],
        ['type' => 'text', 'name' => 'CPU 核心', 'description' => 'vCPU 数量，KVM 必须为整数', 'default' => '1', 'key' => 'vcpu'],
        ['type' => 'text', 'name' => 'CPU 百分比', 'description' => 'CPU 使用率限制，0 表示不额外限制', 'default' => '0', 'key' => 'cpu_percent'],
        ['type' => 'text', 'name' => '内存 MB', 'description' => '容器内存，单位 MB', 'default' => '512', 'key' => 'ram_mb'],
        ['type' => 'text', 'name' => '硬盘 GB', 'description' => '系统盘大小，单位 GB', 'default' => '5', 'key' => 'disk_gb'],
        ['type' => 'text', 'name' => '带宽 Mbps', 'description' => '网络带宽限制，0 表示不限制', 'default' => '100', 'key' => 'network_bw_mbps'],
        ['type' => 'dropdown', 'name' => '流量模式', 'description' => 'total=总流量，in_out=分别限制入/出方向', 'default' => 'total', 'key' => 'traffic_mode', 'options' => ['total' => '总流量', 'in_out' => '入/出分开']],
        ['type' => 'text', 'name' => '月流量 GB', 'description' => 'total 模式下的月流量限制，0 表示不限制', 'default' => '100', 'key' => 'monthly_traffic_gb'],
        ['type' => 'text', 'name' => '入站流量 GB', 'description' => 'in_out 模式下入站流量限制，0 表示不限制', 'default' => '0', 'key' => 'traffic_in_gb'],
        ['type' => 'text', 'name' => '出站流量 GB', 'description' => 'in_out 模式下出站流量限制，0 表示不限制', 'default' => '0', 'key' => 'traffic_out_gb'],
        ['type' => 'text', 'name' => 'IO 速度 MB/s', 'description' => '磁盘 IO 限制，0 表示不限制', 'default' => '0', 'key' => 'io_speed_mbps'],
        ['type' => 'dropdown', 'name' => '分配 NAT', 'description' => '开通时是否分配 NAT 端口映射', 'default' => 'true', 'key' => 'assign_nat', 'options' => ['true' => '启用', 'false' => '禁用']],
        ['type' => 'text', 'name' => 'NAT 端口数量', 'description' => '开通时分配的端口映射数量，最小 2', 'default' => '2', 'key' => 'port_mapping_count'],
        ['type' => 'text', 'name' => '快照配额', 'description' => '每台实例允许保留的快照数量', 'default' => '3', 'key' => 'snapshot_limit'],
        ['type' => 'text', 'name' => '额外端口', 'description' => '逗号分隔的容器端口，例如 80,443', 'default' => '', 'key' => 'extra_ports'],
        ['type' => 'dropdown', 'name' => '自动公网 IPv4', 'description' => '开通时是否从 CLICD 公网 IPv4 池分配独立 IPv4', 'default' => 'false', 'key' => 'assign_ipv4', 'options' => ['true' => '启用', 'false' => '禁用']],
        ['type' => 'text', 'name' => '公网 IPv4 数量', 'description' => '自动分配公网 IPv4 的数量，通常填写 1', 'default' => '1', 'key' => 'ipv4_count'],
        ['type' => 'text', 'name' => '指定公网 IPv4', 'description' => '指定分配的公网 IPv4，多个用逗号分隔；留空则从地址池自动分配', 'default' => '', 'key' => 'public_ipv4s'],
        ['type' => 'dropdown', 'name' => '自动 IPv6', 'description' => '开通时自动分配 IPv6', 'default' => 'false', 'key' => 'assign_ipv6', 'options' => ['true' => '启用', 'false' => '禁用']],
        ['type' => 'text', 'name' => 'IPv6 数量', 'description' => '自动分配 IPv6 的数量，通常填写 1', 'default' => '1', 'key' => 'ipv6_count'],
        ['type' => 'text', 'name' => '指定 IPv6', 'description' => '指定分配的 IPv6 地址，多个用逗号分隔；留空则从地址池自动分配', 'default' => '', 'key' => 'ipv6_addresses'],
        ['type' => 'dropdown', 'name' => 'SSH 鉴权模式', 'description' => 'auto_password=自动生成密码，password=使用指定密码，key=使用 SSH 公钥', 'default' => 'auto_password', 'key' => 'ssh_auth_mode', 'options' => ['auto_password' => '自动密码', 'password' => '指定密码', 'key' => 'SSH 公钥']],
        ['type' => 'text', 'name' => '指定 SSH 密码', 'description' => 'SSH 鉴权模式为 password 时使用；其他模式留空', 'default' => '', 'key' => 'ssh_password'],
        ['type' => 'text', 'name' => 'SSH 公钥', 'description' => 'SSH 鉴权模式为 key 时使用；填写完整 public key', 'default' => '', 'key' => 'ssh_public_key'],
        ['type' => 'dropdown', 'name' => '同步到期时间', 'description' => '开通/续费时把魔方到期日期同步到 CLICD，格式会转换为 YYYY-MM-DD', 'default' => 'true', 'key' => 'sync_expiry', 'options' => ['true' => '启用', 'false' => '禁用']],
    ];
}

function clicd_base_url($params)
{
    if (!empty($params['server_host'])) {
        return rtrim($params['server_host'], '/');
    }

    $host = $params['server_ip'] ?? $params['ip'] ?? '';
    $port = $params['port'] ?? '';
    $scheme = (!empty($params['secure']) && (string)$params['secure'] !== '0') ? 'https' : 'http';

    if (stripos($host, 'http://') === 0 || stripos($host, 'https://') === 0) {
        $base = rtrim($host, '/');
    } else {
        $base = $scheme . '://' . $host;
    }

    if ($port !== '' && strpos(parse_url($base, PHP_URL_HOST) ?: $base, ':') === false) {
        $base .= ':' . $port;
    }

    return rtrim($base, '/');
}

function clicd_api_key($params)
{
    foreach (['accesshash', 'server_password', 'password'] as $key) {
        if (!empty($params[$key])) {
            return trim($params[$key]);
        }
    }
    return '';
}

function clicd_request($params, $endpoint, $data = [], $method = 'GET', $timeout = 30)
{
    $url = clicd_base_url($params) . $endpoint;
    $apiKey = clicd_api_key($params);
    $method = strtoupper($method);

    $curl = curl_init();
    $headers = [
        'Content-Type: application/json',
        'X-API-Key: ' . $apiKey,
        'Authorization: Bearer ' . $apiKey,
    ];

    $options = [
        CURLOPT_URL            => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_USERAGENT      => 'Mofang-CLICD',
    ];

    if ($method !== 'GET' && $data !== null) {
        $options[CURLOPT_POSTFIELDS] = json_encode($data, JSON_UNESCAPED_UNICODE);
    }

    curl_setopt_array($curl, $options);
    $body = curl_exec($curl);
    $errno = curl_errno($curl);
    $error = curl_error($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    clicd_debug('request', ['url' => $url, 'method' => $method, 'http_code' => $httpCode, 'errno' => $errno]);

    if ($errno) {
        return ['success' => false, 'message' => 'CURL ERROR: ' . $error, '_http_code' => 0];
    }

    $decoded = json_decode($body, true);
    if (!is_array($decoded)) {
        return ['success' => false, 'message' => 'Invalid JSON response: ' . substr((string)$body, 0, 300), '_http_code' => $httpCode];
    }

    $decoded['_http_code'] = $httpCode;
    return $decoded;
}

function clicd_request_debug($params, $endpoint, $data = [], $method = 'GET', $timeout = 30)
{
    $started = microtime(true);
    $res = clicd_request($params, $endpoint, $data, $method, $timeout);
    return [
        'response' => $res,
        'debug'    => clicd_debug_entry('CLICD API request', [
            'method'   => strtoupper($method),
            'endpoint' => $endpoint,
            'payload'  => $data,
            'http'     => is_array($res) ? ($res['_http_code'] ?? null) : null,
            'success'  => clicd_success($res),
            'message'  => clicd_message($res, ''),
            'ms'       => (int)round((microtime(true) - $started) * 1000),
        ]),
    ];
}

function clicd_success($res)
{
    if (!is_array($res)) {
        return false;
    }
    if (isset($res['success'])) {
        return (bool)$res['success'];
    }
    return isset($res['code']) && (int)$res['code'] >= 200 && (int)$res['code'] < 300;
}

function clicd_message($res, $fallback = '操作失败')
{
    if (!is_array($res)) {
        return $fallback;
    }
    return $res['message'] ?? $res['msg'] ?? $res['error'] ?? $fallback;
}

function clicd_container_name($params)
{
    $name = $params['domain'] ?? '';
    if (is_array($name)) {
        $name = reset($name);
    }
    $name = trim((string)$name);
    if ($name === '') {
        $name = 'host-' . ($params['hostid'] ?? time());
    }
    $name = preg_replace('/[^A-Za-z0-9_.-]/', '-', $name);
    return trim($name, '-.');
}

function clicd_host_id($params)
{
    foreach (['hostid', 'id', 'serviceid', 'service_id', 'relid'] as $key) {
        if (!empty($params[$key]) && is_numeric($params[$key])) {
            return (int)$params[$key];
        }
    }
    return 0;
}
function clicd_first_string($value)
{
    if (is_array($value)) {
        foreach ($value as $item) {
            if (is_array($item)) {
                foreach (['address', 'ip', 'ipv4', 'public_ip', 'public_ipv4'] as $key) {
                    if (!empty($item[$key])) {
                        $itemValue = trim((string)$item[$key]);
                        if ($itemValue !== '') {
                            return $itemValue;
                        }
                    }
                }
                continue;
            }

            $itemValue = trim((string)$item);
            if ($itemValue !== '') {
                return $itemValue;
            }
        }
        return '';
    }

    $value = trim((string)$value);
    return $value;
}

function clicd_public_host_from_container($container = [])
{
    if (is_array($container)) {
        foreach (['public_ipv4s', 'public_ipv4', 'public_ip', 'ipv4_addresses', 'ipv4', 'nat_public_ip', 'host_ip', 'external_ip', 'node_ip', 'nat_host'] as $key) {
            if (!empty($container[$key])) {
                $value = clicd_first_string($container[$key]);
                if ($value !== '') {
                    return $value;
                }
            }
        }
    }

    return '';
}

function clicd_public_ipv4_from_routing($params, $container = [])
{
    if (!is_array($container)) {
        return '';
    }

    $containerId = isset($container['id']) ? (string)$container['id'] : '';
    $containerName = isset($container['name']) ? (string)$container['name'] : clicd_container_name($params);

    $res = clicd_request($params, '/api/v1/routing', [], 'GET', 30);
    if (!clicd_success($res) || empty($res['data']['ipv4_assignments']) || !is_array($res['data']['ipv4_assignments'])) {
        return '';
    }

    foreach ($res['data']['ipv4_assignments'] as $assignment) {
        if (!is_array($assignment)) {
            continue;
        }
        $matchId = $containerId !== '' && isset($assignment['container_id']) && (string)$assignment['container_id'] === $containerId;
        $matchName = $containerName !== '' && isset($assignment['container_name']) && (string)$assignment['container_name'] === $containerName;
        if ($matchId || $matchName) {
            return clicd_first_string($assignment['address'] ?? '');
        }
    }

    return '';
}

function clicd_public_host($params, $container = [], $useRouting = false)
{
    $fromContainer = clicd_public_host_from_container($container);
    if ($fromContainer !== '') {
        return $fromContainer;
    }

    if ($useRouting) {
        $fromRouting = clicd_public_ipv4_from_routing($params, $container);
        if ($fromRouting !== '') {
            return $fromRouting;
        }
    }

    foreach (['server_ip', 'ip'] as $key) {
        if (!empty($params[$key])) {
            $value = trim((string)$params[$key]);
            if (stripos($value, 'http://') === 0 || stripos($value, 'https://') === 0) {
                return parse_url($value, PHP_URL_HOST) ?: $value;
            }
            return $value;
        }
    }

    return parse_url(clicd_base_url($params), PHP_URL_HOST) ?: '';
}

function clicd_container_ssh_port($container)
{
    if (!is_array($container)) {
        return '';
    }
    foreach (['ssh_port', 'host_ssh_port', 'nat_ssh_port'] as $key) {
        if (isset($container[$key]) && $container[$key] !== '') {
            return (int)$container[$key];
        }
    }
    return '';
}

function clicd_container_password($container)
{
    if (!is_array($container)) {
        return '';
    }
    foreach (['ssh_password', 'password', 'root_password', 'default_password'] as $key) {
        if (isset($container[$key]) && $container[$key] !== '') {
            $password = trim((string)$container[$key]);
            if ($password !== '' && !preg_match('/^\*+$/', $password)) {
                return $password;
            }
        }
    }
    return '';
}

function clicd_store_password($password)
{
    $password = (string)$password;
    if ($password === '') {
        return '';
    }
    return function_exists('cmf_encrypt') ? cmf_encrypt($password) : $password;
}

function clicd_webssh_url($params, $ticket, $containerName)
{
    $baseUrl = rtrim(clicd_base_url($params), '/');
    $scheme = stripos($baseUrl, 'https://') === 0 ? 'wss' : 'ws';
    $host = parse_url($baseUrl, PHP_URL_HOST);
    $port = parse_url($baseUrl, PHP_URL_PORT);
    $wsBase = $scheme . '://' . $host . ($port ? ':' . $port : '');
    $wsUrl = $wsBase
        . '/api/ssh?container=' . rawurlencode((string)$containerName)
        . '&container_name=' . rawurlencode((string)$containerName)
        . '&ticket=' . rawurlencode((string)$ticket);

    $siteScheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $siteHost = $_SERVER['HTTP_HOST'] ?? '';
    $handler = ($siteHost !== '' ? $siteScheme . '://' . $siteHost : '') . '/plugins/servers/clicd/handlers/webssh.php';

    return $handler
        . '?ws=' . rawurlencode($wsUrl)
        . '&protocol=' . rawurlencode('clicd-ticket.' . (string)$ticket)
        . '&ticket=' . rawurlencode((string)$ticket)
        . '&container=' . rawurlencode((string)$containerName);
}

function clicd_bool_option($value, $default = false)
{
    if ($value === null || $value === '') {
        return $default;
    }
    if (is_bool($value)) {
        return $value;
    }
    return in_array(strtolower((string)$value), ['1', 'true', 'yes', 'on'], true);
}

function clicd_int_option($options, $key, $default = 0)
{
    if (!isset($options[$key]) || $options[$key] === '') {
        return $default;
    }
    return (int)$options[$key];
}

function clicd_float_option($options, $key, $default = 0)
{
    if (!isset($options[$key]) || $options[$key] === '') {
        return $default;
    }
    return (float)$options[$key];
}

function clicd_number_value($value, $default = 0)
{
    if (is_numeric($value)) {
        return (float)$value;
    }
    if (is_string($value) && preg_match('/-?\d+(?:\.\d+)?/', $value, $match)) {
        return (float)$match[0];
    }
    return $default;
}

function clicd_pick_number($sources, $keys, $default = 0)
{
    foreach ($sources as $source) {
        if (!is_array($source)) {
            continue;
        }
        foreach ($keys as $key) {
            if (array_key_exists($key, $source) && $source[$key] !== '' && $source[$key] !== null) {
                return clicd_number_value($source[$key], $default);
            }
        }
    }
    return $default;
}

function clicd_pct($value)
{
    $value = clicd_number_value($value, 0);
    if ($value < 0) {
        return 0;
    }
    if ($value > 100) {
        return 100;
    }
    return round($value, 2);
}

function clicd_bytes_to_gb($bytes)
{
    return round(clicd_number_value($bytes, 0) / 1073741824, 2);
}

function clicd_bytes_to_mb($bytes)
{
    return round(clicd_number_value($bytes, 0) / 1048576, 2);
}

function clicd_format_bytes($bytes)
{
    $value = clicd_number_value($bytes, 0);
    if ($value >= 1073741824) {
        return round($value / 1073741824, 2) . ' GB';
    }
    if ($value >= 1048576) {
        return round($value / 1048576, 2) . ' MB';
    }
    if ($value >= 1024) {
        return round($value / 1024, 2) . ' KB';
    }
    return round($value, 2) . ' B';
}

function clicd_format_rate($bytesPerSecond)
{
    $value = clicd_number_value($bytesPerSecond, 0);
    if ($value >= 1073741824) {
        return round($value / 1073741824, 2) . ' GB/s';
    }
    if ($value >= 1048576) {
        return round($value / 1048576, 2) . ' MB/s';
    }
    if ($value >= 1024) {
        return round($value / 1024, 2) . ' KB/s';
    }
    return round($value, 2) . ' B/s';
}

function clicd_extra_ports($value)
{
    if (empty($value)) {
        return [];
    }
    $ports = [];
    foreach (preg_split('/[,;\s]+/', (string)$value) as $port) {
        $port = (int)trim($port);
        if ($port > 0 && $port <= 65535) {
            $ports[] = $port;
        }
    }
    return array_values(array_unique($ports));
}

function clicd_csv_values($value)
{
    if (is_array($value)) {
        $parts = $value;
    } else {
        $parts = preg_split('/[,;\s]+/', (string)$value);
    }

    $result = [];
    foreach ($parts as $part) {
        $part = trim((string)$part);
        if ($part !== '') {
            $result[] = $part;
        }
    }
    return array_values(array_unique($result));
}

function clicd_expiry_from_params($params)
{
    $options = $params['configoptions'] ?? [];
    if (!clicd_bool_option($options['sync_expiry'] ?? 'true', true)) {
        return '';
    }
    $raw = $params['nextduedate'] ?? '';
    if ($raw === '' || $raw === '0' || $raw === 0 || $raw === '0000-00-00' || $raw === '0000-00-00 00:00:00') {
        return '';
    }

    $timestamp = 0;
    if (is_numeric($raw)) {
        $timestamp = (int)$raw;
        if ($timestamp > 20000000000) {
            $timestamp = (int)floor($timestamp / 1000);
        }
    } else {
        $timestamp = strtotime((string)$raw);
    }

    if ($timestamp === false || $timestamp <= time()) {
        return '';
    }

    return date('Y-m-d', $timestamp);
}

function clicd_container_payload($params)
{
    $options = $params['configoptions'] ?? [];
    $trafficMode = $options['traffic_mode'] ?? 'total';
    $assignNat = clicd_bool_option($options['assign_nat'] ?? 'true', true);
    $assignIpv4 = clicd_bool_option($options['assign_ipv4'] ?? 'false', false);
    $assignIpv6 = clicd_bool_option($options['assign_ipv6'] ?? 'false', false);
    $publicIpv4s = clicd_csv_values($options['public_ipv4s'] ?? '');
    $ipv6Addresses = clicd_csv_values($options['ipv6_addresses'] ?? '');
    if (!empty($publicIpv4s)) {
        $assignIpv4 = true;
    }
    if (!empty($ipv6Addresses)) {
        $assignIpv6 = true;
    }
    $sshAuthMode = strtolower(trim((string)($options['ssh_auth_mode'] ?? 'auto_password')));
    if (!in_array($sshAuthMode, ['auto_password', 'password', 'key'], true)) {
        $sshAuthMode = 'auto_password';
    }

    return [
        'name'               => clicd_container_name($params),
        'virtualization'     => $options['virtualization'] ?? 'lxc',
        'template_id'        => $options['template_id'] ?? '',
        'vcpu'               => clicd_float_option($options, 'vcpu', 1),
        'cpu_percent'        => clicd_int_option($options, 'cpu_percent', 0),
        'ram_mb'             => clicd_int_option($options, 'ram_mb', 512),
        'disk_gb'            => clicd_int_option($options, 'disk_gb', 5),
        'network_bw_mbps'    => clicd_int_option($options, 'network_bw_mbps', 100),
        'monthly_traffic_gb' => clicd_int_option($options, 'monthly_traffic_gb', 100),
        'traffic_mode'       => in_array($trafficMode, ['total', 'in_out'], true) ? $trafficMode : 'total',
        'traffic_in_gb'      => clicd_int_option($options, 'traffic_in_gb', 0),
        'traffic_out_gb'     => clicd_int_option($options, 'traffic_out_gb', 0),
        'io_speed_mbps'      => clicd_int_option($options, 'io_speed_mbps', 0),
        'extra_ports'        => clicd_extra_ports($options['extra_ports'] ?? ''),
        'port_mapping_count' => $assignNat ? max(2, clicd_int_option($options, 'port_mapping_count', 2)) : 0,
        'assign_nat'         => $assignNat,
        'assign_ipv4'        => $assignIpv4,
        'ipv4_count'         => max(1, clicd_int_option($options, 'ipv4_count', 1)),
        'public_ipv4s'       => $publicIpv4s,
        'snapshot_limit'     => max(1, clicd_int_option($options, 'snapshot_limit', 3)),
        'assign_ipv6'        => $assignIpv6,
        'ipv6_count'         => max(1, clicd_int_option($options, 'ipv6_count', 1)),
        'ipv6_addresses'     => $ipv6Addresses,
        'ssh_auth_mode'      => $sshAuthMode,
        'ssh_password'       => (string)($options['ssh_password'] ?? ''),
        'ssh_public_key'     => trim((string)($options['ssh_public_key'] ?? '')),
        'expires_at'         => clicd_expiry_from_params($params),
    ];
}

function clicd_find_container($params)
{
    $name = clicd_container_name($params);
    return clicd_request($params, '/api/v1/containers/' . rawurlencode($name), [], 'GET');
}

function clicd_post_value($key, $default = '')
{
    if (function_exists('input')) {
        $value = input('post.' . $key);
        return $value === null ? $default : $value;
    }
    return $_POST[$key] ?? $default;
}

function clicd_request_value($key, $default = '')
{
    if (function_exists('input')) {
        $value = input('param.' . $key);
        if ($value === null) {
            $value = input('*.' . $key);
        }
        return $value === null ? $default : $value;
    }
    if (isset($_POST[$key])) {
        return $_POST[$key];
    }
    return $_GET[$key] ?? $default;
}

function clicd_json_input()
{
    $input = [];
    if (!empty($_POST) && is_array($_POST)) {
        $input = $_POST;
    }

    $raw = file_get_contents('php://input');
    $data = json_decode((string)$raw, true);
    if (is_array($data)) {
        return array_merge($input, $data);
    }

    $form = [];
    parse_str((string)$raw, $form);
    if (!empty($form) && is_array($form)) {
        return array_merge($input, $form);
    }

    return $input;
}

function clicd_param_value($data, $key, $default = '')
{
    if (is_array($data) && array_key_exists($key, $data)) {
        return $data[$key];
    }
    return clicd_request_value($key, $default);
}

function clicd_container_api_id($params, &$container = null)
{
    $res = clicd_find_container($params);
    if (clicd_success($res) && !empty($res['data']) && is_array($res['data'])) {
        $container = $res['data'];
        if (!empty($container['id'])) {
            return (string)$container['id'];
        }
        if (!empty($container['uuid'])) {
            return (string)$container['uuid'];
        }
        if (!empty($container['name'])) {
            return (string)$container['name'];
        }
    }

    $container = [];
    return clicd_container_name($params);
}

function clicd_port_mappings_from_container($container)
{
    if (!is_array($container)) {
        return [];
    }

    foreach (['port_mappings', 'portMappings', 'nat', 'nat_list', 'NatList'] as $key) {
        if (!empty($container[$key]) && is_array($container[$key])) {
            return $container[$key];
        }
    }

    return [];
}

function clicd_normalize_port_mappings($mappings)
{
    if (!is_array($mappings)) {
        return [];
    }

    $result = [];
    foreach ($mappings as $index => $mapping) {
        if (!is_array($mapping)) {
            continue;
        }
        $protocol = strtolower((string)($mapping['protocol'] ?? 'tcp'));
        $result[] = [
            'index'          => is_numeric($index) ? (int)$index : $index,
            'host_port'      => $mapping['host_port'] ?? '',
            'container_port' => $mapping['container_port'] ?? '',
            'protocol'       => in_array($protocol, ['tcp', 'udp'], true) ? $protocol : 'tcp',
            'tcp_selected'   => $protocol === 'udp' ? '' : 'selected',
            'udp_selected'   => $protocol === 'udp' ? 'selected' : '',
            'description'    => $mapping['description'] ?? '',
        ];
    }

    return $result;
}

function clicd_nat_post_action()
{
    $func = clicd_request_value('func', '');
    return strtolower(trim((string)$func));
}

function clicd_handle_nat_post($params)
{
    $action = clicd_nat_post_action();
    if ($action === '') {
        return ['message' => '', 'mappings' => null];
    }

    if (!in_array($action, ['randomport', 'addnat', 'updatenat', 'deletenat'], true)) {
        return ['message' => '', 'mappings' => null];
    }

    $map = [
        'randomport' => 'clicd_randomPort',
        'addnat'     => 'clicd_addNat',
        'updatenat'  => 'clicd_updateNat',
        'deletenat'  => 'clicd_deleteNat',
    ];

    if (!isset($map[$action]) || !function_exists($map[$action])) {
        return ['message' => '', 'mappings' => null];
    }

    $res = call_user_func($map[$action], $params);
    if (!is_array($res)) {
        return ['message' => (string)$res, 'mappings' => null];
    }

    $ok = (($res['status'] ?? '') === 'success' || (int)($res['status'] ?? 0) === 200);
    $prefix = $ok ? '成功: ' : '失败: ';
    return [
        'message'  => $prefix . ($res['msg'] ?? '操作完成'),
        'mappings' => ($ok && isset($res['data']['port_mappings']) && is_array($res['data']['port_mappings'])) ? $res['data']['port_mappings'] : null,
    ];
}

function clicd_nat_payload_from_post()
{
    return clicd_nat_payload_from_data(null);
}

function clicd_nat_payload_from_data($data = null)
{
    $hostPort = (int)clicd_param_value($data, 'host_port', 0);
    $containerPort = (int)clicd_param_value($data, 'container_port', 0);
    $protocol = strtolower(trim((string)clicd_param_value($data, 'protocol', 'tcp')));
    $description = trim((string)clicd_param_value($data, 'description', ''));

    if ($hostPort < 1 || $hostPort > 65535) {
        return ['error' => '公网端口必须在 1-65535 之间'];
    }
    if ($containerPort < 1 || $containerPort > 65535) {
        return ['error' => '容器端口必须在 1-65535 之间'];
    }
    if (!in_array($protocol, ['tcp', 'udp'], true)) {
        return ['error' => '协议只支持 tcp 或 udp'];
    }

    return [
        'container_port' => $containerPort,
        'host_port'      => $hostPort,
        'protocol'       => $protocol,
        'description'    => $description,
    ];
}

function clicd_nat_ajax($params)
{
    $input = clicd_json_input();
    $action = strtolower(trim((string)clicd_param_value($input, 'action', '')));
    $debug = [clicd_debug_entry('NAT ajax received', [
        'action' => $action,
        'input'  => $input,
        'query'  => $_GET,
    ])];

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $debug[] = clicd_debug_entry('Container resolved', [
        'container_id' => $containerId,
        'container'    => [
            'id'   => $container['id'] ?? null,
            'uuid' => $container['uuid'] ?? null,
            'name' => $container['name'] ?? null,
        ],
    ]);

    if (!in_array($action, ['random-port', 'add', 'update', 'delete'], true)) {
        return ['status' => 'error', 'msg' => '未知 NAT 操作', 'debug' => $debug];
    }

    if ($action === 'random-port') {
        $call = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($containerId) . '/random-port', [], 'GET', 30);
        $debug[] = $call['debug'];
        $res = $call['response'];
        return clicd_success($res)
            ? ['status' => 'success', 'msg' => '随机端口: ' . ($res['data']['port'] ?? ''), 'port' => $res['data']['port'] ?? '', 'debug' => $debug]
            : ['status' => 'error', 'msg' => clicd_message($res, '获取随机端口失败'), 'debug' => $debug];
    }

    if ($action === 'delete') {
        $index = clicd_param_value($input, 'index', '');
        if ($index === '' || !is_numeric($index) || (int)$index < 0) {
            return ['status' => 'error', 'msg' => '端口映射索引错误', 'debug' => $debug];
        }
        $endpoint = '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings/' . rawurlencode((string)(int)$index);
        $call = clicd_request_debug($params, $endpoint, [], 'DELETE', 30);
        $debug[] = $call['debug'];
        $res = $call['response'];
    } else {
        $payload = clicd_nat_payload_from_data($input);
        if (isset($payload['error'])) {
            return ['status' => 'error', 'msg' => $payload['error'], 'debug' => $debug];
        }

        if ($action === 'add') {
            $endpoint = '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings';
            $call = clicd_request_debug($params, $endpoint, $payload, 'POST', 30);
        } else {
            $index = clicd_param_value($input, 'index', '');
            if ($index === '' || !is_numeric($index) || (int)$index < 0) {
                return ['status' => 'error', 'msg' => '端口映射索引错误', 'debug' => $debug];
            }
            $endpoint = '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings/' . rawurlencode((string)(int)$index);
            $call = clicd_request_debug($params, $endpoint, $payload, 'PUT', 30);
        }
        $debug[] = $call['debug'];
        $res = $call['response'];
    }

    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, 'NAT 操作失败'), 'debug' => $debug];
    }

    return [
        'status'        => 'success',
        'msg'           => clicd_message($res, 'NAT 操作成功'),
        'port_mappings' => clicd_normalize_port_mappings($res['data'] ?? []),
        'debug'         => $debug,
    ];
}

function clicd_info_ajax($params)
{
    $debug = [clicd_debug_entry('Info ajax received', ['query' => $_GET])];
    $res = clicd_find_container($params);
    if (!clicd_success($res) || empty($res['data']) || !is_array($res['data'])) {
        return ['status' => 'error', 'msg' => clicd_message($res, '获取实例信息失败'), 'debug' => $debug];
    }

    $c = $res['data'];
    $name = $c['name'] ?? clicd_container_name($params);

    $usageCall = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($name) . '/usage', [], 'GET', 30);
    if (!clicd_success($usageCall['response']) && !empty($c['uuid'])) {
        $usageCall = clicd_request_debug($params, '/api/containers/' . rawurlencode((string)$c['uuid']) . '/usage', [], 'GET', 30);
    }
    $trafficCall = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($name) . '/traffic', [], 'GET', 30);
    $debug[] = $usageCall['debug'];
    $debug[] = $trafficCall['debug'];

    $usageRes = $usageCall['response'];
    $usage = clicd_success($usageRes) && isset($usageRes['data']) && is_array($usageRes['data']) ? $usageRes['data'] : [];
    $trafficRes = $trafficCall['response'];
    $traffic = clicd_success($trafficRes) && isset($trafficRes['data']) && is_array($trafficRes['data']) ? $trafficRes['data'] : [];
    $options = $params['configoptions'] ?? [];
    $sources = [$usage, $traffic, $c];

    $rxBytes = clicd_pick_number($sources, ['rx_used_bytes', 'rx_bytes', 'traffic_used_rx', 'in_bytes', 'input_bytes', 'network_rx_bytes'], 0);
    $txBytes = clicd_pick_number($sources, ['tx_used_bytes', 'tx_bytes', 'traffic_used_tx', 'out_bytes', 'output_bytes', 'network_tx_bytes'], 0);
    $totalBytes = clicd_pick_number($sources, ['total_used_bytes', 'traffic_used_bytes'], 0);
    if ($rxBytes <= 0 && $txBytes <= 0 && $totalBytes > 0) {
        $txBytes = $totalBytes;
    }
    if ($rxBytes <= 0) {
        $rxBytes = clicd_pick_number($sources, ['traffic_in_gb', 'in_gb', 'rx_gb'], 0) * 1073741824;
    }
    if ($txBytes <= 0) {
        $txBytes = clicd_pick_number($sources, ['traffic_out_gb', 'out_gb', 'tx_gb'], 0) * 1073741824;
    }

    $limitGB = clicd_pick_number([$traffic, $c, $options], ['monthly_traffic_gb', 'traffic_limit_gb', 'limit_gb'], 0);
    $trafficUsedGB = round(($rxBytes + $txBytes) / 1073741824, 2);
    $trafficPercent = $limitGB > 0 ? clicd_pct(($trafficUsedGB / $limitGB) * 100) : 0;

    $cpuPercent = clicd_pct(clicd_pick_number([$usage, $traffic], ['cpu_usage_pct', 'cpu_percent', 'cpu_usage_percent', 'cpu_usage', 'cpu'], 0));

    $memoryUsedMB = clicd_pick_number($sources, ['memory_used_mb', 'mem_used_mb', 'ram_used_mb', 'memory_usage_mb'], 0);
    if ($memoryUsedMB <= 0) {
        $memoryUsedMB = clicd_bytes_to_mb(clicd_pick_number($sources, ['memory_usage_bytes', 'memory_used', 'mem_used', 'ram_used', 'memory_bytes'], 0));
    }
    $memoryTotalMB = clicd_pick_number([$usage, $c, $options], ['memory_total_mb', 'mem_total_mb', 'ram_total_mb', 'ram_mb'], 0);
    if ($memoryTotalMB <= 0) {
        $memoryTotalMB = clicd_bytes_to_mb(clicd_pick_number($sources, ['memory_total', 'mem_total', 'ram_total'], 0));
    }
    $memoryPercent = $memoryTotalMB > 0 ? clicd_pct(($memoryUsedMB / $memoryTotalMB) * 100) : 0;

    $vcpu = clicd_pick_number([$c, $options], ['vcpu', 'cpu', 'cores'], 1);
    $loadPercent = clicd_pick_number([$usage, $traffic], ['load_percent', 'load_usage_percent'], -1);
    if ($loadPercent < 0) {
        $loadValue = clicd_pick_number([$usage, $traffic], ['load1', 'load', 'load_average'], 0);
        $loadPercent = $vcpu > 0 ? ($loadValue / $vcpu) * 100 : 0;
    }
    $loadPercent = clicd_pct($loadPercent);

    $diskUsedGB = clicd_pick_number($sources, ['disk_used_gb', 'disk_usage_gb', 'storage_used_gb'], 0);
    if ($diskUsedGB <= 0) {
        $diskUsedGB = clicd_bytes_to_gb(clicd_pick_number($sources, ['disk_usage_bytes', 'disk_used', 'disk_usage', 'storage_used'], 0));
    }
    $diskTotalGB = clicd_pick_number([$usage, $c, $options], ['disk_total_gb', 'storage_total_gb', 'disk_gb'], 0);
    if ($diskTotalGB <= 0) {
        $diskTotalGB = clicd_bytes_to_gb(clicd_pick_number($sources, ['disk_total', 'storage_total'], 0));
    }
    $diskPercent = $diskTotalGB > 0 ? clicd_pct(($diskUsedGB / $diskTotalGB) * 100) : 0;

    $netInBps = clicd_pick_number($sources, ['rx_bps', 'in_bps', 'network_rx_bps', 'net_in_bps'], 0);
    $netOutBps = clicd_pick_number($sources, ['tx_bps', 'out_bps', 'network_tx_bps', 'net_out_bps'], 0);
    $diskReadBps = clicd_pick_number($sources, ['disk_read_bps', 'read_bps', 'io_read_bps'], 0);
    $diskWriteBps = clicd_pick_number($sources, ['disk_write_bps', 'write_bps', 'io_write_bps'], 0);

    return [
        'status' => 'success',
        'data'   => [
            'cpu_percent'    => $cpuPercent,
            'cpu_detail'     => $cpuPercent . '%',
            'mem_percent'    => $memoryPercent,
            'mem_detail'     => ($memoryTotalMB > 0 ? round($memoryUsedMB, 0) . ' / ' . round($memoryTotalMB, 0) . ' MB' : '-'),
            'load_percent'   => $loadPercent,
            'load_detail'    => $loadPercent . '%',
            'disk_percent'   => $diskPercent,
            'disk_detail'    => ($diskTotalGB > 0 ? round($diskUsedGB, 2) . ' / ' . round($diskTotalGB, 2) . ' GB' : '-'),
            'traffic_used'   => $trafficUsedGB,
            'traffic_limit'  => $limitGB,
            'traffic_in_gb'  => round($rxBytes / 1073741824, 2),
            'traffic_out_gb' => round($txBytes / 1073741824, 2),
            'traffic_used_text' => clicd_format_bytes($rxBytes + $txBytes),
            'traffic_limit_text'=> $limitGB > 0 ? round($limitGB, 2) . ' GB' : '不限',
            'traffic_in_text'   => clicd_format_bytes($rxBytes),
            'traffic_out_text'  => clicd_format_bytes($txBytes),
            'traffic_percent'=> $trafficPercent,
            'net_in_bps'     => round($netInBps, 2),
            'net_out_bps'    => round($netOutBps, 2),
            'net_in_rate'    => clicd_format_rate($netInBps),
            'net_out_rate'   => clicd_format_rate($netOutBps),
            'disk_read_bps'  => round($diskReadBps, 2),
            'disk_write_bps' => round($diskWriteBps, 2),
            'disk_read_rate' => clicd_format_rate($diskReadBps),
            'disk_write_rate'=> clicd_format_rate($diskWriteBps),
            'chart_time'     => date('H:i:s'),
            'usage'          => $usage,
        ],
        'debug'  => $debug,
    ];
}

function clicd_domain_status_from_container($container)
{
    if (!is_array($container)) {
        return 'Active';
    }

    if (!empty($container['policy_blocked'])) {
        return 'Suspended';
    }

    $status = strtolower(trim((string)($container['status'] ?? '')));
    if (in_array($status, ['suspended', 'blocked', 'policy_blocked', 'disabled'], true)) {
        return 'Suspended';
    }

    return 'Active';
}

function clicd_update_host_from_container($params, $container)
{
    $hostId = clicd_host_id($params);
    if ($hostId <= 0 || !is_array($container)) {
        return;
    }

    $update = [
        'domainstatus' => clicd_domain_status_from_container($container),
        'username'     => 'root',
        'dedicatedip'  => clicd_public_host($params, $container, true),
    ];

    $sshPort = clicd_container_ssh_port($container);
    if ($sshPort !== '') {
        $update['port'] = $sshPort;
    }

    $password = clicd_container_password($container);
    if ($password !== '') {
        $update['password'] = clicd_store_password($password);
    }

    try {
        Db::name('host')->where('id', $hostId)->update($update);
    } catch (\Exception $e) {
        clicd_debug('host update failed', $e->getMessage());
    }
}

function clicd_TestLink($params)
{
    $res = clicd_request($params, '/api/v1/dashboard', [], 'GET');
    return [
        'status' => 200,
        'data'   => [
            'server_status' => clicd_success($res) ? 1 : 0,
            'msg'           => clicd_success($res) ? '连接成功' : clicd_message($res, '连接失败'),
        ],
    ];
}

function clicd_CreateAccount($params)
{
    $exists = clicd_find_container($params);
    if (clicd_success($exists)) {
        return ['status' => 'error', 'msg' => '容器已存在，不能重复开通'];
    }

    $payload = clicd_container_payload($params);
    if (empty($payload['template_id'])) {
        return ['status' => 'error', 'msg' => '产品配置缺少 template_id'];
    }

    $res = clicd_request($params, '/api/v1/containers', $payload, 'POST', 120);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '开通失败')];
    }

    $hostId = clicd_host_id($params);
    if ($hostId > 0) {
        try {
            Db::name('host')->where('id', $hostId)->update([
                'domainstatus' => 'Active',
                'username'     => 'root',
                'dedicatedip'  => clicd_public_ipv4_from_routing($params) ?: clicd_public_host($params),
            ]);
        } catch (\Exception $e) {
            return ['status' => 'error', 'msg' => '开通成功但同步魔方数据库失败: ' . $e->getMessage()];
        }
    }

    $detail = clicd_find_container($params);
    if (clicd_success($detail) && isset($detail['data'])) {
        clicd_update_host_from_container($params, $detail['data']);
    }

    return ['status' => 'success', 'msg' => clicd_message($res, '开通成功')];
}

function clicd_TerminateAccount($params)
{
    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/delete', [], 'DELETE', 60);
    return clicd_success($res)
        ? ['status' => 'success', 'msg' => clicd_message($res, '删除任务已提交')]
        : ['status' => 'error', 'msg' => clicd_message($res, '删除失败')];
}

function clicd_action($params, $action, $successMsg, $timeout = 60)
{
    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/' . $action, [], 'POST', $timeout);
    return clicd_success($res)
        ? ['status' => 'success', 'msg' => clicd_message($res, $successMsg)]
        : ['status' => 'error', 'msg' => clicd_message($res, $successMsg . '失败')];
}

function clicd_On($params)
{
    return clicd_action($params, 'start', '开机任务已提交');
}

function clicd_Off($params)
{
    return clicd_action($params, 'stop', '关机任务已提交');
}

function clicd_Reboot($params)
{
    return clicd_action($params, 'restart', '重启任务已提交');
}

function clicd_SuspendAccount($params)
{
    return clicd_Off($params);
}

function clicd_UnsuspendAccount($params)
{
    return clicd_On($params);
}

function clicd_Status($params)
{
    $res = clicd_find_container($params);
    if (!clicd_success($res) || empty($res['data'])) {
        return ['status' => 'error', 'msg' => clicd_message($res, '查询失败')];
    }

    $status = strtolower($res['data']['status'] ?? '');
    if ($status === 'running') {
        return ['status' => 'success', 'data' => ['status' => 'on', 'des' => '运行中']];
    }
    if ($status === 'stopped') {
        return ['status' => 'success', 'data' => ['status' => 'off', 'des' => '已关机']];
    }
    return ['status' => 'success', 'data' => ['status' => 'unknown', 'des' => $status ?: '未知']];
}

function clicd_Sync($params)
{
    $res = clicd_find_container($params);
    if (!clicd_success($res) || empty($res['data'])) {
        return ['status' => 'error', 'msg' => clicd_message($res, '同步失败')];
    }
    clicd_update_host_from_container($params, $res['data']);
    return ['status' => 'success', 'msg' => '同步成功'];
}

function clicd_Reinstall($params)
{
    $templateId = $params['reinstall_os'] ?? '';
    if ($templateId === '') {
        $templateId = ($params['configoptions']['template_id'] ?? '');
    }
    if ($templateId === '') {
        return ['status' => 'error', 'msg' => '缺少重装系统模板 ID'];
    }

    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/reinstall', ['template_id' => $templateId], 'POST', 60);
    if (clicd_success($res)) {
        $detail = clicd_find_container($params);
        if (clicd_success($detail) && isset($detail['data'])) {
            clicd_update_host_from_container($params, $detail['data']);
        } elseif (isset($res['data']) && is_array($res['data'])) {
            clicd_update_host_from_container($params, $res['data']);
        }
    }
    return clicd_success($res)
        ? ['status' => 'success', 'msg' => clicd_message($res, '重装任务已提交')]
        : ['status' => 'error', 'msg' => clicd_message($res, '重装失败')];
}

function clicd_CrackPassword($params, $new_pass)
{
    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/reset-password', ['password' => $new_pass], 'POST', 60);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '重置密码失败')];
    }

    $password = $res['data']['ssh_password'] ?? $res['data']['password'] ?? $new_pass;
    $hostId = clicd_host_id($params);
    if ($hostId > 0) {
        try {
            Db::name('host')->where('id', $hostId)->update(['password' => clicd_store_password($password)]);
            $detail = clicd_find_container($params);
            if (clicd_success($detail) && isset($detail['data'])) {
                clicd_update_host_from_container($params, $detail['data']);
            }
        } catch (\Exception $e) {
            return ['status' => 'error', 'msg' => '密码重置成功但同步魔方数据库失败: ' . $e->getMessage()];
        }
    }

    return ['status' => 'success', 'msg' => clicd_message($res, '密码重置成功')];
}

function clicd_TrafficReset($params)
{
    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/traffic-reset', [], 'POST', 30);
    return clicd_success($res)
        ? ['status' => 'success', 'msg' => clicd_message($res, '流量已重置')]
        : ['status' => 'error', 'msg' => clicd_message($res, '流量重置失败')];
}

function clicd_randomPort($params)
{
    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/random-port', [], 'GET', 30);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '获取随机端口失败')];
    }

    $port = $res['data']['port'] ?? '';
    return ['status' => 200, 'msg' => $port ? '随机端口: ' . $port : '随机端口获取成功', 'data' => ['port' => $port]];
}

function clicd_addNat($params)
{
    $payload = clicd_nat_payload_from_post();
    if (isset($payload['error'])) {
        return ['status' => 'error', 'msg' => $payload['error']];
    }

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings', $payload, 'POST', 30);
    return clicd_success($res)
        ? ['status' => 200, 'msg' => clicd_message($res, '端口映射添加成功'), 'data' => ['port_mappings' => clicd_normalize_port_mappings($res['data'] ?? [])]]
        : ['status' => 'error', 'msg' => clicd_message($res, '端口映射添加失败')];
}

function clicd_updateNat($params)
{
    $index = clicd_request_value('index', '');
    if ($index === '' || !is_numeric($index) || (int)$index < 0) {
        return ['status' => 'error', 'msg' => '端口映射索引错误'];
    }

    $payload = clicd_nat_payload_from_post();
    if (isset($payload['error'])) {
        return ['status' => 'error', 'msg' => $payload['error']];
    }

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $endpoint = '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings/' . rawurlencode((string)(int)$index);
    $res = clicd_request($params, $endpoint, $payload, 'PUT', 30);
    return clicd_success($res)
        ? ['status' => 200, 'msg' => clicd_message($res, '端口映射更新成功'), 'data' => ['port_mappings' => clicd_normalize_port_mappings($res['data'] ?? [])]]
        : ['status' => 'error', 'msg' => clicd_message($res, '端口映射更新失败')];
}

function clicd_deleteNat($params)
{
    $index = clicd_request_value('index', '');
    if ($index === '' || !is_numeric($index) || (int)$index < 0) {
        return ['status' => 'error', 'msg' => '端口映射索引错误'];
    }

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $endpoint = '/api/v1/containers/' . rawurlencode($containerId) . '/port-mappings/' . rawurlencode((string)(int)$index);
    $res = clicd_request($params, $endpoint, [], 'DELETE', 30);
    return clicd_success($res)
        ? ['status' => 200, 'msg' => clicd_message($res, '端口映射删除成功'), 'data' => ['port_mappings' => clicd_normalize_port_mappings($res['data'] ?? [])]]
        : ['status' => 'error', 'msg' => clicd_message($res, '端口映射删除失败')];
}

function clicd_natList($params)
{
    $res = clicd_find_container($params);
    if (!clicd_success($res) || empty($res['data']) || !is_array($res['data'])) {
        return ['status' => 'error', 'msg' => clicd_message($res, '获取 NAT 列表失败')];
    }

    return [
        'status' => 200,
        'msg'    => '获取成功',
        'data'   => [
            'port_mappings' => clicd_normalize_port_mappings(clicd_port_mappings_from_container($res['data'])),
            'debug' => [
                clicd_debug_entry('NatList', [
                    'container' => [
                        'id'   => $res['data']['id'] ?? null,
                        'uuid' => $res['data']['uuid'] ?? null,
                        'name' => $res['data']['name'] ?? null,
                    ],
                    'http' => $res['_http_code'] ?? null,
                ]),
            ],
        ],
    ];
}

function clicd_infoData($params)
{
    $data = clicd_info_ajax($params);
    if (($data['status'] ?? '') !== 'success') {
        return [
            'status' => 200,
            'msg'    => $data['msg'] ?? '流量统计暂不可用',
            'data'   => [
                'cpu_percent'    => 0,
                'cpu_detail'     => '-',
                'mem_percent'    => 0,
                'mem_detail'     => '-',
                'load_percent'   => 0,
                'load_detail'    => '-',
                'disk_percent'   => 0,
                'disk_detail'    => '-',
                'traffic_used'   => '-',
                'traffic_limit'  => '-',
                'traffic_in_gb'  => '-',
                'traffic_out_gb' => '-',
                'traffic_used_text' => '-',
                'traffic_limit_text'=> '-',
                'traffic_in_text'   => '-',
                'traffic_out_text'  => '-',
                'traffic_percent'=> 0,
                'net_in_bps'     => 0,
                'net_out_bps'    => 0,
                'net_in_rate'    => '0 B/s',
                'net_out_rate'   => '0 B/s',
                'disk_read_bps'  => 0,
                'disk_write_bps' => 0,
                'disk_read_rate' => '0 B/s',
                'disk_write_rate'=> '0 B/s',
                'chart_time'     => date('H:i:s'),
                'debug'          => $data['debug'] ?? [],
            ],
        ];
    }

    $data['data']['debug'] = $data['debug'] ?? [];
    return ['status' => 200, 'msg' => '获取成功', 'data' => $data['data']];
}

function clicd_ChangePackage($params)
{
    $options = $params['configoptions'] ?? [];
    $name = clicd_container_name($params);

    $resource = [
        'vcpu'             => clicd_float_option($options, 'vcpu', 0),
        'ram_mb'           => clicd_int_option($options, 'ram_mb', 0),
        'io_speed_mbps'    => clicd_int_option($options, 'io_speed_mbps', 0),
        'network_bw_mbps'  => clicd_int_option($options, 'network_bw_mbps', 0),
    ];
    $resource = array_filter($resource, function ($value) {
        return $value !== 0 && $value !== 0.0;
    });

    if (!empty($resource)) {
        $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/resource-limit', $resource, 'PUT', 60);
        if (!clicd_success($res)) {
            return ['status' => 'error', 'msg' => clicd_message($res, '资源限制调整失败')];
        }
    }

    $traffic = [
        'traffic_mode'       => $options['traffic_mode'] ?? 'total',
        'monthly_traffic_gb' => clicd_int_option($options, 'monthly_traffic_gb', 0),
        'traffic_in_gb'      => clicd_int_option($options, 'traffic_in_gb', 0),
        'traffic_out_gb'     => clicd_int_option($options, 'traffic_out_gb', 0),
    ];
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/traffic-limit', $traffic, 'PUT', 30);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '流量限制调整失败')];
    }

    $expiresAt = clicd_expiry_from_params($params);
    if ($expiresAt !== '') {
        $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/expiry', ['expires_at' => $expiresAt], 'PUT', 30);
        if (!clicd_success($res)) {
            return ['status' => 'error', 'msg' => clicd_message($res, '到期时间同步失败')];
        }
    }

    return ['status' => 'success', 'msg' => '配置变更成功'];
}

function clicd_Renew($params)
{
    $expiresAt = clicd_expiry_from_params($params);
    if ($expiresAt === '') {
        return ['status' => 'success', 'msg' => '未启用到期时间同步'];
    }
    $name = clicd_container_name($params);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($name) . '/expiry', ['expires_at' => $expiresAt], 'PUT', 30);
    return clicd_success($res)
        ? ['status' => 'success', 'msg' => '续费到期时间同步成功']
        : ['status' => 'error', 'msg' => clicd_message($res, '续费同步失败')];
}

function clicd_AdminButton($params)
{
    if (empty($params['domain'])) {
        return [];
    }
    return [
        'Sync'         => '同步状态',
        'TrafficReset' => '重置流量',
    ];
}

function clicd_ClientButton($params)
{
    if (empty($params['domain'])) {
        return [];
    }
    return [
        'webssh' => [
            'place' => 'console',
            'name'  => 'WebSSH',
        ],
    ];
}

function clicd_webssh($params)
{
    $container = [];
    $containerName = clicd_container_name($params);
    clicd_container_api_id($params, $container);
    if (!empty($container['name'])) {
        $containerName = (string)$container['name'];
    }

    $res = clicd_request($params, '/api/v1/ssh-ticket', ['container_name' => $containerName], 'POST', 30);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, 'WebSSH ticket create failed')];
    }

    $ticket = $res['data']['ticket'] ?? '';
    if ($ticket === '') {
        return ['status' => 'error', 'msg' => 'WebSSH ticket is empty'];
    }

    $url = clicd_webssh_url($params, $ticket, $containerName);
    $jsUrl = json_encode($url, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

    return [
        'status' => 'success',
        'msg'    => "WebSSH started<script type='text/javascript'>window.open({$jsUrl}, '_blank');</script>",
    ];
}

function clicd_firewallList($params)
{
    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30);
    if (!clicd_success($res) || empty($res['data'])) {
        return ['status' => 'error', 'msg' => clicd_message($res, '获取防火墙设置失败')];
    }

    return [
        'status' => 200,
        'msg'    => '获取成功',
        'data'   => $res['data'],
    ];
}

function clicd_firewallUpdate($params)
{
    $input = clicd_json_input();
    $enabled = clicd_param_value($input, 'enabled', 'true');
    $enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN);
    $defaultAction = strtoupper(trim((string)clicd_param_value($input, 'default_action', '')));
    $rules = clicd_param_value($input, 'rules', '[]');

    if (is_string($rules)) {
        $decodedRules = json_decode($rules, true);
        if (is_array($decodedRules)) {
            $rules = $decodedRules;
        }
    }
    if (!is_array($rules)) {
        $rules = [];
    }

    $payload = [
        'enabled' => $enabled,
        'rules'   => $rules,
    ];
    if (in_array($defaultAction, ['ACCEPT', 'DROP'], true)) {
        $payload['default_action'] = $defaultAction;
    }

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $res = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', $payload, 'PUT', 30);
    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '更新防火墙设置失败')];
    }

    // GET after PUT to confirm the actual state after CLICD processes it
    $getRes = clicd_request($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30);
    $actualData = [];
    if (clicd_success($getRes) && !empty($getRes['data']) && is_array($getRes['data'])) {
        $actualData = $getRes['data'];
    }

    return [
        'status' => 200,
        'msg'    => clicd_message($res, '防火墙设置已更新'),
        'data'   => $actualData,
    ];
}

function clicd_firewall_ajax($params)
{
    $input = clicd_json_input();
    $action = strtolower(trim((string)clicd_param_value($input, 'action', '')));
    $debug = [clicd_debug_entry('Firewall ajax received', [
        'action' => $action,
        'input'  => $input,
        'query'  => $_GET,
    ])];

    $container = [];
    $containerId = clicd_container_api_id($params, $container);
    $debug[] = clicd_debug_entry('Container resolved', [
        'container_id' => $containerId,
        'container'    => [
            'id'   => $container['id'] ?? null,
            'uuid' => $container['uuid'] ?? null,
            'name' => $container['name'] ?? null,
        ],
    ]);

    if (!in_array($action, ['list', 'update'], true)) {
        return ['status' => 'error', 'msg' => '未知防火墙操作', 'debug' => $debug];
    }

    if ($action === 'list') {
        $call = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', [], 'GET', 30);
        $debug[] = $call['debug'];
        $res = $call['response'];
        if (!clicd_success($res) || empty($res['data'])) {
            return ['status' => 'error', 'msg' => clicd_message($res, '获取防火墙设置失败'), 'debug' => $debug];
        }
        return [
            'status' => 'success',
            'msg'    => '获取成功',
            'data'   => $res['data'],
            'debug'  => $debug,
        ];
    }

    // update
    $enabled = clicd_param_value($input, 'enabled', 'true');
    $enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN);
    $defaultAction = strtoupper(trim((string)clicd_param_value($input, 'default_action', '')));
    $rules = clicd_param_value($input, 'rules', '[]');

    if (is_string($rules)) {
        $decodedRules = json_decode($rules, true);
        if (is_array($decodedRules)) {
            $rules = $decodedRules;
        }
    }
    if (!is_array($rules)) {
        $rules = [];
    }

    $payload = [
        'enabled' => $enabled,
        'rules'   => $rules,
    ];
    if (in_array($defaultAction, ['ACCEPT', 'DROP'], true)) {
        $payload['default_action'] = $defaultAction;
    }

    $call = clicd_request_debug($params, '/api/v1/containers/' . rawurlencode($containerId) . '/firewall', $payload, 'PUT', 30);
    $debug[] = $call['debug'];
    $res = $call['response'];

    if (!clicd_success($res)) {
        return ['status' => 'error', 'msg' => clicd_message($res, '更新防火墙设置失败'), 'debug' => $debug];
    }

    return [
        'status' => 'success',
        'msg'    => clicd_message($res, '防火墙设置已更新'),
        'data'   => $res['data'] ?? [],
        'debug'  => $debug,
    ];
}

function clicd_AllowFunction()
{
    return [
        'client' => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh', 'firewallList', 'firewallUpdate'],
        'admin'  => ['TrafficReset', 'randomPort', 'addNat', 'updateNat', 'deleteNat', 'natList', 'infoData', 'webssh', 'firewallList', 'firewallUpdate'],
    ];
}

function clicd_ClientArea($params)
{
    return [
        'info'     => ['name' => '实例信息'],
        'nat'      => ['name' => 'NAT转发'],
        'firewall' => ['name' => '防火墙'],
    ];
}

function clicd_ClientAreaOutput($params, $key)
{
    $func = strtolower(trim((string)clicd_request_value('func', '')));
    if ($func === 'natajax') {
        clicd_json_response(clicd_nat_ajax($params));
    }
    if ($func === 'infoajax') {
        clicd_json_response(clicd_info_ajax($params));
    }
    if ($func === 'firewallajax') {
        clicd_json_response(clicd_firewall_ajax($params));
    }

    if (!in_array($key, ['info', 'nat', 'firewall'], true)) {
        return '';
    }

    $res = clicd_find_container($params);
    if (!clicd_success($res) || empty($res['data'])) {
        return '获取实例信息失败: ' . clicd_message($res, '未知错误');
    }

    $c = $res['data'];
    $publicHost = clicd_public_host($params, $c, true);

    if ($key === 'nat') {
        $operation = clicd_handle_nat_post($params);
        $operationMsg = $operation['message'] ?? '';
        $postMappings = $operation['mappings'] ?? null;
        if ($operationMsg !== '') {
            $res = clicd_find_container($params);
            $c = clicd_success($res) && !empty($res['data']) && is_array($res['data']) ? $res['data'] : $c;
        }
        $mappings = $postMappings !== null ? $postMappings : clicd_port_mappings_from_container($c);

        return [
            'template' => 'templates/nat.html',
            'vars'     => [
                'container'     => $c,
                'container_name'=> $c['name'] ?? clicd_container_name($params),
                'ssh_port'      => $c['ssh_port'] ?? '',
                'server_ip'     => $publicHost,
                'nat_host'      => $publicHost,
                'operation_msg' => $operationMsg,
                'service_id'    => clicd_request_value('id', $params['hostid'] ?? ''),
                'area_key'      => 'nat',
                'port_mappings' => clicd_normalize_port_mappings($mappings),
            ],
        ];
    }

    if ($key === 'firewall') {
        return [
            'template' => 'templates/firewall.html',
            'vars'     => [
                'container'      => $c,
                'container_name' => $c['name'] ?? clicd_container_name($params),
                'server_ip'      => $publicHost,
                'service_id'     => clicd_request_value('id', $params['hostid'] ?? ''),
                'area_key'       => 'firewall',
            ],
        ];
    }

    $initialRxBytes = (int)($c['traffic_used_rx'] ?? $c['rx_bytes'] ?? 0);
    $initialTxBytes = (int)($c['traffic_used_tx'] ?? $c['tx_bytes'] ?? 0);
    $initialTrafficUsed = ($initialRxBytes || $initialTxBytes) ? round(($initialRxBytes + $initialTxBytes) / 1073741824, 2) : '-';
    $initialTrafficIn = $initialRxBytes ? round($initialRxBytes / 1073741824, 2) : '-';
    $initialTrafficOut = $initialTxBytes ? round($initialTxBytes / 1073741824, 2) : '-';
    $initialTrafficLimit = isset($c['monthly_traffic_gb']) && $c['monthly_traffic_gb'] !== '' ? $c['monthly_traffic_gb'] : '-';
    $initialTrafficUsedText = ($initialRxBytes || $initialTxBytes) ? clicd_format_bytes($initialRxBytes + $initialTxBytes) : '-';
    $initialTrafficInText = $initialRxBytes ? clicd_format_bytes($initialRxBytes) : '-';
    $initialTrafficOutText = $initialTxBytes ? clicd_format_bytes($initialTxBytes) : '-';
    $initialTrafficLimitText = is_numeric($initialTrafficLimit) ? round((float)$initialTrafficLimit, 2) . ' GB' : '-';
    $options = $params['configoptions'] ?? [];

    return [
        'template' => 'templates/info.html',
        'vars'     => [
            'container'      => $c,
            'status_text'    => (($c['status'] ?? '') === 'running') ? '运行中' : '已关机',
            'server_ip'      => $publicHost,
            'ssh_host'       => $publicHost,
            'ssh_port'       => $c['ssh_port'] ?? '',
            'ssh_password'   => $c['ssh_password'] ?? '',
            'ipv4'           => $c['ip'] ?? '',
            'ipv6'           => $c['ipv6'] ?? '',
            'vcpu'           => $c['vcpu'] ?? ($options['vcpu'] ?? ''),
            'ram_mb'         => $c['ram_mb'] ?? ($options['ram_mb'] ?? ''),
            'disk_gb'        => $c['disk_gb'] ?? ($options['disk_gb'] ?? ''),
            'bandwidth'      => $c['network_bw_mbps'] ?? ($options['network_bw_mbps'] ?? ''),
            'traffic_used'   => $initialTrafficUsed,
            'traffic_limit'  => $initialTrafficLimit,
            'traffic_in_gb'  => $initialTrafficIn,
            'traffic_out_gb' => $initialTrafficOut,
            'traffic_used_text' => $initialTrafficUsedText,
            'traffic_limit_text'=> $initialTrafficLimitText,
            'traffic_in_text'   => $initialTrafficInText,
            'traffic_out_text'  => $initialTrafficOutText,
            'expires_at'     => $c['expires_at'] ?? '',
            'service_id'     => clicd_request_value('id', $params['hostid'] ?? ''),
            'area_key'       => 'info',
        ],
    ];
}
