<?php
namespace EasyChat\Library\Http;

use EasyChat\Library\SafeRuntime;








class PublicUrlGuard {

    private $resolver;
    private $curl_version_number;

    public function __construct(?callable $resolver = null, ?int $curl_version_number = null) {
        $this->resolver = $resolver;
        $this->curl_version_number = $curl_version_number;
    }

    public function prepare(string $url, array $allowed_hosts = []): array {
        $url = trim($url);
        if ($url === '' || preg_match('/[\x00-\x20\x7F\\\\]/u', $url)) {
            return $this->denied('remote_url_invalid');
        }

        $parts = parse_url($url);
        if (!is_array($parts)
            || strtolower((string)($parts['scheme'] ?? '')) !== 'https'
            || isset($parts['user'])
            || isset($parts['pass'])
            || (isset($parts['port']) && (int)$parts['port'] !== 443)
        ) {
            return $this->denied('remote_url_invalid');
        }

        $host = strtolower(trim((string)($parts['host'] ?? ''), '.'));
        if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
            return $this->denied('remote_url_invalid');
        }
        if ($host === '' || ($allowed_hosts && !$this->hostAllowed($host, $allowed_hosts))) {
            return $this->denied('remote_host_denied');
        }

        $ips = $this->resolvePublicIps($host);
        if (!$ips) {
            return $this->denied('remote_host_not_public');
        }

        $resolve = $this->curlResolveEntries($host, $ips);

        return [
            'success' => true,
            'url' => $url,
            'host' => $host,
            'ips' => $ips,
            'curl_options' => [
                CURLOPT_FOLLOWLOCATION => false,
                CURLOPT_MAXREDIRS => 0,
                CURLOPT_RESOLVE => $resolve,
            ],
        ];
    }

    










    private function curlResolveEntries(string $host, array $ips): array {
        $encoded = [];
        foreach ($ips as $ip) {
            $encoded[] = strpos($ip, ':') !== false ? '[' . $ip . ']' : $ip;
        }

        if (count($encoded) > 1 && $this->supportsMultiAddressResolve()) {
            return [$host . ':443:' . implode(',', $encoded)];
        }

        foreach ($encoded as $ip) {
            if (strpos($ip, '[') !== 0) {
                return [$host . ':443:' . $ip];
            }
        }

        return $encoded ? [$host . ':443:' . $encoded[0]] : [];
    }

    private function supportsMultiAddressResolve(): bool {
        $version = $this->curl_version_number;
        if ($version === null && function_exists('curl_version')) {
            $info = curl_version();
            $version = is_array($info) ? (int)($info['version_number'] ?? 0) : 0;
        }

         
        return (int)$version >= 0x073B00;
    }

    private function resolvePublicIps(string $host): array {
        if ($this->resolver) {
            $ips = call_user_func($this->resolver, $host);
            $ips = is_array($ips) ? $ips : [];
        } elseif (filter_var($host, FILTER_VALIDATE_IP) !== false) {
            $ips = [$host];
        } else {
            $ips = function_exists('gethostbynamel')
                ? (array)SafeRuntime::call(function () use ($host) {
                    return gethostbynamel($host) ?: [];
                }, [])
                : [];
            if (function_exists('dns_get_record') && defined('DNS_AAAA')) {
                $aaaa_records = SafeRuntime::call(function () use ($host) {
                    return dns_get_record($host, DNS_AAAA);
                }, []);
                foreach ((array)$aaaa_records as $row) {
                    if (!empty($row['ipv6'])) {
                        $ips[] = $row['ipv6'];
                    }
                }
            }
        }

        $public = [];
        foreach (array_unique($ips) as $ip) {
            if (!$this->isPublicIp((string)$ip)) {
                 
                 
                return [];
            }
            $public[] = $ip;
        }
        return $public;
    }

    private function isPublicIp(string $ip): bool {
        if (filter_var($ip, FILTER_VALIDATE_IP) === false
            || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
        ) {
            return false;
        }

        $packed = inet_pton($ip);
        if (!is_string($packed)) {
            return false;
        }

         
         
        if (strlen($packed) === 16 && substr($packed, 0, 12) === str_repeat("\0", 10) . "\xff\xff") {
            return false;
        }

        return true;
    }

    private function hostAllowed(string $host, array $allowed_hosts): bool {
        foreach ($allowed_hosts as $allowed) {
            $allowed = strtolower(trim((string)$allowed, '. '));
            if ($allowed !== '' && ($host === $allowed || substr($host, -strlen('.' . $allowed)) === '.' . $allowed)) {
                return true;
            }
        }
        return false;
    }

    private function denied(string $error): array {
        return ['success' => false, 'error' => $error, 'curl_options' => []];
    }
}
