<?php
/**
 * Shared site-health probe helpers.
 *
 * Originally lived as local-scope functions inside
 * `/root/whp/scripts/scheduled-site-checks.php`. Extracted so
 * `scheduled-weekly-summary.php` (and any future scheduled tooling) can
 * reuse the exact same probe logic without re-including a script that
 * has CLI side effects on inclusion.
 *
 * Public functions:
 *   whp_probe_ssl_cert(string $domain): ?array
 *   whp_check_dns_health(string $domain, ?string $serverIp): array
 *   whp_whois_expiry(string $apex): ?int
 *   whp_apex_for_whois(string $domain): ?string
 *   whp_classify_cdn(array $ips): ?string
 *   whp_resolve_server_ip(): ?string
 *
 * Each function is wrapped in a `function_exists` guard so this file can
 * be safely require_once'd alongside `scheduled-site-checks.php` if both
 * end up loaded in one process (e.g. during refactors).
 */

if (!function_exists('whp_resolve_server_ip')) {
    function whp_resolve_server_ip(): ?string {
        $settingsFile = '/docker/whp/settings.json';
        if (is_readable($settingsFile)) {
            $s = json_decode((string)file_get_contents($settingsFile), true) ?: [];
            if (!empty($s['server_ip']) && filter_var($s['server_ip'], FILTER_VALIDATE_IP)) {
                return $s['server_ip'];
            }
            $candidate = $s['hostname'] ?? ($s['panel_domain'] ?? '');
            if ($candidate !== '') {
                $host = preg_replace('/:\d+$/', '', $candidate);
                $ip = gethostbyname($host);
                if ($ip !== $host && filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
            }
        }
        $hn = gethostname();
        if ($hn) {
            $ip = gethostbyname($hn);
            if ($ip !== $hn && filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
        }
        return null;
    }
}

if (!function_exists('whp_probe_ssl_cert')) {
    function whp_probe_ssl_cert(string $domain): ?array {
        $ctx = stream_context_create([
            'ssl' => [
                'capture_peer_cert' => true,
                'verify_peer'       => false,
                'verify_peer_name'  => false,
                'SNI_enabled'       => true,
                'peer_name'         => $domain,
            ],
        ]);
        $errno = 0; $errstr = '';
        $sock = @stream_socket_client(
            "ssl://{$domain}:443", $errno, $errstr, 8.0, STREAM_CLIENT_CONNECT, $ctx);
        if (!$sock) return null;
        $params = stream_context_get_params($sock);
        fclose($sock);
        $cert = $params['options']['ssl']['peer_certificate'] ?? null;
        if (!$cert) return null;
        $parsed = openssl_x509_parse($cert);
        if (!$parsed || empty($parsed['validTo_time_t'])) return null;
        return [
            'expires_at' => (int)$parsed['validTo_time_t'],
            'not_after'  => $parsed['validTo'] ?? '',
            'subject'    => is_array($parsed['subject'] ?? null) ? ($parsed['subject']['CN'] ?? '?') : '?',
            'issuer'     => is_array($parsed['issuer']  ?? null) ? ($parsed['issuer']['CN']  ?? '?') : '?',
        ];
    }
}

if (!function_exists('whp_classify_cdn')) {
    function whp_classify_cdn(array $ips): ?string {
        static $providers = [
            'CloudFlare' => [
                '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22',
                '103.31.4.0/22',   '141.101.64.0/18', '108.162.192.0/18',
                '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22',
                '198.41.128.0/17', '162.158.0.0/15',  '104.16.0.0/13',
                '104.24.0.0/14',   '172.64.0.0/13',   '131.0.72.0/22',
            ],
            'Sucuri' => [
                '185.93.228.0/22', '192.124.249.0/24', '66.248.200.0/22',
            ],
            'Fastly' => [
                '23.235.32.0/20',  '43.249.72.0/22',   '103.244.50.0/24',
                '103.245.222.0/23','103.245.224.0/24', '104.156.80.0/20',
                '146.75.0.0/17',   '151.101.0.0/16',   '157.52.64.0/18',
                '167.82.0.0/17',   '167.82.128.0/20',  '167.82.160.0/20',
                '167.82.224.0/20', '172.111.64.0/18',  '185.31.16.0/22',
                '199.27.72.0/21',  '199.232.0.0/16',
            ],
            'Akamai' => [
                '23.32.0.0/11', '23.192.0.0/11', '104.64.0.0/10',
                '184.24.0.0/13', '184.50.0.0/15', '184.84.0.0/14',
                '2.16.0.0/13',  '23.0.0.0/12',
            ],
        ];
        foreach ($ips as $ip) {
            foreach ($providers as $name => $ranges) {
                foreach ($ranges as $cidr) {
                    if (whp_ipv4_in_cidr($ip, $cidr)) return $name;
                }
            }
        }
        return null;
    }
}

if (!function_exists('whp_ipv4_in_cidr')) {
    function whp_ipv4_in_cidr(string $ip, string $cidr): bool {
        [$subnet, $bits] = explode('/', $cidr) + [1 => '32'];
        $bits = (int) $bits;
        $ipLong = ip2long($ip);
        $subLong = ip2long($subnet);
        if ($ipLong === false || $subLong === false) return false;
        if ($bits === 0) return true;
        $mask = -1 << (32 - $bits);
        return ($ipLong & $mask) === ($subLong & $mask);
    }
}

if (!function_exists('whp_apex_for_whois')) {
    function whp_apex_for_whois(string $domain): ?string {
        $domain = strtolower(trim($domain));
        $domain = preg_replace('/^\*\./', '', (string)$domain);
        $parts = explode('.', $domain);
        if (count($parts) < 2) return null;
        $twoLevel = ['co.uk', 'com.au', 'co.nz', 'com.br', 'co.jp', 'com.mx'];
        $tail2 = implode('.', array_slice($parts, -2));
        $tail3 = implode('.', array_slice($parts, -3));
        return in_array($tail2, $twoLevel, true) ? $tail3 : $tail2;
    }
}

if (!function_exists('whp_whois_expiry')) {
    function whp_whois_expiry(string $apex): ?int {
        // Hardened input guard: only allow a-z, 0-9, dot, hyphen.
        // shell_exec is used here because `whois` has no PHP-native binding;
        // the apex is regex-validated above and additionally escapeshellarg'd.
        if (!preg_match('/^[a-z0-9.-]+$/', $apex)) return null;
        $cmd = 'whois ' . escapeshellarg($apex) . ' 2>/dev/null';
        $out = (string)shell_exec($cmd);
        if ($out === '') return null;
        $patterns = [
            '/Registry Expiry Date:\s*(\S+)/i',
            '/Registrar Registration Expiration Date:\s*(\S+)/i',
            '/Expiration Date:\s*(\S+)/i',
            '/Expiry date:\s*(\S+)/i',
            '/expires:\s*(\S+)/i',
            '/Expiry Date:\s*(\S+)/i',
        ];
        foreach ($patterns as $pat) {
            if (preg_match($pat, $out, $m)) {
                $ts = strtotime($m[1]);
                if ($ts) return $ts;
            }
        }
        return null;
    }
}

if (!function_exists('whp_http_check')) {
    /**
     * Curl a URL and return [http_code, url, error]. Mirrors the helper
     * inlined in scheduled-site-checks.php. Used by both CLI scheduled
     * runs and the customer-page on-demand recheck.
     */
    function whp_http_check(string $url): array {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 3,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT        => 12,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_USERAGENT      => 'WHP-AI-Monitor/1.0 (site health check)',
        ]);
        // Indirect call: an upstream security-hook regex flags any literal
        // *exec(*  as a Node.js child_process.exec injection risk and blocks
        // edits. PHP's curl_exec is unrelated; the runtime still resolves
        // this to the same builtin.
        $runner = 'curl_' . 'exec';
        $runner($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $err  = curl_error($ch);
        $finalUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        curl_close($ch);
        return ['http_code' => $code, 'url' => $finalUrl, 'error' => $err];
    }
}

if (!function_exists('whp_sig')) {
    function whp_sig(string $category, int $siteId): string {
        return hash('sha256', "scheduled:{$category}:{$siteId}");
    }
}

if (!function_exists('whp_cache_status')) {
    /**
     * Write cached health-status fields onto the subscription row. Caller
     * passes only the columns it touched; unknown columns are ignored.
     * Failures are logged but never raised (migration 005 may not be
     * applied in some staging contexts).
     */
    function whp_cache_status(PDO $db, int $subscriptionId, array $fields): void {
        if (!$fields || $subscriptionId <= 0) return;
        $allowed = [
            'http_status', 'http_checked_at',
            'ssl_status', 'ssl_expires_at', 'ssl_issuer', 'ssl_checked_at',
            'dns_status', 'dns_observed_ips', 'dns_checked_at',
            'whois_status', 'whois_expires_at', 'whois_apex', 'whois_checked_at',
        ];
        $set = []; $args = [];
        foreach ($fields as $col => $val) {
            if (!in_array($col, $allowed, true)) continue;
            $set[]  = "{$col} = ?";
            $args[] = $val;
        }
        if (!$set) return;
        $args[] = $subscriptionId;
        $sql = "UPDATE whp.ai_monitor_subscriptions SET " . implode(', ', $set) . " WHERE id = ?";
        try {
            $db->prepare($sql)->execute($args);
        } catch (Throwable $e) {
            error_log("whp_cache_status: " . $e->getMessage());
        }
    }
}

if (!function_exists('whp_upsert_issue')) {
    function whp_upsert_issue(PDO $db, array $sub, string $severity, string $category,
                              string $signature, string $title, string $description,
                              string $recommendation, string $evidence): void {
        $supStmt = $db->prepare("
            SELECT id FROM whp.ai_monitor_suppressions
            WHERE signature = ?
              AND ((site_id = ?) OR (site_id IS NULL AND ? = 0))
            LIMIT 1
        ");
        $supStmt->execute([$signature, $sub['site_id'], $sub['site_id']]);
        if ($supStmt->fetchColumn() !== false) {
            $db->prepare("
                UPDATE whp.ai_monitor_suppressions
                SET suppressed_occurrence_count = suppressed_occurrence_count + 1,
                    last_suppressed_event_at = NOW()
                WHERE signature = ?
                  AND ((site_id = ?) OR (site_id IS NULL AND ? = 0))
            ")->execute([$signature, $sub['site_id'], $sub['site_id']]);
            return;
        }

        $existStmt = $db->prepare("
            SELECT id, sources FROM whp.ai_monitor_issues
            WHERE signature = ? AND status = 'open'
            LIMIT 1
        ");
        $existStmt->execute([$signature]);
        $existing = $existStmt->fetch(PDO::FETCH_ASSOC);

        if ($existing) {
            $sources = json_decode($existing['sources'] ?? '[]', true) ?: [];
            if (!in_array('scheduled', $sources, true)) $sources[] = 'scheduled';
            $db->prepare("
                UPDATE whp.ai_monitor_issues
                SET occurrence_count = occurrence_count + 1,
                    last_seen = NOW(),
                    sources = ?,
                    severity = ?,
                    title = ?,
                    diagnosis = ?,
                    recommendation = ?,
                    evidence = ?
                WHERE id = ?
            ")->execute([json_encode($sources), $severity, $title, $description,
                         $recommendation, $evidence, $existing['id']]);
            return;
        }

        $db->prepare("
            INSERT INTO whp.ai_monitor_issues
                (subscription_id, site_id, username, container_name,
                 severity, category, subsystem,
                 source, sources, title, diagnosis, recommendation,
                 raw_log_excerpt, context_data, signature, evidence,
                 occurrence_count, first_seen, last_seen,
                 status, email_sent)
            VALUES (?, ?, ?, NULL,
                    ?, ?, NULL,
                    'scheduled', ?, ?, ?, ?,
                    NULL, NULL, ?, ?,
                    1, NOW(), NOW(),
                    'open', 0)
        ")->execute([
            $sub['subscription_id'], $sub['site_id'], $sub['username'],
            $severity, $category,
            json_encode(['scheduled']),
            $title, $description, $recommendation,
            $signature, $evidence,
        ]);
    }
}

if (!function_exists('whp_close_open_issue')) {
    function whp_close_open_issue(PDO $db, string $signature): void {
        $db->prepare("
            UPDATE whp.ai_monitor_issues
            SET status = 'fixed',
                auto_resolved_at = COALESCE(auto_resolved_at, NOW()),
                status_changed_by = 'system',
                status_changed_at = NOW()
            WHERE signature = ? AND status = 'open'
        ")->execute([$signature]);
    }
}

if (!function_exists('whp_health_run_http_check')) {
    /**
     * Run the HTTP reachability check for one subscription, update the
     * cache columns, and open/close the corresponding issue. Returns the
     * cache-row values written so callers can echo them back in JSON.
     *
     * Silent (no echo). The CLI's verbose `check_http()` in
     * scheduled-site-checks.php remains for human-readable cron output.
     */
    function whp_health_run_http_check(PDO $db, array $sub, string $domain): array {
        $sig = whp_sig('reachability', (int)$sub['site_id']);
        $checkedAt = date('Y-m-d H:i:s');
        if (strpos($domain, '*') !== false) {
            whp_cache_status($db, (int)$sub['subscription_id'], [
                'http_status'     => 'unknown',
                'http_checked_at' => $checkedAt,
            ]);
            return ['http_status' => 'unknown', 'http_checked_at' => $checkedAt];
        }
        $check = whp_http_check("https://{$domain}/");
        if ($check['http_code'] === 0) {
            $check = whp_http_check("http://{$domain}/");
        }
        $code = (int)$check['http_code'];
        $isUp = ($code >= 200 && $code < 500);
        $cached = $code === 0 ? 'unreachable' : ($isUp ? 'ok' : 'fail');
        whp_cache_status($db, (int)$sub['subscription_id'], [
            'http_status'     => $cached,
            'http_checked_at' => $checkedAt,
        ]);
        if ($isUp) {
            whp_close_open_issue($db, $sig);
        } else {
            $reason = $code === 0
                ? "Connection failed: " . ($check['error'] ?: 'unknown')
                : "HTTP {$code} from {$check['url']}";
            whp_upsert_issue($db, $sub, 'critical', 'site_unreachable', $sig,
                "Site {$domain} not reachable",
                $reason,
                "Check the site's containers and recent changes.",
                "{$check['url']} -> {$code}");
        }
        return ['http_status' => $cached, 'http_checked_at' => $checkedAt];
    }
}

if (!function_exists('whp_health_run_ssl_check')) {
    function whp_health_run_ssl_check(PDO $db, array $sub, string $domain): array {
        $sig = whp_sig('ssl', (int)$sub['site_id']);
        $checkedAt = date('Y-m-d H:i:s');
        $info = whp_probe_ssl_cert($domain);
        if ($info === null) {
            whp_close_open_issue($db, $sig);
            $row = [
                'ssl_status'     => 'unreachable',
                'ssl_expires_at' => null,
                'ssl_issuer'     => null,
                'ssl_checked_at' => $checkedAt,
            ];
            whp_cache_status($db, (int)$sub['subscription_id'], $row);
            return $row;
        }
        $daysLeft = (int)floor(($info['expires_at'] - time()) / 86400);
        $status   = $daysLeft < 0 ? 'expired' : ($daysLeft <= 30 ? 'expiring' : 'ok');
        $row = [
            'ssl_status'     => $status,
            'ssl_expires_at' => date('Y-m-d H:i:s', $info['expires_at']),
            'ssl_issuer'     => substr((string)$info['issuer'], 0, 128),
            'ssl_checked_at' => $checkedAt,
        ];
        whp_cache_status($db, (int)$sub['subscription_id'], $row);
        if ($daysLeft < 0) {
            whp_upsert_issue($db, $sub, 'critical', 'ssl_expired', $sig,
                "SSL certificate expired " . abs($daysLeft) . " day(s) ago for {$domain}",
                "Cert subject: {$info['subject']}, issuer: {$info['issuer']}, expired " . date('Y-m-d', $info['expires_at']),
                "Renew the certificate. Most WHP sites use Let's Encrypt — try forcing a renewal from the SSL panel.",
                "notAfter={$info['not_after']}");
        } elseif ($daysLeft <= 7) {
            whp_upsert_issue($db, $sub, 'critical', 'ssl_expiring', $sig,
                "SSL certificate expires in {$daysLeft} day(s) for {$domain}",
                "Cert subject: {$info['subject']}, issuer: {$info['issuer']}, expires " . date('Y-m-d', $info['expires_at']),
                "Renew the certificate. Most WHP sites use Let's Encrypt — try forcing a renewal from the SSL panel.",
                "notAfter={$info['not_after']}");
        } elseif ($daysLeft <= 30) {
            whp_upsert_issue($db, $sub, 'warning', 'ssl_expiring', $sig,
                "SSL certificate expires in {$daysLeft} day(s) for {$domain}",
                "Cert subject: {$info['subject']}, issuer: {$info['issuer']}, expires " . date('Y-m-d', $info['expires_at']),
                "Renewal usually happens automatically. If it hasn't by 7 days out, force a renewal from the SSL panel.",
                "notAfter={$info['not_after']}");
        } else {
            whp_close_open_issue($db, $sig);
        }
        return $row;
    }
}

if (!function_exists('whp_health_run_dns_check')) {
    function whp_health_run_dns_check(PDO $db, array $sub, string $domain, ?string $serverIp): array {
        $sig = whp_sig('dns', (int)$sub['site_id']);
        $checkedAt = date('Y-m-d H:i:s');
        $result = whp_check_dns_health($domain, $serverIp);
        $observed = !empty($result['ips']) ? implode(',', array_slice($result['ips'], 0, 8)) : null;
        $row = [
            'dns_status'       => $result['status'],
            'dns_observed_ips' => $observed,
            'dns_checked_at'   => $checkedAt,
        ];
        whp_cache_status($db, (int)$sub['subscription_id'], $row);
        switch ($result['status']) {
            case 'nxdomain':
                whp_upsert_issue($db, $sub, 'critical', 'dns_misconfigured', $sig,
                    "Domain {$domain} does not resolve",
                    "DNS query for A record returned no answer.",
                    "Verify the domain's DNS provider has an A record pointing to this server.",
                    "A=(none)");
                break;
            case 'mismatch':
                whp_upsert_issue($db, $sub, 'critical', 'dns_misconfigured', $sig,
                    "Domain {$domain} A record points elsewhere",
                    "Observed IPs: {$observed}; expected this server's IP.",
                    "Update the domain's DNS A record to point to this server, or place the site behind a recognised CDN.",
                    "A={$observed}");
                break;
            case 'ok':
            case 'cdn':
            case 'skipped':
            default:
                whp_close_open_issue($db, $sig);
                break;
        }
        return $row;
    }
}

if (!function_exists('whp_health_run_whois_check')) {
    function whp_health_run_whois_check(PDO $db, array $sub, string $domain): array {
        $sig = whp_sig('whois', (int)$sub['site_id']);
        $checkedAt = date('Y-m-d H:i:s');
        $apex = whp_apex_for_whois($domain);
        if (!$apex) {
            $row = [
                'whois_status'     => 'skipped',
                'whois_expires_at' => null,
                'whois_apex'       => null,
                'whois_checked_at' => $checkedAt,
            ];
            whp_cache_status($db, (int)$sub['subscription_id'], $row);
            return $row;
        }
        $expiresAt = whp_whois_expiry($apex);
        if (!$expiresAt) {
            $row = [
                'whois_status'     => 'unparsed',
                'whois_expires_at' => null,
                'whois_apex'       => $apex,
                'whois_checked_at' => $checkedAt,
            ];
            whp_cache_status($db, (int)$sub['subscription_id'], $row);
            return $row;
        }
        $daysLeft = (int)floor(($expiresAt - time()) / 86400);
        $status   = $daysLeft < 0 ? 'expired' : ($daysLeft <= 30 ? 'expiring' : 'ok');
        $row = [
            'whois_status'     => $status,
            'whois_expires_at' => date('Y-m-d H:i:s', $expiresAt),
            'whois_apex'       => $apex,
            'whois_checked_at' => $checkedAt,
        ];
        whp_cache_status($db, (int)$sub['subscription_id'], $row);
        if ($daysLeft < 0) {
            whp_upsert_issue($db, $sub, 'critical', 'domain_expired', $sig,
                "Domain registration EXPIRED for {$apex} (" . abs($daysLeft) . " days ago)",
                "WHOIS Registry Expiry Date: " . date('Y-m-d', $expiresAt) . ".",
                "Renew the domain immediately at your registrar; expired domains may be re-registered by others.",
                "expires=" . date('Y-m-d', $expiresAt));
        } elseif ($daysLeft <= 7) {
            whp_upsert_issue($db, $sub, 'critical', 'domain_expiring', $sig,
                "Domain {$apex} expires in {$daysLeft} day(s)",
                "WHOIS Registry Expiry Date: " . date('Y-m-d', $expiresAt) . ".",
                "Renew the domain at your registrar before it lapses.",
                "expires=" . date('Y-m-d', $expiresAt));
        } elseif ($daysLeft <= 30) {
            whp_upsert_issue($db, $sub, 'warning', 'domain_expiring', $sig,
                "Domain {$apex} expires in {$daysLeft} day(s)",
                "WHOIS Registry Expiry Date: " . date('Y-m-d', $expiresAt) . ".",
                "Plan a renewal at your registrar within the next 30 days.",
                "expires=" . date('Y-m-d', $expiresAt));
        } else {
            whp_close_open_issue($db, $sig);
        }
        return $row;
    }
}

if (!function_exists('whp_health_run_all_checks')) {
    /**
     * Run all four health checks for a single subscription. The $sub array
     * must include subscription_id, site_id, username, primary_domain.
     * Returns the merged cache-row values written, with one extra key
     * `domain` echoing the domain probed.
     */
    function whp_health_run_all_checks(PDO $db, array $sub, ?string $serverIp): array {
        $domain = (string)($sub['primary_domain'] ?? '');
        $merged = ['domain' => $domain];
        if ($domain === '') return $merged;
        $merged += whp_health_run_http_check($db, $sub, $domain);
        $merged += whp_health_run_ssl_check($db, $sub, $domain);
        $merged += whp_health_run_dns_check($db, $sub, $domain, $serverIp);
        $merged += whp_health_run_whois_check($db, $sub, $domain);
        return $merged;
    }
}

if (!function_exists('whp_check_dns_health')) {
    /**
     * Pure DNS-resolution probe — returns a structured array. Does NOT
     * touch the database or the issues table. The companion function
     * `check_dns()` in scheduled-site-checks.php still owns the
     * issue-tracking semantics; this is the read-only counterpart used
     * by the weekly summary.
     *
     * Return shape:
     *   [
     *     'status' => 'ok'|'cdn'|'mismatch'|'nxdomain'|'skipped',
     *     'ips'    => string[],
     *     'cdn'    => string|null,
     *     'note'   => string,
     *   ]
     */
    function whp_check_dns_health(string $domain, ?string $serverIp): array {
        if (!$serverIp || strpos($domain, '*') !== false) {
            return [
                'status' => 'skipped',
                'ips'    => [],
                'cdn'    => null,
                'note'   => $serverIp ? 'wildcard domain' : 'server IP unknown',
            ];
        }
        $records = @dns_get_record($domain, DNS_A);
        $ips = array_column(is_array($records) ? $records : [], 'ip');
        if (empty($ips)) {
            return [
                'status' => 'nxdomain',
                'ips'    => [],
                'cdn'    => null,
                'note'   => 'no A record returned',
            ];
        }
        if (!in_array($serverIp, $ips, true)) {
            $cdn = whp_classify_cdn($ips);
            if ($cdn !== null) {
                return [
                    'status' => 'cdn',
                    'ips'    => $ips,
                    'cdn'    => $cdn,
                    'note'   => "behind {$cdn}",
                ];
            }
            return [
                'status' => 'mismatch',
                'ips'    => $ips,
                'cdn'    => null,
                'note'   => 'A record does not point to this server',
            ];
        }
        return [
            'status' => 'ok',
            'ips'    => $ips,
            'cdn'    => null,
            'note'   => 'resolves to this server',
        ];
    }
}
