<?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_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',
        ];
    }
}
