<?php
/**
 * Stream Proxy / Restreaming — PHP port of backend/routes/proxy.py.
 *
 * Endpoints exposed (registered in api/index.php):
 *   POST /api/proxy/sign                      → returns short-lived URL
 *   GET  /api/proxy/live/{token}.m3u8         → rewritten HLS playlist
 *   GET  /api/proxy/seg/{token}               → HLS segment via cache
 *   GET  /api/proxy/vod/{filename}            → VOD pass-through w/ Range
 *   GET  /api/proxy/stats                     → admin-only hub stats
 *
 * Restricted to Shared Profiles ONLY: any sign request whose
 * profile_id does not start with `shared:` is rejected with 403.
 *
 * Caching:  see lib/segment_cache.php (auto-detect APCu → Redis → file).
 * Token:    see lib/proxy_signer.php  (HMAC-SHA256, 30 min TTL).
 */

require_once __DIR__ . '/../lib/proxy_signer.php';
require_once __DIR__ . '/../lib/segment_cache.php';

// ───────────────────────────────────────────────────────────────────
//  Helpers
// ───────────────────────────────────────────────────────────────────
function _ndp_proxy_resolve_shared(string $profile_id): array {
    if ($profile_id === '' || strpos($profile_id, 'shared:') !== 0) {
        json_error(403, 'proxy is restricted to shared profiles');
    }
    $bare = substr($profile_id, strlen('shared:'));
    $settings = get_settings_doc();
    $shared = $settings['shared_profiles'] ?? [];
    foreach ($shared as $sp) {
        $sp_id = (string)($sp['id'] ?? $sp['_id'] ?? '');
        if ($sp_id === '') continue;
        if ($sp_id === $bare || ('shared:' . $sp_id) === $profile_id) return $sp;
    }
    json_error(403, 'shared profile not found');
}

function _ndp_xtream_base(array $sp): string {
    return rtrim((string)($sp['server'] ?? ''), '/');
}

function _ndp_build_upstream_live(array $sp, string $stream_id): string {
    $base = _ndp_xtream_base($sp);
    $u = rawurlencode((string)($sp['username'] ?? ''));
    $p = rawurlencode((string)($sp['password'] ?? ''));
    $s = rawurlencode($stream_id);
    return "$base/live/$u/$p/$s.m3u8";
}

function _ndp_build_upstream_vod(array $sp, string $stype, string $stream_id, string $ext): string {
    $base = _ndp_xtream_base($sp);
    $u = rawurlencode((string)($sp['username'] ?? ''));
    $p = rawurlencode((string)($sp['password'] ?? ''));
    $s = rawurlencode($stream_id);
    $e = rawurlencode($ext);
    $bucket = ($stype === 'movie') ? 'movie' : 'series';
    return "$base/$bucket/$u/$p/$s.$e";
}

/**
 * Open a streaming cURL handle. Caller must `curl_close` after.
 *
 * Hardened in v1.3.1 for IPTV upstreams on non-standard ports
 * (e.g. :8080, :25461). The defaults below address the most common
 * shared-host failure modes:
 *   - HTTP/1.1 forced (some Xtream servers reject HTTP/2 frames).
 *   - SSL_VERIFYPEER off (relays public stream content; cPanel CA
 *     bundles are commonly stale and falsely reject valid certs).
 *   - NOSIGNAL=1 (avoid SIGALRM on shared FPM pools, which crashes
 *     the worker mid-segment fetch).
 *   - Accept star/star (some upstreams 406 on a missing Accept header).
 *   - TCP_NODELAY=1 (lower TTFB on small playlist responses).
 *
 * @param array $extra_headers Additional headers (e.g., Range).
 */
function _ndp_curl_open(string $url, array $extra_headers = []) {
    $ch = curl_init($url);
    // Spoof a common IPTV player user-agent. Many upstream providers
    // return 407/403 to anything that smells like a proxy/scraper, so
    // we present as a stock Android IPTV client instead.
    // Override per-call via $extra_headers['User-Agent'] when needed.
    $upstream_host = parse_url($url, PHP_URL_HOST) ?: '';
    $upstream_scheme = parse_url($url, PHP_URL_SCHEME) ?: 'http';
    $headers = [
        'User-Agent: IPTVSmartersPlayer',
        'Accept: */*',
        'Accept-Encoding: identity',                       // disable gzip — we relay raw
        'Referer: ' . $upstream_scheme . '://' . $upstream_host . '/',
        'Connection: close', // shared hosts: keep-alive can starve workers
        // Suppress proxy-identifying headers if any upstream (Apache
        // mod_proxy / Cloudflare) injected them. A header line with
        // empty value tells cURL to NOT send the header at all.
        'X-Forwarded-For:',
        'X-Forwarded-Host:',
        'X-Forwarded-Proto:',
        'X-Real-IP:',
        'Via:',
        'Forwarded:',
    ];
    // Allow per-call overrides (e.g., Range, or a different UA).
    // Later entries win because cURL honours the last occurrence of
    // a header name in CURLOPT_HTTPHEADER.
    foreach ($extra_headers as $k => $v) $headers[] = "$k: $v";
    curl_setopt_array($ch, [
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => $headers,
        // SSL: only meaningful for https:// upstreams. Relaxed because
        // shared cPanel hosts often have outdated CA bundles, AND we
        // are not protecting any subscriber data here — the proxied
        // stream is the same public bytes the client would otherwise
        // fetch directly. Privacy is provided by the HMAC token, not
        // by upstream TLS verification.
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0,
        // Force HTTP/1.1 — some IPTV servers (and CDNs in front of
        // them) drop HTTP/2 frames on non-standard ports.
        CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
        // Avoid SIGALRM on shared PHP-FPM hosts.
        CURLOPT_NOSIGNAL       => 1,
        CURLOPT_TCP_NODELAY    => 1,
        // Surface the verbose log at the right place when admin
        // turns it on via the env var (off by default — verbose
        // logging on every segment would flood error_log).
        CURLOPT_VERBOSE        => (bool)getenv('NDP_PROXY_VERBOSE'),
    ]);
    return $ch;
}

/**
 * Centralised upstream-error logger.
 *
 * Writes a single line to api/error_log so the admin can grep the
 * exact reason every black screen happened. Also stashes the most
 * recent error in APCu / file so /api/proxy/stats can return it
 * without grepping logs.
 *
 * @param string  $where    e.g. "live-playlist", "segment", "vod".
 * @param string  $url      upstream URL we tried.
 * @param int     $errno    cURL error number (CURLE_*).
 * @param string  $errmsg   cURL textual error.
 * @param int     $http     HTTP status returned (0 if connection failed).
 */
function _ndp_log_upstream_error(string $where, string $url, int $errno, string $errmsg, int $http = 0): void {
    $ts   = date('Y-m-d H:i:s');
    $line = "[$ts] [proxy:$where] curl_errno=$errno  http=$http  url=$url  err=\"$errmsg\"";
    // 1) per-API error log (lives next to api/index.php so cPanel users
    //    can find it via File Manager → "view error_log"). Falls back
    //    to error_log() if the directory isn't writable.
    $log_path = __DIR__ . '/../error_log';
    if (@file_put_contents($log_path, $line . "\n", FILE_APPEND | LOCK_EX) === false) {
        @error_log($line);
    }
    // 2) Stash latest error for /api/proxy/stats.
    $entry = ['where' => $where, 'errno' => $errno, 'http' => $http,
              'msg' => $errmsg, 'url' => $url, 'ts' => $ts];
    try {
        if (function_exists('apcu_store') && function_exists('apcu_enabled') && @apcu_enabled()) {
            @apcu_store('ndpseg-stat:last_upstream_error', $entry, 600);
        } else {
            $tmpdir = sys_get_temp_dir();
            @file_put_contents($tmpdir . '/ndpseg_last_error.json', json_encode($entry));
        }
    } catch (Throwable $e) { /* swallow */ }
}

/**
 * Read the last upstream error stashed by _ndp_log_upstream_error.
 * Used by /api/proxy/stats to surface in the admin widget.
 */
function _ndp_last_upstream_error(): ?array {
    try {
        if (function_exists('apcu_fetch') && function_exists('apcu_enabled') && @apcu_enabled()) {
            $v = @apcu_fetch('ndpseg-stat:last_upstream_error', $ok);
            if ($ok && is_array($v)) return $v;
        }
        $tmp = sys_get_temp_dir() . '/ndpseg_last_error.json';
        if (is_file($tmp)) {
            $j = @json_decode((string)@file_get_contents($tmp), true);
            if (is_array($j)) return $j;
        }
    } catch (Throwable $e) { /* swallow */ }
    return null;
}

// ───────────────────────────────────────────────────────────────────
//  1. POST /api/proxy/sign
// ───────────────────────────────────────────────────────────────────
function route_proxy_sign(): void {
    $user = require_auth();   // any authenticated subscriber
    $body = json_decode(file_get_contents('php://input') ?: '[]', true) ?: [];

    // Always-on telemetry: even if the body is malformed we still
    // know SOMETHING pinged us. This is how an admin confirms that
    // their freshly-built APK is actually talking to the proxy.
    _ndp_seg_counter_inc('sign_requests', 1);

    $pid  = (string)($body['profile_id'] ?? '');
    $kind = (string)($body['kind'] ?? '');
    $sid  = (string)($body['stream_id'] ?? '');
    if ($pid === '' || $kind === '' || $sid === '') {
        json_error(400, 'profile_id, kind and stream_id are required');
    }

    // Strict shared-only check happens here AND on every subsequent
    // segment fetch — defence in depth against forged tokens.
    $sp = _ndp_proxy_resolve_shared($pid);

    if ($kind === 'live') {
        $upstream      = _ndp_build_upstream_live($sp, $sid);
        $upstream_base = substr($upstream, 0, strrpos($upstream, '/') ?: strlen($upstream));
        $token = ndp_proxy_sign_token([
            'v' => 1, 'k' => 'live', 'pid' => $pid, 'sid' => $sid, 'ub' => $upstream_base,
        ]);
        send_json([
            'proxy_url'   => "/api/proxy/live/$token.m3u8",
            'expires_in'  => NDP_PROXY_TOKEN_TTL,
        ]);
        return;
    }
    if ($kind === 'vod') {
        $stype = (string)($body['stream_type'] ?? '');
        if ($stype !== 'movie' && $stype !== 'series') {
            json_error(400, "stream_type must be 'movie' or 'series'");
        }
        $ext = strtolower(trim((string)($body['ext'] ?? 'mp4'), "."));
        if ($ext === '') $ext = 'mp4';
        $token = ndp_proxy_sign_token([
            'v' => 1, 'k' => 'vod', 'pid' => $pid, 'sid' => $sid,
            'st' => $stype, 'ext' => $ext,
        ]);
        send_json([
            'proxy_url'  => "/api/proxy/vod/$token.$ext",
            'expires_in' => NDP_PROXY_TOKEN_TTL,
        ]);
        return;
    }
    json_error(400, "kind must be 'live' or 'vod'");
}

// ───────────────────────────────────────────────────────────────────
//  2. GET /api/proxy/live/{token}.m3u8     (server-side relay)
// ───────────────────────────────────────────────────────────────────
// HYBRID STRATEGY
// ───────────────
//   • Live TV  → relayed through this server with a spoofed
//                User-Agent (`IPTVSmartersPlayer` set in
//                _ndp_curl_open). Reason: ExoPlayer / Capacitor
//                WebView sends its own UA when following a 302, and
//                most IPTV providers 403/407 unknown UAs. Relaying
//                also lets us rewrite playlist URLs so segments come
//                back through us with the same spoof.
//   • VOD      → 302 redirect (movies/series are too big to pipe
//                through PHP-FPM; players follow 302 fine for MP4
//                because Range requests work natively).
//
// Bandwidth trade-off accepted: live streams (~2-5 Mbps each) flow
// through Hostinger. VOD doesn't.
function route_proxy_live_playlist(string $token): void {
    $payload = ndp_proxy_verify_token($token);
    if (!$payload || ($payload['k'] ?? '') !== 'live') {
        json_error(403, 'invalid or expired token');
    }
    // Make PHP transparent (see route_proxy_segment for the rationale).
    @ini_set('zlib.output_compression', '0');
    @ini_set('output_buffering', '0');
    if (function_exists('apache_setenv')) {
        @apache_setenv('no-gzip', '1');
        @apache_setenv('dont-vary', '1');
    }
    while (ob_get_level() > 0) ob_end_clean();

    $sp = _ndp_proxy_resolve_shared((string)($payload['pid'] ?? ''));
    $upstream = _ndp_build_upstream_live($sp, (string)($payload['sid'] ?? ''));
    $upstream_base = substr($upstream, 0, strrpos($upstream, '/') ?: strlen($upstream));

    // Fetch upstream playlist with cURL (UA = IPTVSmartersPlayer).
    $ch = _ndp_curl_open($upstream);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $body = curl_exec($ch);
    if ($body === false) {
        $errno  = curl_errno($ch);
        $errmsg = curl_error($ch) ?: 'unknown cURL error';
        curl_close($ch);
        _ndp_log_upstream_error('live-playlist', $upstream, $errno, $errmsg, 0);
        http_response_code(502);
        header('Content-Type: text/plain; charset=utf-8');
        header('x-proxy-kind: upstream-fetch-failed');
        echo "upstream playlist unavailable\ncurl_errno=$errno\nmessage=$errmsg\n";
        return;
    }
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ct   = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/octet-stream';
    curl_close($ch);
    if ($code < 200 || $code >= 300) {
        _ndp_log_upstream_error('live-playlist', $upstream, 0, "non-2xx status", (int)$code);
        http_response_code($code ?: 502);
        header('x-proxy-kind: upstream-error');
        header('Content-Type: text/plain; charset=utf-8');
        echo substr((string)$body, 0, 1024);
        return;
    }

    // Some Xtream servers redirect .m3u8 → raw MPEG-TS. Pass it
    // through as-is; players still play it via Content-Type.
    if (strpos(ltrim($body, "\xEF\xBB\xBF \t\r\n"), '#EXTM3U') !== 0) {
        http_response_code(200);
        header("Content-Type: $ct");
        header('x-proxy-kind: passthrough-not-m3u8');
        echo $body;
        return;
    }

    // Rewrite every URL line in the m3u8 to point back through us so
    // segment fetches inherit the User-Agent spoof.
    $rewritten = preg_replace_callback(
        '/^[^#\s].*$/m',
        function ($m) use ($upstream_base) {
            $url = trim($m[0]);
            if ($url === '') return $url;
            $abs = (strpos($url, '://') !== false)
                ? $url
                : rtrim($upstream_base, '/') . '/' . ltrim($url, '/');
            $seg_token = ndp_proxy_sign_token([
                'v' => 1, 'k' => 'seg', 'u' => $abs,
            ]);
            return "/api/proxy/seg/$seg_token";
        },
        $body
    );

    http_response_code(200);
    header('Content-Type: application/vnd.apple.mpegurl');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');
    header('Content-Encoding: identity');
    header('X-Accel-Buffering: no');
    header('x-proxy-kind: m3u8');
    echo $rewritten;
}

// ───────────────────────────────────────────────────────────────────
//  3. GET /api/proxy/seg/{token}            (true streaming relay)
// ───────────────────────────────────────────────────────────────────
// fpassthru-style relay: cURL writes each chunk straight to the
// client via CURLOPT_WRITEFUNCTION, so memory stays flat regardless
// of segment size. Spoofed UA via _ndp_curl_open() defeats upstream
// 403/407 blocks. We DO NOT cache to APCU here — caching would
// require buffering, which is exactly what we're avoiding for live.
function route_proxy_segment(string $token): void {
    $payload = ndp_proxy_verify_token($token);
    if (!$payload || ($payload['k'] ?? '') !== 'seg') {
        json_error(403, 'invalid or expired segment token');
    }
    $upstream = (string)($payload['u'] ?? '');
    if (!preg_match('#^https?://#i', $upstream)) {
        json_error(403, 'bad segment url');
    }

    // ── Make PHP truly transparent ─────────────────────────────────
    // Hostinger's PHP-FPM enables zlib output_compression, mod_deflate
    // and per-request output buffering by default — all three corrupt
    // a binary MPEG-TS / fMP4 relay (player sees Content-Length lying,
    // wrong MIME, or chunked-encoding mismatch → ERROR_CODE_IO_BAD_HTTP_STATUS).
    @ini_set('zlib.output_compression', '0');
    @ini_set('output_buffering', '0');
    @ini_set('implicit_flush', '1');
    if (function_exists('apache_setenv')) {
        @apache_setenv('no-gzip', '1');
        @apache_setenv('dont-vary', '1');
    }
    while (ob_get_level() > 0) ob_end_clean();

    // ── Forward Range + caller's User-Agent if it's a real player ──
    // We always default to IPTVSmartersPlayer, but if the device sent
    // its own UA we forward that — some upstreams whitelist specific
    // UAs (VLC, ExoPlayer, etc.) and reject everything else.
    $extra = [];
    if (isset($_SERVER['HTTP_RANGE']) && $_SERVER['HTTP_RANGE'] !== '') {
        $extra['Range'] = $_SERVER['HTTP_RANGE'];
    }
    $client_ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if ($client_ua !== '' && stripos($client_ua, 'mozilla') === false) {
        // Forward only "real player" UAs; never forward browser UAs
        // (those trigger upstream HTML login pages).
        $extra['User-Agent'] = $client_ua;
    }
    $ch = _ndp_curl_open($upstream, $extra);

    // ── Mirror upstream status + safe headers; stream chunk-by-chunk
    // We forward 401/403/404/5xx EXACTLY so ExoPlayer's auto-retry can
    // make the right decision instead of seeing a generic 500.
    $safe_keys = ['content-type','content-length','content-range','accept-ranges'];
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $hdr) use ($safe_keys) {
        $len = strlen($hdr);
        $line = trim($hdr);
        if ($line === '') return $len;
        if (preg_match('#^HTTP/\S+\s+(\d+)#', $line, $m)) {
            http_response_code((int)$m[1]);
            return $len;
        }
        $colon = strpos($line, ':');
        if ($colon === false) return $len;
        $name = strtolower(trim(substr($line, 0, $colon)));
        $val  = trim(substr($line, $colon + 1));
        // Drop upstream Cache-Control / Set-Cookie / Content-Encoding
        // — we control caching ourselves, and Content-Encoding would
        // lie to the client (Apache may double-compress otherwise).
        if (in_array($name, $safe_keys, true)) header(ucfirst($name) . ': ' . $val, true);
        return $len;
    });
    curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $chunk) {
        echo $chunk;
        if (function_exists('flush')) @flush();
        return strlen($chunk);
    });

    // ── No-cache for live; tell every hop (CDN, browser) "do not store"
    header('x-proxy-kind: seg-relay');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');
    header('Content-Encoding: identity');
    header('X-Accel-Buffering: no'); // disable nginx-style proxy buffering if any

    if (!curl_exec($ch)) {
        $errno  = curl_errno($ch);
        $errmsg = curl_error($ch) ?: 'unknown cURL error';
        _ndp_log_upstream_error('segment', $upstream, $errno, $errmsg, 0);
        // Headers may already be flushed; can't change status reliably here.
    }
    curl_close($ch);
}

// ───────────────────────────────────────────────────────────────────
//  4. GET /api/proxy/vod/{filename}
// ───────────────────────────────────────────────────────────────────
function route_proxy_vod(string $filename): void {
    $dot = strrpos($filename, '.');
    if ($dot === false) json_error(400, 'bad vod filename');
    $token = substr($filename, 0, $dot);
    $payload = ndp_proxy_verify_token($token);
    if (!$payload || ($payload['k'] ?? '') !== 'vod') {
        json_error(403, 'invalid or expired token');
    }
    $sp = _ndp_proxy_resolve_shared((string)($payload['pid'] ?? ''));
    $upstream = _ndp_build_upstream_vod(
        $sp,
        (string)($payload['st'] ?? 'movie'),
        (string)($payload['sid'] ?? ''),
        (string)($payload['ext'] ?? 'mp4')
    );

    // ── VOD bypass: 302 redirect straight to upstream ───────────────
    // Hostinger's PHP-FPM workers can't reliably pipe multi-GB movie
    // files; long-running streams hit fastcgi_read_timeout / output
    // buffer caps and the client sees "bytes missing" / truncated
    // playback. For VOD (movies & series) we instead hand the player
    // the upstream URL directly via a 302 — the device fetches the
    // file from the provider and Range/seek work natively. We keep
    // the HMAC token gate (verified above) so the redirect URL is
    // still subscriber-authenticated.
    //
    // Live TV continues to use _ndp_curl_open() because (a) anti-407
    // header spoofing is required, (b) HLS playlists must be rewritten
    // so segments route back through /api/proxy/seg, and (c) live
    // bandwidth is bounded per-channel (low risk of FPM timeout).
    header('x-proxy-kind: vod-redirect');
    header('Cache-Control: private, max-age=60');
    header('Location: ' . $upstream, true, 302);
    return;

    // Legacy pass-through (kept below as dead code in case a host
    // explicitly disables redirects — flip the early-return above to
    // re-enable). Not reached at runtime.
    $extra = [];
    if (isset($_SERVER['HTTP_RANGE']) && $_SERVER['HTTP_RANGE'] !== '') {
        $extra['Range'] = $_SERVER['HTTP_RANGE'];
    }
    $ch = _ndp_curl_open($upstream, $extra);

    // We want the body streamed straight to the client (no big PHP
    // memory hit on multi-GB movies). Use a write-callback that
    // echoes each chunk and a header-callback that mirrors the safe
    // headers from the upstream response.
    $headers_sent = false;
    $safe_keys = ['content-type','content-length','content-range','accept-ranges','etag','last-modified'];
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $hdr) use (&$headers_sent, $safe_keys) {
        $len = strlen($hdr);
        $line = trim($hdr);
        if ($line === '') return $len;
        if (preg_match('#^HTTP/\S+\s+(\d+)#', $line, $m)) {
            $code = (int)$m[1];
            if ($code > 0) http_response_code($code);
            return $len;
        }
        $colon = strpos($line, ':');
        if ($colon === false) return $len;
        $name = strtolower(trim(substr($line, 0, $colon)));
        $val  = trim(substr($line, $colon + 1));
        if (in_array($name, $safe_keys, true)) header(ucfirst($name) . ': ' . $val, true);
        if ($name === 'content-type' || $name === 'content-length') $headers_sent = true;
        return $len;
    });
    curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $chunk) {
        echo $chunk;
        // Push immediately (avoid PHP output buffering).
        if (function_exists('flush')) @flush();
        return strlen($chunk);
    });
    // Disable PHP output buffering so the stream is truly progressive.
    while (ob_get_level() > 0) ob_end_clean();
    header('x-proxy-kind: vod-passthrough');
    header('Cache-Control: public, max-age=300');
    if (!curl_exec($ch)) {
        $errno  = curl_errno($ch);
        $errmsg = curl_error($ch) ?: 'unknown cURL error';
        _ndp_log_upstream_error('vod', $upstream, $errno, $errmsg, 0);
        if (!$headers_sent) json_error(502, "vod unavailable: curl_errno=$errno $errmsg");
    }
    curl_close($ch);
}

// ───────────────────────────────────────────────────────────────────
//  5. GET /api/proxy/stats — admin only
// ───────────────────────────────────────────────────────────────────
function route_proxy_stats(): void {
    require_admin();
    $stats = ndp_seg_stats();
    // Surface the most-recent upstream error (if any) so the admin
    // widget can show "WHY playback failed" in plain English without
    // forcing the user to grep error_log via SSH.
    $stats['last_upstream_error'] = _ndp_last_upstream_error();
    send_json($stats);
}
