<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/request.php';
ini_set('error_log', 'error_log');


/**
 * 3x-ui (MHSanaei) API-token mode helpers.
 *
 * Modern 3x-ui (>= v3.1.0, the "multi inbound clients" rework) moved client
 * CRUD from /panel/api/inbounds/* to /panel/api/clients/* and enforces a CSRF
 * token on every cookie-authenticated POST. Cookie login therefore can no
 * longer create/update/delete clients on those panels. The supported path is
 * an API token (Settings -> generate token) sent as a Bearer header, which
 * bypasses CSRF and reaches the new client endpoints.
 *
 * When a panel row has a non-empty `xui_api_token`, every x-ui_single call uses
 * this token mode and the new endpoints. When it is empty the legacy cookie
 * behaviour is used unchanged, so existing older panels keep working.
 */
if (!function_exists('xui_panel_mode')) {
    /**
     * Return the explicit 3x-ui connection generation.
     *
     * v2 stores this in marzban_panel.xui_mode so legacy credentials never get
     * mixed with the modern API-token workflow. For rows created before mode separation we safely infer
     * the mode from whether an API token already exists.
     */
    function xui_panel_mode($panel)
    {
        if (!is_array($panel)) return 'legacy';
        $mode = strtolower(trim((string)($panel['xui_mode'] ?? '')));
        if (in_array($mode, array('modern', 'legacy'), true)) return $mode;
        return trim((string)($panel['xui_api_token'] ?? '')) !== '' ? 'modern' : 'legacy';
    }
}

if (!function_exists('xui_panel_uses_token')) {
    function xui_panel_uses_token($panel)
    {
        return is_array($panel)
            && xui_panel_mode($panel) === 'modern'
            && isset($panel['xui_api_token'])
            && trim((string) $panel['xui_api_token']) !== '';
    }
}

if (!function_exists('xui_panel_is_legacy')) {
    function xui_panel_is_legacy($panel)
    {
        return xui_panel_mode($panel) === 'legacy';
    }
}

if (!function_exists('xui_panel_token')) {
    function xui_panel_token($panel)
    {
        return trim((string) ($panel['xui_api_token'] ?? ''));
    }
}

if (!function_exists('xui_api_token_request')) {
    /**
     * Perform an authenticated request against a modern 3x-ui panel using its
     * API token. Returns the raw CurlRequest result (status/body/error) and, on
     * a {"success":false} body, sets ['error'] so callers can treat it like the
     * legacy error shape.
     */
    function xui_api_token_request($panel, $method, $path, $jsonBody = null, $timeout = 8)
    {
        $base = rtrim((string) ($panel['url_panel'] ?? ''), '/');
        $req = new CurlRequest($base . $path);
        $req->setTimeout($timeout);
        $req->setBearerToken(xui_panel_token($panel));
        $headers = array('Accept: application/json', 'X-Requested-With: XMLHttpRequest');
        if ($jsonBody !== null) {
            $headers[] = 'Content-Type: application/json';
        }
        $req->setHeaders($headers);

        $method = strtoupper($method);
        $payload = $jsonBody === null
            ? ''
            : (is_string($jsonBody) ? $jsonBody : json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        if ($method === 'GET') {
            $resp = $req->get();
        } elseif ($method === 'POST') {
            $resp = $req->post($payload);
        } elseif ($method === 'PUT') {
            $resp = $req->put($payload);
        } elseif ($method === 'PATCH') {
            $resp = $req->PATCH($payload);
        } elseif ($method === 'DELETE') {
            $resp = $req->delete($payload !== '' ? $payload : null);
        } else {
            return array('status'=>0,'body'=>'','error'=>'Unsupported 3x-ui API method: '.$method);
        }

        if (isset($resp['body']) && is_string($resp['body'])) {
            $decoded = json_decode($resp['body'], true);
            if (is_array($decoded) && array_key_exists('success', $decoded) && $decoded['success'] === false) {
                $resp['error'] = $decoded['msg'] ?? 'Unknown panel error';
            }
        }
        $status = (int) ($resp['status'] ?? 0);
        if (($status < 200 || $status >= 300) && empty($resp['error'])) {
            $resp['error'] = '3x-ui API HTTP ' . $status;
        }

        return $resp;
    }
}

if (!function_exists('xui_test_api_token_url')) {
    /**
     * Validate a 3x-ui API token without requiring a panel row to exist yet.
     * Used by both the Telegram admin flow and the web admin panel.
     */
    function xui_test_api_token_url($url, $token, $timeout = 7)
    {
        $url = rtrim(trim((string) $url), '/');
        $token = trim((string) $token);
        if ($url === '' || $token === '') {
            return array(
                'ok' => false,
                'verified' => false,
                'status' => 0,
                'message' => 'آدرس پنل یا توکن API خالی است.',
            );
        }

        $panel = array('url_panel' => $url, 'xui_api_token' => $token);
        $response = xui_api_token_request($panel, 'GET', '/panel/api/clients/list', null, $timeout);
        $status = (int) ($response['status'] ?? 0);
        $decoded = xui_token_decode($response);
        $verified = $status >= 200 && $status < 300
            && is_array($decoded)
            && (($decoded['success'] ?? false) === true);

        if ($verified) {
            return array(
                'ok' => true,
                'verified' => true,
                'status' => $status,
                'message' => '✅ اتصال API Token به 3x-ui با موفقیت برقرار شد.',
            );
        }

        $message = '';
        if (is_array($decoded)) {
            $message = trim((string) ($decoded['msg'] ?? $decoded['detail'] ?? ''));
        }
        if ($message === '') {
            $message = trim((string) ($response['error'] ?? ''));
        }
        if ($message === '') {
            $message = $status > 0 ? ('HTTP ' . $status) : 'پاسخی از پنل درپیدا نشد.';
        }

        return array(
            'ok' => false,
            'verified' => false,
            'status' => $status,
            'message' => '❌ توکن API تأیید نشد: ' . $message,
        );
    }
}


if (!function_exists('xui_parse_inbound_ids')) {
    /** Convert legacy single-id / comma / star / JSON values into a clean int list. */
    function xui_parse_inbound_ids($value)
    {
        if (is_array($value)) {
            $raw = $value;
        } else {
            $text = trim((string) $value);
            if ($text === '') return array();
            $decoded = json_decode($text, true);
            if (is_array($decoded)) {
                $raw = $decoded;
            } else {
                $raw = preg_split('/[,*|;\s]+/', $text) ?: array();
            }
        }
        $ids = array();
        foreach ($raw as $v) {
            if (is_array($v)) {
                foreach (xui_parse_inbound_ids($v) as $nested) $ids[] = $nested;
                continue;
            }
            if (is_numeric($v) && (int)$v > 0) $ids[] = (int)$v;
        }
        return array_values(array_unique($ids));
    }
}

if (!function_exists('xui_token_fetch_inbounds')) {
    /** Fetch and normalize the real inbound list from modern 3x-ui. */
    function xui_token_fetch_inbounds($panel, $timeout = 7)
    {
        $resp = xui_api_token_request($panel, 'GET', '/panel/api/inbounds/list', null, $timeout);
        $decoded = xui_token_decode($resp);
        $ok = (int)($resp['status'] ?? 0) >= 200 && (int)($resp['status'] ?? 0) < 300
            && is_array($decoded) && (($decoded['success'] ?? false) === true);
        if (!$ok) {
            $msg = is_array($decoded) ? trim((string)($decoded['msg'] ?? $decoded['detail'] ?? '')) : '';
            if ($msg === '') $msg = trim((string)($resp['error'] ?? 'دریافت لیست اینباند ناموفق بود.'));
            return array('ok' => false, 'items' => array(), 'message' => $msg, 'response' => $resp);
        }
        $rows = isset($decoded['obj']) && is_array($decoded['obj']) ? $decoded['obj'] : array();
        $items = array();
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $id = (int)($row['id'] ?? 0);
            if ($id <= 0) continue;
            $items[] = array(
                'id' => $id,
                'remark' => trim((string)($row['remark'] ?? ('Inbound #' . $id))),
                'protocol' => trim((string)($row['protocol'] ?? '')),
                'port' => (int)($row['port'] ?? 0),
                'enable' => !array_key_exists('enable', $row) || (bool)$row['enable'],
                'trafficReset' => trim((string)($row['trafficReset'] ?? '')),
                'trafficResetDay' => (int)($row['trafficResetDay'] ?? 0),
            );
        }
        return array('ok' => true, 'items' => $items, 'message' => '', 'response' => $resp);
    }
}

if (!function_exists('xui_token_fetch_inbounds_url')) {
    function xui_token_fetch_inbounds_url($url, $token, $timeout = 7)
    {
        return xui_token_fetch_inbounds(array(
            'url_panel' => rtrim(trim((string)$url), '/'),
            'xui_api_token' => trim((string)$token),
        ), $timeout);
    }
}

if (!function_exists('xui_token_format_inbounds')) {
    function xui_token_format_inbounds($items, $limit = 12)
    {
        if (!is_array($items) || !$items) return 'هیچ Inbound فعالی پیدا نشد.';
        $lines = array();
        $n = 0;
        foreach ($items as $item) {
            if (++$n > $limit) break;
            $id = (int)($item['id'] ?? 0);
            $remark = trim((string)($item['remark'] ?? '')) ?: ('Inbound #' . $id);
            $protocol = strtoupper(trim((string)($item['protocol'] ?? '')));
            $port = (int)($item['port'] ?? 0);
            $state = !empty($item['enable']) ? 'فعال' : 'خاموش';
            $meta = trim($protocol . ($port > 0 ? ' : ' . $port : ''));
            $lines[] = "#{$id} — {$remark}" . ($meta !== '' ? " ({$meta})" : '') . " — {$state}";
        }
        return implode("\n", $lines);
    }
}

if (!function_exists('xui_inbound_snapshot_payload')) {
    function xui_inbound_snapshot_payload($items)
    {
        $safe = array();
        foreach ((array)$items as $item) {
            if (!is_array($item)) continue;
            $id = (int)($item['id'] ?? 0);
            if ($id <= 0) continue;
            $safe[] = array(
                'id' => $id,
                'remark' => trim((string)($item['remark'] ?? '')),
                'protocol' => trim((string)($item['protocol'] ?? '')),
                'port' => (int)($item['port'] ?? 0),
                'enable' => !empty($item['enable']),
                'trafficReset' => trim((string)($item['trafficReset'] ?? '')),
                'trafficResetDay' => (int)($item['trafficResetDay'] ?? 0),
            );
        }
        return json_encode($safe, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
}

if (!function_exists('xui_store_inbound_snapshot')) {
    /** Persist the last known modern inbound list for diagnostics/web UI. */
    function xui_store_inbound_snapshot($panel, $items)
    {
        if (!is_array($panel) || empty($panel['name_panel']) || !function_exists('update')) return false;
        try {
            update('marzban_panel', 'xui_inbound_snapshot', xui_inbound_snapshot_payload($items), 'name_panel', $panel['name_panel']);
            update('marzban_panel', 'xui_last_sync', (string)time(), 'name_panel', $panel['name_panel']);
            return true;
        } catch (Throwable $e) {
            @error_log('[xui snapshot] ' . $e->getMessage());
            return false;
        }
    }
}

if (!function_exists('xui_refresh_inbounds')) {
    function xui_refresh_inbounds($panel, $timeout = 7)
    {
        if (!xui_panel_uses_token($panel)) {
            return array('ok' => false, 'items' => array(), 'message' => 'این عملیات فقط برای ثنایی جدید / Token Mode است.');
        }
        $result = xui_token_fetch_inbounds($panel, $timeout);
        if (!empty($result['ok'])) xui_store_inbound_snapshot($panel, $result['items'] ?? array());
        return $result;
    }
}

if (!function_exists('xui_token_resolve_inbound_ids')) {
    /**
     * Validate selected inbound IDs before create. If the panel has exactly one
     * enabled inbound, it is safe to auto-select it; otherwise force an explicit
     * choice and return the real available IDs instead of leaking GORM's
     * unhelpful "record not found" error.
     */
    function xui_token_resolve_inbound_ids($panel, $requested)
    {
        $list = xui_token_fetch_inbounds($panel);
        if (empty($list['ok'])) {
            return array('ok' => false, 'ids' => array(), 'message' => 'لیست Inboundهای 3x-ui درپیدا نشد: ' . ($list['message'] ?? 'خطای نامشخص'), 'items' => array());
        }
        $items = $list['items'] ?? array();
        $enabled = array_values(array_filter($items, static function ($x) { return !empty($x['enable']); }));
        $pool = $enabled ?: $items;
        $available = array();
        foreach ($pool as $item) $available[(int)$item['id']] = true;
        $wanted = xui_parse_inbound_ids($requested);
        $valid = array_values(array_filter($wanted, static function ($id) use ($available) { return isset($available[(int)$id]); }));
        if ($valid) {
            return array('ok' => true, 'ids' => $valid, 'message' => '', 'items' => $items, 'auto' => false);
        }
        if (count($pool) === 1) {
            return array('ok' => true, 'ids' => array((int)$pool[0]['id']), 'message' => 'Inbound به‌صورت خودکار انتخاب شد.', 'items' => $items, 'auto' => true);
        }
        $why = $wanted
            ? 'شناسه Inbound ذخیره‌شده روی این پنل معتبر نیست.'
            : 'برای این پنل هنوز Inbound انتخاب نشده است.';
        return array(
            'ok' => false,
            'ids' => array(),
            'items' => $items,
            'message' => $why . "\n\nInboundهای قابل انتخاب:\n" . xui_token_format_inbounds($pool) . "\n\nاز مدیریت پنل، Inbound درست را انتخاب و ذخیره کن.",
        );
    }
}

if (!function_exists('xui_token_decode')) {
    /** Decode a 3x-ui JSON response without changing CurlRequest's legacy shape. */
    function xui_token_decode($response)
    {
        if (!is_array($response) || !isset($response['body']) || !is_string($response['body'])) {
            return null;
        }
        $decoded = json_decode($response['body'], true);
        return is_array($decoded) ? $decoded : null;
    }
}


if (!function_exists('xui_token_clients_by_tgid')) {
    /** 3x-ui v3.6.0: lookup all clients associated with a Telegram ID. */
    function xui_token_clients_by_tgid($panel, $tgId, $timeout = 7)
    {
        $tgId = trim((string)$tgId);
        if ($tgId === '' || !preg_match('/^-?\d+$/', $tgId)) return array('ok'=>false,'items'=>array(),'message'=>'Telegram ID باید عددی باشد.');
        $resp = xui_api_token_request($panel, 'GET', '/panel/api/clients/get/tg/' . rawurlencode($tgId), null, $timeout);
        $decoded = xui_token_decode($resp);
        $ok = (int)($resp['status'] ?? 0) >= 200 && (int)($resp['status'] ?? 0) < 300 && is_array($decoded) && (($decoded['success'] ?? false) === true);
        if (!$ok) {
            $msg = is_array($decoded) ? trim((string)($decoded['msg'] ?? $decoded['detail'] ?? '')) : '';
            if ($msg === '') $msg = trim((string)($resp['error'] ?? 'Telegram ID lookup failed.'));
            return array('ok'=>false,'items'=>array(),'message'=>$msg,'response'=>$resp);
        }
        $obj = $decoded['obj'] ?? array();
        if (isset($obj['clients']) && is_array($obj['clients'])) $obj = $obj['clients'];
        if (isset($obj['client']) && is_array($obj['client']) && !array_is_list($obj)) $obj = array($obj['client']);
        if (!is_array($obj)) $obj = array();
        if (!array_is_list($obj) && isset($obj['email'])) $obj = array($obj);
        return array('ok'=>true,'items'=>array_values(array_filter($obj,'is_array')),'raw'=>$decoded,'response'=>$resp);
    }
}

if (!function_exists('xui_token_openapi')) {
    /** v3.6.0 protects openapi.json; fetch it with the configured API token. */
    function xui_token_openapi($panel, $timeout = 8)
    {
        $resp = xui_api_token_request($panel, 'GET', '/panel/api/openapi.json', null, $timeout);
        $decoded = xui_token_decode($resp);
        $ok = (int)($resp['status'] ?? 0) >= 200 && (int)($resp['status'] ?? 0) < 300 && is_array($decoded) && isset($decoded['paths']);
        return $ok ? array('ok'=>true,'spec'=>$decoded,'response'=>$resp) : array('ok'=>false,'spec'=>null,'message'=>trim((string)($resp['error'] ?? 'OpenAPI دریافت نشد.')),'response'=>$resp);
    }
}

if (!function_exists('xui_token_openapi_operations')) {
    function xui_token_openapi_operations(array $spec)
    {
        $out=array();
        foreach (($spec['paths'] ?? array()) as $path=>$methods) {
            if (!is_array($methods)) continue;
            foreach ($methods as $method=>$meta) {
                $m=strtoupper((string)$method);
                if (!in_array($m,array('GET','POST','PUT','PATCH','DELETE'),true) || !is_array($meta)) continue;
                $out[]=array('method'=>$m,'path'=>(string)$path,'summary'=>(string)($meta['summary']??''),'operationId'=>(string)($meta['operationId']??''),'tags'=>array_values((array)($meta['tags']??array())));
            }
        }
        return $out;
    }
}

if (!function_exists('xui_token_openapi_match')) {
    function xui_token_openapi_match(array $spec, $method, $actualPath)
    {
        $method=strtoupper(trim((string)$method));
        if (!in_array($method,array('GET','POST','PUT','PATCH','DELETE'),true)) return null;
        $pathOnly=parse_url((string)$actualPath,PHP_URL_PATH);
        if (!is_string($pathOnly) || strpos($pathOnly,'/panel/api/')!==0) return null;
        $actual=array_values(array_filter(explode('/',trim($pathOnly,'/')),static fn($v)=>$v!==''));
        foreach (($spec['paths']??array()) as $tpl=>$methods) {
            if (!is_array($methods) || !isset($methods[strtolower($method)])) continue;
            $parts=array_values(array_filter(explode('/',trim((string)$tpl,'/')),static fn($v)=>$v!==''));
            if (count($parts)!==count($actual)) continue;
            $match=true;
            foreach($parts as $i=>$seg){ if(preg_match('/^\{[^{}]+\}$/',$seg))continue; if(rawurldecode($seg)!==rawurldecode((string)$actual[$i])){$match=false;break;} }
            if(!$match)continue;
            $meta=is_array($methods[strtolower($method)])?$methods[strtolower($method)]:array();
            return array('method'=>$method,'path'=>(string)$tpl,'summary'=>(string)($meta['summary']??''),'operationId'=>(string)($meta['operationId']??''),'tags'=>array_values((array)($meta['tags']??array())));
        }
        return null;
    }
}

if (!function_exists('xui_token_get_inbound_detail')) {
    /** Fetch one full inbound object from 3x-ui v3.6.x. */
    function xui_token_get_inbound_detail($panel, $inboundId, $timeout = 8)
    {
        $id = (int)$inboundId;
        if ($id <= 0) return array('ok'=>false,'message'=>'Inbound ID معتبر نیست.');
        $resp = xui_api_token_request($panel, 'GET', '/panel/api/inbounds/get/' . $id, null, $timeout);
        $decoded = xui_token_decode($resp);
        $obj = is_array($decoded) && isset($decoded['obj']) && is_array($decoded['obj']) ? $decoded['obj'] : null;
        $ok = (int)($resp['status'] ?? 0) >= 200 && (int)($resp['status'] ?? 0) < 300 && is_array($obj) && (($decoded['success'] ?? true) !== false);
        if (!$ok) {
            $msg = is_array($decoded) ? trim((string)($decoded['msg'] ?? '')) : '';
            if ($msg === '') $msg = trim((string)($resp['error'] ?? 'Inbound detail دریافت نشد.'));
            return array('ok'=>false,'message'=>$msg,'response'=>$resp);
        }
        return array('ok'=>true,'inbound'=>$obj,'response'=>$resp);
    }
}

if (!function_exists('xui_token_set_monthly_reset_day')) {
    /**
     * 3x-ui v3.6.0 added a custom day-of-month for monthly inbound traffic
     * resets. Update the full inbound model so settings/clients are preserved.
     */
    function xui_token_set_monthly_reset_day($panel, $inboundId, $day, $timeout = 10)
    {
        $id = (int)$inboundId; $day = (int)$day;
        if ($id <= 0) return array('ok'=>false,'message'=>'Inbound ID معتبر نیست.');
        if ($day < 1 || $day > 31) return array('ok'=>false,'message'=>'روز Reset باید بین 1 تا 31 باشد.');
        $detail = xui_token_get_inbound_detail($panel, $id, $timeout);
        if (empty($detail['ok'])) return $detail;
        $inbound = (array)$detail['inbound'];
        $inbound['id'] = $id;
        $inbound['trafficReset'] = 'monthly';
        $inbound['trafficResetDay'] = $day;
        // Fields populated only by response projections should not be posted back.
        unset($inbound['clientStats'], $inbound['fallbackParent']);
        $resp = xui_api_token_request($panel, 'POST', '/panel/api/inbounds/update/' . $id, $inbound, $timeout);
        $decoded = xui_token_decode($resp);
        $status = (int)($resp['status'] ?? 0);
        $ok = $status >= 200 && $status < 300 && (!is_array($decoded) || (($decoded['success'] ?? true) !== false));
        if (!$ok) {
            $msg = is_array($decoded) ? trim((string)($decoded['msg'] ?? '')) : '';
            if ($msg === '') $msg = trim((string)($resp['error'] ?? ('HTTP ' . $status)));
            return array('ok'=>false,'message'=>$msg,'response'=>$resp);
        }
        return array('ok'=>true,'day'=>$day,'inbound'=>$decoded['obj'] ?? $inbound,'response'=>$resp);
    }
}

if (!function_exists('xui_subscription_url_with_format')) {
    function xui_subscription_url_with_format($url, $format)
    {
        $url=trim((string)$url); if($url==='')return '';
        $parts=parse_url($url); if(!is_array($parts)||empty($parts['scheme'])||empty($parts['host']))return '';
        $query=array(); if(!empty($parts['query']))parse_str($parts['query'],$query); $query['format']=$format;
        $base=$parts['scheme'].'://'.$parts['host'].(isset($parts['port'])?':'.$parts['port']:'').($parts['path']??'');
        return $base.'?'.http_build_query($query,'','&',PHP_QUERY_RFC3986).(!empty($parts['fragment'])?'#'.$parts['fragment']:'');
    }
}

if (!function_exists('xui_subscription_info')) {
    /** Fetch v3.6.0 live subscription info (?format=info). */
    function xui_subscription_info($subscriptionUrl, $timeout = 8)
    {
        $url=xui_subscription_url_with_format($subscriptionUrl,'info');
        if($url==='')return array('ok'=>false,'message'=>'Subscription URL معتبر نیست.');
        $req=new CurlRequest($url);$req->setTimeout($timeout);$req->setHeaders(array('Accept: application/json','User-Agent: iTzAYDiN-BOT/6 3x-ui-v3.6'));
        $resp=$req->get();$status=(int)($resp['status']??0);$decoded=isset($resp['body'])?json_decode((string)$resp['body'],true):null;
        return ($status>=200&&$status<300) ? array('ok'=>true,'info'=>is_array($decoded)?$decoded:(string)($resp['body']??''),'response'=>$resp) : array('ok'=>false,'message'=>(string)($resp['error']??('HTTP '.$status)),'response'=>$resp);
    }
}

if (!function_exists('xui_subscription_raw')) {
    function xui_subscription_raw($subscriptionUrl, $timeout = 8)
    {
        $url=trim((string)$subscriptionUrl); if(!filter_var($url,FILTER_VALIDATE_URL))return array('ok'=>false,'message'=>'Subscription URL معتبر نیست.');
        $req=new CurlRequest($url);$req->setTimeout($timeout);$req->setHeaders(array('Accept: */*','User-Agent: iTzAYDiN-BOT/6 3x-ui-v3.6'));
        $resp=$req->get();$status=(int)($resp['status']??0);
        return ($status>=200&&$status<300) ? array('ok'=>true,'body'=>(string)($resp['body']??''),'response'=>$resp) : array('ok'=>false,'message'=>(string)($resp['error']??('HTTP '.$status)),'response'=>$resp);
    }
}

if (!function_exists('xui_subscription_format')) {
    /**
     * 3x-ui v3.6 subscription format probe. The format value is deliberately
     * not hard-coded to a fixed list so future server-advertised formats can
     * be tested without another bot release.
     */
    function xui_subscription_format($subscriptionUrl, $format, $timeout = 8)
    {
        $format = trim((string)$format);
        if ($format === '' || !preg_match('/^[A-Za-z0-9._-]{1,40}$/', $format)) {
            return array('ok'=>false,'message'=>'format معتبر نیست. فقط حروف/عدد/._- مجاز است.');
        }
        $url = xui_subscription_url_with_format($subscriptionUrl, $format);
        if ($url === '') return array('ok'=>false,'message'=>'Subscription URL معتبر نیست.');
        $req = new CurlRequest($url);
        $req->setTimeout($timeout);
        $req->setHeaders(array('Accept: */*','User-Agent: iTzAYDiN-BOT/6 3x-ui-v3.6-format-probe'));
        $resp = $req->get();
        $status = (int)($resp['status'] ?? 0);
        $body = (string)($resp['body'] ?? '');
        return ($status >= 200 && $status < 300)
            ? array('ok'=>true,'format'=>$format,'body'=>$body,'response'=>$resp)
            : array('ok'=>false,'format'=>$format,'message'=>(string)($resp['error'] ?? ('HTTP '.$status)),'response'=>$resp);
    }
}

if (!function_exists('xui_subscription_user_agent_probe')) {
    /** Exercise v3.6 User-Agent based subscription format auto-detection. */
    function xui_subscription_user_agent_probe($subscriptionUrl, $userAgent, $timeout = 8)
    {
        $url = trim((string)$subscriptionUrl);
        $ua = trim((string)$userAgent);
        if (!filter_var($url, FILTER_VALIDATE_URL)) return array('ok'=>false,'message'=>'Subscription URL معتبر نیست.');
        if ($ua === '' || strlen($ua) > 220 || preg_match('/[\r\n]/', $ua)) return array('ok'=>false,'message'=>'User-Agent معتبر نیست.');
        $req = new CurlRequest($url);
        $req->setTimeout($timeout);
        $req->setHeaders(array('Accept: */*','User-Agent: '.$ua));
        $resp = $req->get();
        $status = (int)($resp['status'] ?? 0);
        return ($status >= 200 && $status < 300)
            ? array('ok'=>true,'user_agent'=>$ua,'body'=>(string)($resp['body'] ?? ''),'response'=>$resp)
            : array('ok'=>false,'user_agent'=>$ua,'message'=>(string)($resp['error'] ?? ('HTTP '.$status)),'response'=>$resp);
    }
}

if (!function_exists('xui_token_client_record')) {
    /**
     * Read the modern 3x-ui client record. v3.6.x returns the client together
     * with every inbound id assigned to it.
     */
    function xui_token_client_record($panel, $email)
    {
        $resp = xui_api_token_request(
            $panel,
            'GET',
            '/panel/api/clients/get/' . rawurlencode((string) $email),
            null,
            6
        );
        $decoded = xui_token_decode($resp);
        if (!is_array($decoded) || (($resp['status'] ?? 0) < 200 || ($resp['status'] ?? 0) >= 300) || ($decoded['success'] ?? false) !== true) {
            return array('response' => $resp, 'client' => null, 'inboundIds' => array());
        }
        $obj = isset($decoded['obj']) && is_array($decoded['obj']) ? $decoded['obj'] : array();
        $client = isset($obj['client']) && is_array($obj['client']) ? $obj['client'] : null;
        $inboundIds = isset($obj['inboundIds']) && is_array($obj['inboundIds']) ? $obj['inboundIds'] : array();
        return array('response' => $resp, 'client' => $client, 'inboundIds' => $inboundIds);
    }
}

if (!function_exists('xui_token_normalize_client')) {
    /** Normalize legacy/new client arrays to the model.Client JSON used by 3x-ui. */
    function xui_token_normalize_client(array $client, array $fallback = array())
    {
        $merged = $fallback;
        $uuidValue = null;
        foreach ($client as $key => $value) {
            if ($key === 'uuid') {
                $uuidValue = $value;
                continue;
            }
            if ($key === 'total') {
                $merged['totalGB'] = $value;
                continue;
            }
            // Legacy callers often send an empty flow while extending a user.
            // On token mode, preserve the existing flow instead of clearing it.
            if ($key === 'flow' && trim((string) $value) === '') {
                continue;
            }
            $merged[$key] = $value;
        }

        if ($uuidValue !== null && trim((string) $uuidValue) !== '') {
            $merged['id'] = (string) $uuidValue;
        } elseif (!isset($merged['id']) && isset($merged['uuid'])) {
            $merged['id'] = $merged['uuid'];
        }
        unset($merged['uuid'], $merged['createdAt'], $merged['updatedAt'], $merged['inboundIds']);

        // ClientRecord stores WireGuard allowedIPs as text, while model.Client
        // expects a JSON array. Avoid sending an incompatible string on update.
        if (isset($merged['allowedIPs']) && is_string($merged['allowedIPs'])) {
            $rawAllowed = trim($merged['allowedIPs']);
            if ($rawAllowed === '') {
                unset($merged['allowedIPs']);
            } else {
                $decodedAllowed = json_decode($rawAllowed, true);
                if (is_array($decodedAllowed)) {
                    $merged['allowedIPs'] = array_values(array_map('strval', $decodedAllowed));
                } else {
                    $merged['allowedIPs'] = array_values(array_filter(array_map('trim', explode(',', $rawAllowed)), 'strlen'));
                }
            }
        }

        foreach (array('limitIp', 'tgId', 'reset', 'keepAlive') as $key) {
            if (!isset($merged[$key]) || $merged[$key] === '') {
                $merged[$key] = 0;
            } else {
                $merged[$key] = (int) $merged[$key];
            }
        }
        foreach (array('totalGB', 'expiryTime') as $key) {
            if (isset($merged[$key]) && $merged[$key] !== '') {
                $merged[$key] = (int) $merged[$key];
            }
        }
        if (isset($merged['enable'])) {
            $merged['enable'] = filter_var($merged['enable'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
            if ($merged['enable'] === null) {
                $merged['enable'] = true;
            }
        }
        if (isset($merged['flow']) && trim((string) $merged['flow']) === '') {
            unset($merged['flow']);
        }
        return $merged;
    }
}

if (!function_exists('xui_token_legacy_client_response')) {
    /**
     * Compatibility adapter: modern 3x-ui splits profile and traffic data,
     * while the rest of this bot expects the old 2.9 getClientTraffics shape.
     */
    function xui_token_legacy_client_response($panel, $email)
    {
        $record = xui_token_client_record($panel, $email);
        if (!is_array($record['client'] ?? null)) {
            return $record['response'];
        }

        $client = $record['client'];
        $inboundIds = $record['inboundIds'] ?? array();
        $trafficResp = xui_api_token_request(
            $panel,
            'GET',
            '/panel/api/clients/traffic/' . rawurlencode((string) $email),
            null,
            6
        );
        $trafficJson = xui_token_decode($trafficResp);
        $traffic = (is_array($trafficJson) && isset($trafficJson['obj']) && is_array($trafficJson['obj']))
            ? $trafficJson['obj']
            : array();

        $inboundId = $traffic['inboundId'] ?? null;
        if (($inboundId === null || $inboundId === '') && !empty($inboundIds)) {
            $inboundId = reset($inboundIds);
        }

        $legacy = array(
            'id'         => $traffic['id'] ?? null,
            'inboundId'  => $inboundId !== null ? (int) $inboundId : null,
            'enable'     => array_key_exists('enable', $client) ? (bool) $client['enable'] : (bool) ($traffic['enable'] ?? true),
            'email'      => (string) ($client['email'] ?? $email),
            'up'         => (int) ($traffic['up'] ?? 0),
            'down'       => (int) ($traffic['down'] ?? 0),
            'expiryTime' => (int) ($client['expiryTime'] ?? $traffic['expiryTime'] ?? 0),
            'total'      => (int) ($client['totalGB'] ?? $traffic['total'] ?? 0),
            'totalGB'    => (int) ($client['totalGB'] ?? $traffic['total'] ?? 0),
            'reset'      => (int) ($client['reset'] ?? 0),
            'uuid'       => (string) ($client['uuid'] ?? $client['id'] ?? ''),
            'subId'      => (string) ($client['subId'] ?? ''),
            'flow'       => (string) ($client['flow'] ?? ''),
            'limitIp'    => (int) ($client['limitIp'] ?? 0),
            'tgId'       => (int) ($client['tgId'] ?? 0),
            'group'      => (string) ($client['group'] ?? ''),
            'comment'    => (string) ($client['comment'] ?? ''),
        );

        return array(
            'status' => 200,
            'body' => json_encode(
                array('success' => true, 'msg' => '', 'obj' => $legacy),
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ),
        );
    }
}

if (!function_exists('xui_token_single_link')) {
    /**
     * Fetch a client's connection links directly from a modern 3x-ui panel via
     * the token API (/panel/api/clients/links/{email}). Returns the first
     * vless/vmess/trojan link or null.
     */
    function xui_token_single_link($panel, $email)
    {
        if (!xui_panel_uses_token($panel) || $email === null || $email === '') {
            return null;
        }
        $resp = xui_api_token_request($panel, 'GET', '/panel/api/clients/links/' . rawurlencode($email), null, 6);
        if (empty($resp['body'])) {
            return null;
        }
        $decoded = json_decode($resp['body'], true);
        if (!is_array($decoded)) {
            return null;
        }
        $obj = $decoded['obj'] ?? $decoded;
        $candidates = array();
        if (is_string($obj)) {
            $candidates = preg_split('/\R/', trim($obj)) ?: array();
        } elseif (is_array($obj)) {
            foreach ($obj as $item) {
                if (is_string($item)) {
                    $candidates[] = $item;
                } elseif (is_array($item)) {
                    foreach (array('uri', 'link', 'url') as $k) {
                        if (!empty($item[$k]) && is_string($item[$k])) {
                            $candidates[] = $item[$k];
                        }
                    }
                }
            }
        }
        foreach ($candidates as $candidate) {
            $candidate = trim((string) $candidate);
            if ($candidate !== '' && preg_match('/^(vless|vmess|trojan):\/\//i', $candidate)) {
                return $candidate;
            }
        }
        return null;
    }
}

if (!function_exists('xuisingle_cookie_path')) {
    function xuisingle_cookie_path() {
        static $path = null;
        if ($path === null) {
            try {
                $entropy = bin2hex(random_bytes(8));
            } catch (\Throwable $e) {
                $entropy = uniqid('', true);
            }
            $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR
                . 'xuisingle_cookie_' . getmypid() . '_' . $entropy . '.txt';
        }
        return $path;
    }
}

function panel_login_cookie($code_panel)
{
    $panel = select("marzban_panel", "*", "code_panel", $code_panel, "select");
    $curl = curl_init();
    if (function_exists('aycore_apply_curl_proxy')) aycore_apply_curl_proxy($curl, 'panel');
    curl_setopt_array($curl, array(
        CURLOPT_URL => $panel['url_panel'] . '/login',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT_MS => 4000,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_POSTFIELDS => "username={$panel['username_panel']}&password=" . urlencode($panel['password_panel']),
        CURLOPT_COOKIEJAR => xuisingle_cookie_path(),
    ));
    $response = curl_exec($curl);
    if (curl_error($curl)) {
        $token = [];
        $token['errror'] = curl_error($curl);
        return $token;
    }
    curl_close($curl);
    return $response;
}
function login($code_panel, $verify = true)
{
    $panel = select("marzban_panel", "*", "code_panel", $code_panel, "select");
    if ($panel['datelogin'] != null && $verify) {
        $date = json_decode($panel['datelogin'], true);
        if (isset($date['time'])) {
            $timecurrent = time();
            $start_date = time() - strtotime($date['time']);
            if ($start_date <= 3000) {
                file_put_contents(xuisingle_cookie_path(), $date['access_token']);
                return;
            }
        }
    }
    $response = panel_login_cookie($panel['code_panel']);
    $time = date('Y/m/d H:i:s');
    $data = json_encode(array(
        'time' => $time,
        'access_token' => file_get_contents(xuisingle_cookie_path())
    ));
    update("marzban_panel", "datelogin", $data, 'name_panel', $panel['name_panel']);
    if (!is_string($response))
        return array('success' => false);
    return json_decode($response, true);
}

function panel_get_inbounds_list($baseUrl)
{
    $req = new CurlRequest(rtrim($baseUrl, '/') . '/panel/api/inbounds/list');
    $req->setTimeout(5);
    $req->setHeaders(['Accept: application/json']);
    $req->setCookie(xuisingle_cookie_path());
    return $req->get();
}

function find_uuid_by_email_in_list($jsonList, $email)
{
    $j = json_decode($jsonList, true);
    if (!($j['success'] ?? false)) {
        return null;
    }
    foreach ($j['obj'] ?? [] as $inb) {
        foreach (($inb['clientStats'] ?? []) as $c) {
            if (($c['email'] ?? '') === $email) {
                return $c['uuid'] ?? $c['id'] ?? null;
            }
        }
        $settings = $inb['settings'] ?? '{}';
        if (is_string($settings)) {
            $settings = json_decode($settings, true);
        }
        foreach (($settings['clients'] ?? []) as $c) {
            if (($c['email'] ?? '') === $email) {
                return $c['id'] ?? null;
            }
        }
    }
    return null;
}

function get_client_traffic_by_uuid($baseUrl, $uuid)
{
    $req = new CurlRequest(rtrim($baseUrl, '/') . "/panel/api/inbounds/getClientTrafficsById/{$uuid}");
    $req->setHeaders(['Accept: application/json']);
    $req->setCookie(xuisingle_cookie_path());
    return $req->get();
}

function extract_links_from_raw_subscription($raw)
{
    if (!is_string($raw)) {
        return [];
    }
    $trimmed = trim($raw);
    if ($trimmed === '') {
        return [];
    }
    $decoded = base64_decode($trimmed, true);
    if ($decoded !== false && preg_match('/(vless|vmess|trojan):\/\//i', $decoded)) {
        $text = $decoded;
    } else {
        $text = $raw;
    }
    $links = [];
    $lines = preg_split('/\R/', trim($text));
    if (!is_array($lines)) {
        return [];
    }
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '') {
            continue;
        }
        if (preg_match('/^(vless|vmess|trojan):\/\//i', $line)) {
            $links[] = $line;
        }
    }
    return $links;
}

function pick_single_link_text($raw)
{
    $links = extract_links_from_raw_subscription($raw);
    return $links[0] ?? null;
}

function fetch_subscription_links_with_retry($subscriptionUrl, $attempts = 1, $delayMicroseconds = 500000)
{
    $attempts = max(1, (int)$attempts);
    $links = [];
    $lastBody = null;
    for ($i = 0; $i < $attempts; $i++) {
        $req = new CurlRequest($subscriptionUrl);
        $req->setTimeout(6);
        $req->setHeaders(['User-Agent: SubFetcher/1.3']);
        $res = $req->get();
        if (($res['status'] ?? 0) >= 200 && ($res['status'] ?? 0) < 300 && isset($res['body'])) {
            $lastBody = $res['body'];
            $links = extract_links_from_raw_subscription($res['body']);
            if (!empty($links)) {
                return ['links' => $links, 'body' => $res['body']];
            }
        }
        if ($i < $attempts - 1) {
            usleep($delayMicroseconds);
        }
    }
    return ['links' => $links, 'body' => $lastBody];
}

function fallback_single_link_from_clients_api($username, $panelName)
{
    if (!$username || !$panelName || !function_exists('GetClientsS_UI')) {
        return null;
    }
    $clientData = GetClientsS_UI($username, $panelName);
    if (!is_array($clientData) || empty($clientData)) {
        return null;
    }
    $candidates = [];
    if (isset($clientData['uri'])) {
        if (is_array($clientData['uri'])) {
            $candidates = array_merge($candidates, $clientData['uri']);
        } else {
            $candidates[] = $clientData['uri'];
        }
    }
    if (isset($clientData['links']) && is_array($clientData['links'])) {
        foreach ($clientData['links'] as $item) {
            if (is_string($item)) {
                $candidates[] = $item;
            } elseif (is_array($item) && isset($item['uri'])) {
                $candidates[] = $item['uri'];
            }
        }
    }
    if (isset($clientData['config']) && is_array($clientData['config'])) {
        foreach ($clientData['config'] as $item) {
            if (is_string($item)) {
                $candidates[] = $item;
            } elseif (is_array($item) && isset($item['uri'])) {
                $candidates[] = $item['uri'];
            }
        }
    }
    foreach ($candidates as $candidate) {
        if (!is_string($candidate)) {
            continue;
        }
        $candidate = trim($candidate);
        if ($candidate === '') {
            continue;
        }
        if (preg_match('/^(vless|vmess|trojan):\/\//i', $candidate)) {
            return $candidate;
        }
    }
    return null;
}

function get_subscription_links_with_retry($subscriptionUrl, $attempts = 3, $delayMicroseconds = 1200000)
{
    $result = fetch_subscription_links_with_retry($subscriptionUrl, $attempts, $delayMicroseconds);
    return $result['links'];
}

function pick_single_link_from_sub($subUrl)
{
    $result = fetch_subscription_links_with_retry($subUrl, 1, 0);
    return $result['links'][0] ?? null;
}

function panel_get_inbound($baseUrl, $inboundId)
{
    $req = new CurlRequest(rtrim($baseUrl, '/') . '/panel/api/inbounds/get/' . $inboundId);
    $req->setTimeout(5);
    $req->setHeaders(['Accept: application/json']);
    $req->setCookie(xuisingle_cookie_path());
    $r = $req->get();
    if (($r['status'] ?? 0) >= 200 && ($r['status'] ?? 0) < 300) {
        $j = json_decode($r['body'] ?? '', true);
        if (!is_array($j)) {
            return null;
        }
        $obj = $j['obj'] ?? null;
        if (!is_array($obj)) {
            return null;
        }
        if (isset($obj['settings']) && is_string($obj['settings'])) {
            $decodedSettings = json_decode($obj['settings'], true);
            if (is_array($decodedSettings)) {
                $obj['settings'] = $decodedSettings;
            }
        }
        if (isset($obj['streamSettings']) && is_string($obj['streamSettings'])) {
            $decodedStream = json_decode($obj['streamSettings'], true);
            if (is_array($decodedStream)) {
                $obj['streamSettings'] = $decodedStream;
            }
        }
        return $obj;
    }
    return null;
}

function find_client_by_subId($inbObj, $subId)
{
    $settings = $inbObj['settings'] ?? [];
    if (is_string($settings)) {
        $settings = json_decode($settings, true);
    }
    $settings = is_array($settings) ? $settings : [];
    foreach (($settings['clients'] ?? []) as $c) {
        if (($c['subId'] ?? '') === $subId) {
            return $c;
        }
    }
    return null;
}

function build_vless_link_from_inbound($inb, $client, $publicHost)
{
    $uuid = $client['id'] ?? $client['uuid'] ?? '';
    if ($uuid === '') {
        return null;
    }
    $port = $inb['port'] ?? '';
    $remark = $inb['remark'] ?? ($client['email'] ?? '');
    $stream = $inb['streamSettings'] ?? [];
    if (is_string($stream)) {
        $stream = json_decode($stream, true);
    }
    $stream = is_array($stream) ? $stream : [];
    $net = $stream['network'] ?? 'tcp';
    $sec = $stream['security'] ?? 'none';
    $q = [
        'type' => $net,
        'security' => $sec,
        'encryption' => 'none',
    ];
    if (!empty($client['flow'])) {
        $q['flow'] = $client['flow'];
    }
    if ($net === 'tcp') {
        $hdr = $stream['tcpSettings']['header']['type'] ?? 'none';
        if ($hdr !== 'none') {
            $q['headerType'] = $hdr;
        }
    }
    if ($sec === 'reality') {
        $rs = $stream['realitySettings'] ?? [];
        $set = $rs['settings'] ?? [];
        $pbk = $set['publicKey'] ?? null;
        $sid = $rs['shortIds'][0] ?? null;
        $sni = $set['serverName'] ?? ($rs['serverNames'][0] ?? null);
        if (!$sni && !empty($rs['dest']) && strpos($rs['dest'], ':') !== false) {
            $parts = explode(':', $rs['dest']);
            $sni = $parts[0];
        }
        $fp = $set['fingerprint'] ?? null;
        $spx = $set['spiderX'] ?? null;
        if ($pbk) {
            $q['pbk'] = $pbk;
        }
        if ($sid) {
            $q['sid'] = $sid;
        }
        if ($sni) {
            $q['sni'] = $sni;
        }
        if ($fp) {
            $q['fp'] = $fp;
        }
        if ($spx && $spx !== '/') {
            $q['spx'] = $spx;
        }
    }
    $query = http_build_query($q, '', '&', PHP_QUERY_RFC3986);
    $hash = rawurlencode($remark ?: ($client['email'] ?? ''));
    return "vless://{$uuid}@{$publicHost}:{$port}?{$query}#{$hash}";
}

function get_single_link_smart($panelBase, $inboundId, $subscriptionUrl, $username = null, $panelName = null, $panelCode = null)
{
    $subscriptionUrl = is_string($subscriptionUrl) ? trim($subscriptionUrl) : '';
    if ($subscriptionUrl === '') {
        return null;
    }
    $result = fetch_subscription_links_with_retry($subscriptionUrl, 1, 500000);
    if (!empty($result['links'])) {
        return $result['links'][0];
    }
    if ($panelName) {
        $panelRowForToken = select("marzban_panel", "*", "name_panel", $panelName, "select");
        if (xui_panel_uses_token($panelRowForToken)) {
            $tokenLink = xui_token_single_link($panelRowForToken, $username);
            if ($tokenLink) {
                return $tokenLink;
            }
        }
    }
    $fallback = fallback_single_link_from_clients_api($username, $panelName);
    if ($fallback) {
        return $fallback;
    }
    $panelBase = rtrim((string)$panelBase, '/');
    if ($panelBase === '') {
        return null;
    }
    if ($panelCode) {
        login($panelCode);
    }
    $inb = panel_get_inbound($panelBase, $inboundId);
    if (!$inb) {
        return null;
    }
    $path = parse_url($subscriptionUrl, PHP_URL_PATH) ?: '';
    $subId = basename($path);
    if (!$subId) {
        return null;
    }
    $client = find_client_by_subId($inb, $subId);
    if (!$client) {
        return null;
    }
    $host = parse_url($subscriptionUrl, PHP_URL_HOST) ?: 'localhost';
    return build_vless_link_from_inbound($inb, $client, $host);
}

function get_single_link_after_create($panelBase, $inboundId, $subscriptionUrl, $username, $panelName, $panelCode = null)
{
    return get_single_link_smart($panelBase, $inboundId, $subscriptionUrl, $username, $panelName, $panelCode);
}

function get_clinets($username, $namepanel)
{
    $marzban_list_get = select("marzban_panel", "*", "name_panel", $namepanel, "select");
    if (xui_panel_uses_token($marzban_list_get)) {
        return xui_token_legacy_client_response($marzban_list_get, $username);
    }
    login($marzban_list_get['code_panel']);
    $base = rtrim($marzban_list_get['url_panel'], '/');
    $url = $base . "/panel/api/inbounds/getClientTraffics/$username";
    $headers = array(
        'Accept: application/json',
        'Content-Type: application/json',
    );
    $req = new CurlRequest($url);
    $req->setHeaders($headers);
    $req->setCookie(xuisingle_cookie_path());
    $response = $req->get();

    if (isset($response['body'])) {
        $decodedBody = json_decode($response['body'], true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decodedBody)) {
            if (isset($decodedBody['success']) && $decodedBody['success'] === false) {
                $response['error'] = $decodedBody['msg'] ?? 'Unknown panel error';
            }
        }
    }

    if (!empty($response['error']) && stripos($response['error'], 'Inbound Not Found For Email') !== false) {
        $list = panel_get_inbounds_list($base);
        if (($list['status'] ?? 0) >= 200 && ($list['status'] ?? 0) < 300) {
            $uuid = find_uuid_by_email_in_list($list['body'] ?? '', $username);
            if ($uuid) {
                $byId = get_client_traffic_by_uuid($base, $uuid);
                if (($byId['status'] ?? 0) >= 200 && ($byId['status'] ?? 0) < 300) {
                    $response = $byId;
                    $response['error'] = null;
                }
            }
        }
    }


    if (!empty($response['error']) && stripos((string) $response['error'], 'Inbound Not Found For Email') === false) {
        $dedupKey = 'xui_single_resp|' . (string) $response['error'];
        if (function_exists('aycore_dedup_error_log')) {
            aycore_dedup_error_log($dedupKey, json_encode($response));
        } else {
            error_log(json_encode($response));
        }
    }

    if (is_file(xuisingle_cookie_path())) {
        @unlink(xuisingle_cookie_path());
    }

    return $response;
}
function addClient($namepanel, $usernameac, $Expire, $Total, $Uuid, $Flow, $subid, $inboundid, $name_product, $note = "")
{
    $marzban_list_get = select("marzban_panel", "*", "name_panel", $namepanel, "select");
    if (!xui_panel_uses_token($marzban_list_get)) {
        login($marzban_list_get['code_panel']);
    }
    if ($name_product == "usertest") {
        if ($marzban_list_get['on_hold_test'] == "1") {
            if ($Expire == 0) {
                $timeservice = 0;
            } else {
                $timelast = $Expire - time();
                $timeservice = -intval(($timelast / 86400) * 86400000);
            }
        } else {
            $timeservice = $Expire * 1000;
        }
    } else {
        if ($marzban_list_get['conecton'] == "onconecton") {
            if ($Expire == 0) {
                $timeservice = 0;
            } else {
                $timelast = $Expire - time();
                $timeservice = -intval(($timelast / 86400) * 86400000);
            }
        } else {
            $timeservice = $Expire * 1000;
        }
    }
    if (xui_panel_uses_token($marzban_list_get)) {
        if (!isset($usernameac) || trim((string)$usernameac) === '') {
            return array(
                'status' => 422,
                'error' => 'نام کاربری سرویس خالی است.',
                'body' => json_encode(array('success' => false, 'msg' => 'نام کاربری سرویس خالی است.'), JSON_UNESCAPED_UNICODE),
            );
        }

        // 3x-ui 3.6.x resolves every requested inbound before inserting the
        // client. A stale/default ID therefore surfaces as the vague GORM
        // "record not found". Resolve against the panel itself first so the
        // bot can show the admin the actual available inbound IDs.
        //
        // Modern mode can optionally run in "auto all active" mode. In that mode newly
        // created inbounds are picked up automatically for future clients,
        // without changing old subscriptions or mixing this with Legacy mode.
        $requestedInbound = $inboundid;
        if ((string)($marzban_list_get['xui_auto_inbounds'] ?? '0') === '1') {
            $live = xui_refresh_inbounds($marzban_list_get, 8);
            if (!empty($live['ok'])) {
                $activeIds = array();
                foreach ((array)($live['items'] ?? array()) as $it) {
                    if (!empty($it['enable']) && (int)($it['id'] ?? 0) > 0) $activeIds[] = (int)$it['id'];
                }
                if ($activeIds) $requestedInbound = $activeIds;
            }
        }
        $resolved = xui_token_resolve_inbound_ids($marzban_list_get, $requestedInbound);
        if (empty($resolved['ok'])) {
            $friendly = trim((string)($resolved['message'] ?? 'Inbound معتبر پیدا نشد.'));
            return array(
                'status' => 422,
                'error' => $friendly,
                'body' => json_encode(array(
                    'success' => false,
                    'msg' => $friendly,
                    'code' => 'ITZ_XUI_INBOUND_REQUIRED',
                    'availableInbounds' => $resolved['items'] ?? array(),
                ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            );
        }

        $client = array(
            'id' => (string)$Uuid,
            'security' => 'auto',
            'email' => trim((string)$usernameac),
            'totalGB' => (int)$Total,
            'expiryTime' => (int)$timeservice,
            'enable' => true,
            'tgId' => 0,
            'subId' => (string)$subid,
            'limitIp' => 0,
            'reset' => 0,
            'comment' => (string)$note,
        );
        if ($Flow !== '' && $Flow !== null) {
            $client['flow'] = (string)$Flow;
        }

        $response = xui_api_token_request(
            $marzban_list_get,
            'POST',
            '/panel/api/clients/add',
            array(
                'client' => $client,
                'inboundIds' => array_values(array_map('intval', $resolved['ids'])),
            )
        );
        $response['itz_inbound_ids'] = array_values(array_map('intval', $resolved['ids']));
        $response['itz_inbound_auto'] = !empty($resolved['auto']);

        // If an inbound is removed in the tiny gap between validation and create,
        // translate GORM's generic message into something an admin can actually fix.
        $createJson = xui_token_decode($response);
        $createMsg = is_array($createJson) ? trim((string)($createJson['msg'] ?? '')) : '';
        if ($createMsg !== '' && stripos($createMsg, 'record not found') !== false) {
            $response['error'] = 'Inbound انتخاب‌شده دیگر روی پنل وجود ندارد یا لحظه‌ای حذف شده است. از مدیریت پنل، لیست Inboundها را تازه‌سازی و دوباره انتخاب کن.';
        }
        return $response;
    }
    $config = array(
        "id" => intval($inboundid),
        'settings' => json_encode(array(
            'clients' => array(
                array(
                    "id" => $Uuid,
                    "flow" => $Flow,
                    "email" => $usernameac,
                    "totalGB" => $Total,
                    "expiryTime" => $timeservice,
                    "enable" => true,
                    "tgId" => "",
                    "subId" => $subid,
                    "reset" => 0,
                    "comment" => $note
                )
            ),
            'decryption' => 'none',
            'fallbacks' => array(),
        ))
    );
    if (!isset($usernameac))
        return array(
            'status' => 500,
            'msg' => 'username is null'
        );
    $configpanel = json_encode($config, true);
    $url = $marzban_list_get['url_panel'] . '/panel/api/inbounds/addClient';
    $headers = array(
        'Accept: application/json',
        'Content-Type: application/json',
    );
    $req = new CurlRequest($url);
    $req->setHeaders($headers);
    $req->setCookie(xuisingle_cookie_path());
    $response = $req->post($configpanel);
    @unlink(xuisingle_cookie_path());
    return $response;
}
function updateClient($namepanel, $uuid, array $config)
{
    $marzban_list_get = select("marzban_panel", "*", "name_panel", $namepanel, "select");
    if (xui_panel_uses_token($marzban_list_get)) {
        $settings = $config['settings'] ?? array();
        if (is_string($settings)) {
            $settings = json_decode($settings, true);
        }
        $incoming = is_array($settings) && isset($settings['clients'][0]) && is_array($settings['clients'][0])
            ? $settings['clients'][0]
            : null;
        if (!is_array($incoming)) {
            return array(
                'status' => 500,
                'body' => json_encode(array('success' => false, 'msg' => 'client payload missing')),
            );
        }

        $email = trim((string) ($incoming['email'] ?? ''));
        if ($email === '') {
            return array(
                'status' => 500,
                'body' => json_encode(array('success' => false, 'msg' => 'client email missing')),
            );
        }

        // v3.6.x update accepts a full model.Client. Merge the requested
        // changes over the current record so omitted fields are not zeroed.
        $record = xui_token_client_record($marzban_list_get, $email);
        if (!is_array($record['client'] ?? null)) {
            return $record['response'];
        }
        $existing = xui_token_normalize_client($record['client']);
        $client = xui_token_normalize_client($incoming, $existing);
        $client['email'] = $email;

        return xui_api_token_request(
            $marzban_list_get,
            'POST',
            '/panel/api/clients/update/' . rawurlencode($email),
            $client
        );
    }
    login($marzban_list_get['code_panel']);
    $configpanel = json_encode($config, true);
    $url = $marzban_list_get['url_panel'] . '/panel/api/inbounds/updateClient/' . $uuid;
    $headers = array(
        'Accept: application/json',
        'Content-Type: application/json',
    );
    $req = new CurlRequest($url);
    $req->setHeaders($headers);
    $req->setCookie(xuisingle_cookie_path());
    $response = $req->post($configpanel);
    @unlink(xuisingle_cookie_path());
    return $response;
}
function ResetUserDataUsagex_uisin($usernamepanel, $namepanel)
{
    $panel_token_row = select("marzban_panel", "*", "name_panel", $namepanel, "select");
    if (xui_panel_uses_token($panel_token_row)) {
        return xui_api_token_request(
            $panel_token_row,
            'POST',
            '/panel/api/clients/resetTraffic/' . rawurlencode($usernamepanel)
        );
    }
    $data_user = get_clinets($usernamepanel, $namepanel);
    $data_user = json_decode($data_user['body'], true)['obj'];
    $marzban_list_get = select("marzban_panel", "*", "name_panel", $namepanel, "select");
    login($marzban_list_get['code_panel']);
    $url = $marzban_list_get['url_panel'] . "/panel/api/inbounds/{$data_user['inboundId']}/resetClientTraffic/" . $usernamepanel;
    $headers = array(
        'Accept: application/json',
        'Content-Type: application/json',
    );
    $req = new CurlRequest($url);
    $req->setHeaders($headers);
    $req->setCookie(xuisingle_cookie_path());
    $response = $req->post(array());
    @unlink(xuisingle_cookie_path());
    return $response;
}
function removeClient($location, $username)
{
    $marzban_list_get = select("marzban_panel", "*", "name_panel", $location, "select");
    if (xui_panel_uses_token($marzban_list_get)) {
        return xui_api_token_request(
            $marzban_list_get,
            'POST',
            '/panel/api/clients/del/' . rawurlencode($username)
        );
    }
    login($marzban_list_get['code_panel']);
    $url = $marzban_list_get['url_panel'] . "/panel/api/inbounds/{$marzban_list_get['inboundid']}/delClientByEmail/" . $username;
    $headers = array(
        'Accept: application/json',
        'Content-Type: application/json',
    );
    $req = new CurlRequest($url);
    $req->setHeaders($headers);
    $req->setCookie(xuisingle_cookie_path());
    $response = $req->post(array());
    @unlink(xuisingle_cookie_path());
    return $response;
}
