<?php
namespace EasyChat\Library\Http;

class ExternalHttpClient {
    private $registry;
    private $trust_manager;
    private $executor;

    public function __construct($registry = null, ?TlsTrustManager $trust_manager = null, ?callable $executor = null) {
        $this->registry = $registry;
        $this->trust_manager = $trust_manager ?: new TlsTrustManager($registry);
        $this->executor = $executor;
    }

    public function request($method, $url, array $request = []) {
        if (!function_exists('curl_init') && $this->executor === null) {
            return $this->decorateResult([
                'raw' => false,
                'http_code' => 0,
                'curl_errno' => 0,
                'curl_error' => 'curl_unavailable',
                'content_type' => '',
            ], $url, ['bundle' => TlsTrustManager::BUNDLE_SYSTEM, 'cainfo' => '', 'from_cache' => false], false, false);
        }

        $method = strtoupper(trim((string)$method));
        if ($method === '') {
            $method = 'GET';
        }

        $allow_fallback = array_key_exists('allow_tls_fallback', $request) ? (bool)$request['allow_tls_fallback'] : true;
        $profile = (string)($request['trust_profile'] ?? '');
        $base_options = $this->buildCurlOptions($method, $url, $request);
        return $this->executePrepared($url, $base_options, $profile, $allow_fallback);
    }

    public function requestCurlOptions(array $curl_options, $profile = '', $allow_fallback = true) {
        if (!function_exists('curl_init') && $this->executor === null) {
            return $this->decorateResult([
                'raw' => false,
                'http_code' => 0,
                'curl_errno' => 0,
                'curl_error' => 'curl_unavailable',
                'content_type' => '',
            ], '', ['bundle' => TlsTrustManager::BUNDLE_SYSTEM, 'cainfo' => '', 'from_cache' => false], false, false);
        }

        $url = isset($curl_options[CURLOPT_URL]) ? (string)$curl_options[CURLOPT_URL] : '';
        return $this->executePrepared($url, $curl_options, (string)$profile, (bool)$allow_fallback);
    }

    public function getTrustManager() {
        return $this->trust_manager;
    }

    private function executePrepared($url, array $base_options, $profile, $allow_fallback) {
        $initial_trust = $this->trust_manager->initialTrustForUrl($url, $profile);
        $result = $this->run($this->trust_manager->applyCurlOptions($base_options, $initial_trust));
        $decorated = $this->decorateResult($result, $url, $initial_trust, false, false);

        if ($this->transportSucceeded($result)) {
            $this->trust_manager->recordSuccess($url, $initial_trust, 'initial_success', $decorated);
            return $decorated;
        }

        if (!$allow_fallback || !$this->trust_manager->isTlsVerificationError($result)) {
            return $decorated;
        }

        $fallback_trust = $this->trust_manager->fallbackTrustForUrl($url, $profile, true);
        if (!$fallback_trust || ($fallback_trust['bundle'] ?? '') === ($initial_trust['bundle'] ?? '')) {
            return $decorated;
        }

        $fallback_result = $this->run($this->trust_manager->applyCurlOptions($base_options, $fallback_trust));
        $fallback_decorated = $this->decorateResult($fallback_result, $url, $fallback_trust, true, true);
        $fallback_decorated['first_curl_errno'] = (int)($result['curl_errno'] ?? 0);
        $fallback_decorated['first_curl_error'] = (string)($result['curl_error'] ?? '');

        if ($this->transportSucceeded($fallback_result)) {
            $this->trust_manager->recordSuccess($url, $fallback_trust, 'tls_verification_fallback', $fallback_decorated);
        }

        return $fallback_decorated;
    }

    private function buildCurlOptions($method, $url, array $request) {
        if (!array_key_exists('proxy', $request) && $this->registry) {
            $proxy_profile = isset($request['proxy_profile'])
                ? (string)$request['proxy_profile']
                : ProxyPolicy::profileForTrustProfile((string)($request['trust_profile'] ?? ''));
            if ($proxy_profile !== '') {
                $proxy = (new ProxyPolicy($this->registry))->proxyForProfile($proxy_profile);
                if ($proxy !== null) {
                    $request['proxy'] = $proxy;
                }
            }
        }

        $headers = isset($request['headers']) && is_array($request['headers']) ? $request['headers'] : [];
        $options = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => array_key_exists('return_transfer', $request) ? (bool)$request['return_transfer'] : true,
            CURLOPT_HEADER => false,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_CONNECTTIMEOUT => (int)($request['connect_timeout'] ?? 4),
            CURLOPT_TIMEOUT => (int)($request['timeout'] ?? 12),
            CURLOPT_NOSIGNAL => 1,
            CURLOPT_FOLLOWLOCATION => !empty($request['follow_location']),
            CURLOPT_MAXREDIRS => (int)($request['max_redirects'] ?? 3),
        ];

        if (defined('CURLOPT_PROTOCOLS')) {
            $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
        }
        if (defined('CURLOPT_REDIR_PROTOCOLS')) {
            $options[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
        }

        if ($method === 'HEAD') {
            $options[CURLOPT_NOBODY] = true;
        } elseif ($method === 'POST') {
            $options[CURLOPT_POST] = true;
        } elseif ($method !== 'GET') {
            $options[CURLOPT_CUSTOMREQUEST] = $method;
        }

        if (array_key_exists('post_fields', $request)) {
            $options[CURLOPT_POSTFIELDS] = $request['post_fields'];
        } elseif (array_key_exists('body', $request) && $request['body'] !== null) {
            $options[CURLOPT_POSTFIELDS] = $request['body'];
        }

        if (isset($request['write_function']) && is_callable($request['write_function'])) {
            $options[CURLOPT_WRITEFUNCTION] = $request['write_function'];
            $options[CURLOPT_RETURNTRANSFER] = false;
        }

        if (!empty($request['proxy']) && is_array($request['proxy'])) {
            $proxy = $request['proxy'];
            if (!empty($proxy['ip']) && !empty($proxy['port'])) {
                $proxy_ip = strpos((string)$proxy['ip'], ':') !== false ? '[' . $proxy['ip'] . ']' : $proxy['ip'];
                $options[CURLOPT_PROXY] = $proxy_ip;
                $options[CURLOPT_PROXYPORT] = (int)$proxy['port'];
                if (!empty($proxy['login']) && !empty($proxy['password'])) {
                    $options[CURLOPT_PROXYUSERPWD] = $proxy['login'] . ':' . $proxy['password'];
                }
            }
        }

        if (!empty($request['curl_options']) && is_array($request['curl_options'])) {
            foreach ($request['curl_options'] as $key => $value) {
                $options[$key] = $value;
            }
        }

        return $options;
    }

    private function run(array $curl_options) {
        if ($this->executor !== null) {
            $result = call_user_func($this->executor, $curl_options);
            return is_array($result) ? $result : [
                'raw' => false,
                'http_code' => 0,
                'curl_errno' => 0,
                'curl_error' => 'invalid_executor_result',
                'content_type' => '',
            ];
        }

        $ch = curl_init();
        curl_setopt_array($ch, $curl_options);
        $raw = curl_exec($ch);
        $result = [
            'raw' => $raw,
            'http_code' => (int)curl_getinfo($ch, CURLINFO_HTTP_CODE),
            'curl_errno' => (int)curl_errno($ch),
            'curl_error' => (string)curl_error($ch),
            'content_type' => (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE),
        ];
        if (PHP_VERSION_ID < 80500) {
            curl_close($ch);
        }
        return $result;
    }

    private function decorateResult(array $result, $url, array $trust_info, $fallback_attempted, $fallback_used) {
        $result['url'] = $url;
        $result['transport_ok'] = $this->transportSucceeded($result);
        $result['trust_bundle'] = (string)($trust_info['bundle'] ?? TlsTrustManager::BUNDLE_SYSTEM);
        $result['trust_cainfo'] = (string)($trust_info['cainfo'] ?? '');
        $result['trust_from_cache'] = !empty($trust_info['from_cache']);
        $result['tls_fallback_attempted'] = (bool)$fallback_attempted;
        $result['tls_fallback_used'] = (bool)$fallback_used && $this->transportSucceeded($result);
        return $result;
    }

    private function transportSucceeded(array $result) {
        return array_key_exists('raw', $result) && $result['raw'] !== false && (int)($result['curl_errno'] ?? 0) === 0;
    }
}
