<?php
namespace EasyChat\Library\Http;

use EasyChat\Library\SafeRuntime;
use EasyChat\Model\Setting;







class TlsTrustManager {
    const BUNDLE_SYSTEM = 'system-ca';
    const BUNDLE_RUSSIAN_COMBINED = 'russian-combined-ca';
    const DECISIONS_KEY = 'chat_tls_trust_decisions_json';
    const RUSSIAN_CA_META_KEY = 'chat_tls_russian_ca_meta_json';
    const RUSSIAN_CA_URL = 'https://gu-st.ru/content/Other/doc/russiantrustedca.pem';
    const DECISION_TTL_SECONDS = 604800;
    const CA_REFRESH_TTL_SECONDS = 604800;

    private $registry;
    private $runtime_dir;
    private $auto_refresh;
    private $memory_settings = [];

    public function __construct($registry = null, $runtime_dir = '', $auto_refresh = true) {
        $this->registry = $registry;
        $this->runtime_dir = trim((string)$runtime_dir);
        $this->auto_refresh = (bool)$auto_refresh;
    }

    public function initialTrustForUrl($url, $profile = '') {
        $host = $this->hostFromUrl($url);
        if ($host === '' || !$this->isFallbackAllowed($host, $profile)) {
            return $this->bundleInfo(self::BUNDLE_SYSTEM, null, false);
        }

        $decision = $this->decisionForHost($host);
        if (($decision['bundle'] ?? '') === self::BUNDLE_RUSSIAN_COMBINED
            && (int)($decision['expires_at'] ?? 0) > time()) {
            $info = $this->bundleInfo(self::BUNDLE_RUSSIAN_COMBINED, $this->combinedBundlePath(false), true);
            if (!empty($info['cainfo'])) {
                return $info;
            }
        }

        return $this->bundleInfo(self::BUNDLE_SYSTEM, null, false);
    }

    public function fallbackTrustForUrl($url, $profile = '', $allow_refresh = true) {
        $host = $this->hostFromUrl($url);
        if ($host === '' || !$this->isFallbackAllowed($host, $profile)) {
            return null;
        }

        $path = $this->combinedBundlePath($allow_refresh);
        if ($path === '' || !is_file($path)) {
            return null;
        }

        return $this->bundleInfo(self::BUNDLE_RUSSIAN_COMBINED, $path, false);
    }

    public function applyCurlOptions(array $curl_options, array $trust_info) {
        if (!empty($trust_info['cainfo'])) {
            $curl_options[CURLOPT_CAINFO] = $trust_info['cainfo'];
        } else {
            unset($curl_options[CURLOPT_CAINFO]);
        }
        $curl_options[CURLOPT_SSL_VERIFYPEER] = true;
        $curl_options[CURLOPT_SSL_VERIFYHOST] = 2;
        return $curl_options;
    }

    public function recordSuccess($url, array $trust_info, $reason = '', array $transport_result = []) {
        $host = $this->hostFromUrl($url);
        if ($host === '') {
            return;
        }

        $bundle = (string)($trust_info['bundle'] ?? self::BUNDLE_SYSTEM);
        $decisions = $this->loadDecisions();
        if ($bundle === self::BUNDLE_SYSTEM) {
            if (isset($decisions[$host])) {
                unset($decisions[$host]);
                $this->saveDecisions($decisions);
            }
            return;
        }

        $decisions[$host] = [
            'host' => $host,
            'bundle' => $bundle,
            'reason' => $reason !== '' ? $reason : 'tls_fallback_success',
            'checked_at' => time(),
            'expires_at' => time() + self::DECISION_TTL_SECONDS,
            'curl_version' => $this->curlVersion(),
            'openssl_version' => defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : '',
            'bundle_sha256' => !empty($trust_info['cainfo']) && is_file($trust_info['cainfo'])
                ? hash_file('sha256', $trust_info['cainfo'])
                : '',
            'last_http_code' => (int)($transport_result['http_code'] ?? 0),
        ];
        $this->saveDecisions($decisions);
    }

    public function isTlsVerificationError(array $transport_result) {
        $errno = (int)($transport_result['curl_errno'] ?? 0);
        $error = strtolower((string)($transport_result['curl_error'] ?? ''));
        if (in_array($errno, [51, 60, 83, 90], true)) {
            return true;
        }
        return strpos($error, 'certificate') !== false
            || strpos($error, 'unable to get local issuer') !== false
            || strpos($error, 'self signed certificate') !== false
            || strpos($error, 'peer certificate') !== false;
    }

    public function isFallbackAllowed($host, $profile = '') {
        $host = strtolower(trim((string)$host, ". \t\n\r\0\x0B"));
        if ($host === '') {
            return false;
        }

        $exact = [
            'platform-api2.max.ru' => true,
            'platform-api.max.ru' => true,
            'api.easytokens.ru' => true,
            'easytokens.ru' => true,
            'api.easychat24.ru' => true,
            'easychat24.ru' => true,
            'oauth.vk.com' => true,
            'api.vk.com' => true,
            'vk.com' => true,
            'vk.ru' => true,
            'api-seller.ozon.ru' => true,
            'buyer-chat-api.wildberries.ru' => true,
            'feedbacks-api.wildberries.ru' => true,
            'returns-api.wildberries.ru' => true,
            'common-api.wildberries.ru' => true,
        ];
        if (isset($exact[$host])) {
            return true;
        }

        $suffixes = [
            '.max.ru',
            '.vk.com',
            '.vk.ru',
            '.vk.me',
            '.vk-cdn.net',
            '.easychat24.ru',
            '.easytokens.ru',
            '.ozon.ru',
            '.ozonusercontent.com',
            '.ozone.ru',
            '.wildberries.ru',
            '.wbbasket.ru',
            '.wbstatic.net',
        ];
        foreach ($suffixes as $suffix) {
            if (substr($host, -strlen($suffix)) === $suffix) {
                return true;
            }
        }

        return false;
    }

    public function diagnosticsSnapshot() {
        $russian = $this->russianCaPath(false);
        $combined = $this->combinedBundlePath(false);
        return [
            'curl_available' => function_exists('curl_init'),
            'curl_version' => $this->curlVersion(),
            'openssl_version' => defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : '',
            'system_ca_file' => $this->discoverSystemCaFile(),
            'russian_ca_file' => $russian,
            'russian_ca_sha256' => is_file($russian) ? hash_file('sha256', $russian) : '',
            'combined_ca_file' => $combined,
            'combined_ca_sha256' => is_file($combined) ? hash_file('sha256', $combined) : '',
            'russian_ca_meta' => $this->loadJsonSetting(self::RUSSIAN_CA_META_KEY, []),
            'trust_decisions' => $this->loadDecisions(),
        ];
    }

    public function refreshRussianCaBundle($force = false) {
        $meta = $this->loadJsonSetting(self::RUSSIAN_CA_META_KEY, []);
        $runtime = $this->runtimeRussianCaPath();
        if (!$force && (int)($meta['next_check_at'] ?? 0) > time()) {
            if (is_file($runtime)) {
                $content = SafeRuntime::readFile($runtime);
                if (is_string($content) && !empty($this->validateRussianCaPem($content)['success'])) {
                    return ['success' => true, 'path' => $runtime, 'source' => 'runtime_cache'];
                }
            }
            if (isset($meta['success']) && empty($meta['success'])) {
                return $meta;
            }
        }
        if (!$force
            && is_file($runtime)
            && $this->validateRussianCaPem((string)SafeRuntime::readFile($runtime))['success']
            && (int)($meta['next_check_at'] ?? 0) > time()) {
            return ['success' => true, 'path' => $runtime, 'source' => 'runtime_cache'];
        }

        if (!function_exists('curl_init')) {
            return $this->saveRussianCaMeta(false, 'curl_unavailable', $runtime);
        }

        $ch = curl_init(self::RUSSIAN_CA_URL);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_CONNECTTIMEOUT => 4,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $raw = curl_exec($ch);
        $error = curl_error($ch);
        $errno = (int)curl_errno($ch);
        $http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if (PHP_VERSION_ID < 80500) {
            curl_close($ch);
        }

        if (!is_string($raw) || $raw === '' || $http_code < 200 || $http_code >= 300) {
            return $this->saveRussianCaMeta(false, $error !== '' ? 'download_failed_' . $errno : 'http_' . $http_code, $runtime);
        }

        $validation = $this->validateRussianCaPem($raw);
        if (empty($validation['success'])) {
            return $this->saveRussianCaMeta(false, (string)($validation['error'] ?? 'invalid_ca'), $runtime);
        }

        $dir = $this->ensureRuntimeDir();
        if ($dir === '') {
            return $this->saveRussianCaMeta(false, 'runtime_dir_unavailable', $runtime);
        }

        $tmp = $runtime . '.tmp';
        if (!SafeRuntime::writeFile($tmp, $raw) || !SafeRuntime::rename($tmp, $runtime)) {
            SafeRuntime::unlink($tmp);
            return $this->saveRussianCaMeta(false, 'write_failed', $runtime);
        }

        return $this->saveRussianCaMeta(true, '', $runtime, [
            'source_url' => self::RUSSIAN_CA_URL,
            'sha256' => hash('sha256', $raw),
            'certificates' => $validation['certificates'],
        ]);
    }

    private function bundleInfo($bundle, $cainfo, $from_cache) {
        return [
            'bundle' => $bundle,
            'cainfo' => $cainfo ?: '',
            'from_cache' => (bool)$from_cache,
        ];
    }

    private function hostFromUrl($url) {
        $parts = parse_url((string)$url);
        return $parts && !empty($parts['host']) ? strtolower((string)$parts['host']) : '';
    }

    private function decisionForHost($host) {
        $decisions = $this->loadDecisions();
        return is_array($decisions[$host] ?? null) ? $decisions[$host] : [];
    }

    private function loadDecisions() {
        $data = $this->loadJsonSetting(self::DECISIONS_KEY, []);
        return is_array($data) ? $data : [];
    }

    private function saveDecisions(array $decisions) {
        $this->saveSetting(self::DECISIONS_KEY, json_encode($decisions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    private function combinedBundlePath($allow_refresh) {
        $russian = $this->russianCaPath($allow_refresh);
        if ($russian === '' || !is_file($russian)) {
            return '';
        }

        $system = $this->discoverSystemCaFile();
        if ($system === '' || !is_file($system) || !is_readable($system)) {
            return $russian;
        }

        $dir = $this->ensureRuntimeDir();
        if ($dir === '') {
            return $russian;
        }

        $system_mtime = SafeRuntime::call(function () use ($system) { return filemtime($system); }, 0);
        $system_size = SafeRuntime::call(function () use ($system) { return filesize($system); }, 0);
        $fingerprint = hash('sha256', $system . '|' . (string)$system_mtime . '|' . (string)$system_size . '|' . hash_file('sha256', $russian));
        $combined = $dir . DS . 'combined_' . substr($fingerprint, 0, 20) . '.pem';
        if (is_file($combined)) {
            return $combined;
        }

        $system_pem = SafeRuntime::readFile($system);
        $russian_pem = SafeRuntime::readFile($russian);
        if (!is_string($system_pem) || $system_pem === '' || !is_string($russian_pem) || $russian_pem === '') {
            return $russian;
        }

        $content = rtrim($system_pem) . "\n\n" . rtrim($russian_pem) . "\n";
        $tmp = $combined . '.tmp';
        if (!SafeRuntime::writeFile($tmp, $content) || !SafeRuntime::rename($tmp, $combined)) {
            SafeRuntime::unlink($tmp);
            return $russian;
        }

        return $combined;
    }

    private function russianCaPath($allow_refresh) {
        if ($allow_refresh && $this->auto_refresh) {
            $this->refreshRussianCaBundle(false);
        }

        $runtime = $this->runtimeRussianCaPath();
        if (is_file($runtime)) {
            $content = SafeRuntime::readFile($runtime);
            if (is_string($content) && !empty($this->validateRussianCaPem($content)['success'])) {
                return $runtime;
            }
        }

        $bundled = $this->bundledRussianCaPath();
        return is_file($bundled) ? $bundled : '';
    }

    private function bundledRussianCaPath() {
        $library = defined('DIR_LIBRARY') ? DIR_LIBRARY : dirname(__DIR__);
        return $library . DS . 'ca' . DS . 'russiantrustedca.pem';
    }

    private function runtimeRussianCaPath() {
        $dir = $this->runtimeDirPath();
        return $dir !== '' ? $dir . DS . 'russiantrustedca.pem' : '';
    }

    private function runtimeDirPath() {
        if ($this->runtime_dir !== '') {
            return $this->runtime_dir;
        }
        if (defined('DIR_LOGS')) {
            return rtrim(DIR_LOGS, '/\\') . DS . 'tls';
        }
        if (defined('SITE_PATH')) {
            return SITE_PATH . DS . 'log' . DS . 'tls';
        }
        return sys_get_temp_dir() . DS . 'easychat_tls';
    }

    private function ensureRuntimeDir() {
        $dir = $this->runtimeDirPath();
        if ($dir === '') {
            return '';
        }
        if (!SafeRuntime::mkdir($dir, 0777, true)) {
            return '';
        }
        $htaccess = $dir . DS . '.htaccess';
        if (!is_file($htaccess)) {
            SafeRuntime::writeFile($htaccess, "Deny from all\n");
        }
        return $dir;
    }

    private function discoverSystemCaFile() {
        $candidates = [
            ini_get('curl.cainfo'),
            ini_get('openssl.cafile'),
            '/etc/ssl/certs/ca-certificates.crt',
            '/etc/pki/tls/certs/ca-bundle.crt',
            '/etc/ssl/ca-bundle.pem',
            '/usr/local/share/certs/ca-root-nss.crt',
            '/etc/ssl/cert.pem',
        ];
        foreach ($candidates as $candidate) {
            $candidate = trim((string)$candidate);
            if ($candidate !== '' && is_file($candidate) && is_readable($candidate)) {
                return $candidate;
            }
        }
        return '';
    }

    private function validateRussianCaPem($pem) {
        if (!is_string($pem) || substr_count($pem, '-----BEGIN CERTIFICATE-----') < 2) {
            return ['success' => false, 'error' => 'pem_blocks_missing'];
        }
        if (!function_exists('openssl_x509_parse') || !function_exists('openssl_x509_fingerprint')) {
            return ['success' => false, 'error' => 'openssl_x509_unavailable'];
        }

        preg_match_all('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s', $pem, $matches);
        $subjects = [];
        $fingerprints = [];
        foreach ($matches[0] as $cert) {
            $parsed = SafeRuntime::call(function () use ($cert) { return openssl_x509_parse($cert); }, false);
            if (!is_array($parsed)) {
                continue;
            }
            $cn = (string)($parsed['subject']['CN'] ?? '');
            $issuer_cn = (string)($parsed['issuer']['CN'] ?? '');
            $valid_to = (int)($parsed['validTo_time_t'] ?? 0);
            $fingerprint = strtoupper((string)SafeRuntime::call(function () use ($cert) {
                return openssl_x509_fingerprint($cert, 'sha256');
            }, ''));
            $subjects[$cn] = [
                'cn' => $cn,
                'issuer_cn' => $issuer_cn,
                'valid_to' => $valid_to,
                'fingerprint_sha256' => $fingerprint,
            ];
            $fingerprints[$fingerprint] = true;
        }

        $root_fp = 'D26D2D0231B7C39F92CC738512BA54103519E4405D68B5BD703E9788CA8ECF31';
        $sub_fp = 'BBBDE2103E790B999EC62BD03CF625A5A2E7C316E10AFE6A490EEDEAD8B3FD9B';
        if (empty($subjects['Russian Trusted Root CA']) || empty($subjects['Russian Trusted Sub CA'])) {
            return ['success' => false, 'error' => 'expected_subjects_missing'];
        }
        if (empty($fingerprints[$root_fp]) || empty($fingerprints[$sub_fp])) {
            return ['success' => false, 'error' => 'known_fingerprints_missing'];
        }
        if ((int)$subjects['Russian Trusted Root CA']['valid_to'] <= time() + 86400
            || (int)$subjects['Russian Trusted Sub CA']['valid_to'] <= time() + 86400) {
            return ['success' => false, 'error' => 'certificate_expired'];
        }

        return ['success' => true, 'certificates' => array_values($subjects)];
    }

    private function saveRussianCaMeta($success, $error, $path, array $extra = []) {
        $meta = array_merge([
            'success' => (bool)$success,
            'last_checked_at' => time(),
            'next_check_at' => time() + self::CA_REFRESH_TTL_SECONDS,
            'error' => (string)$error,
            'path' => (string)$path,
        ], $extra);
        $this->saveSetting(self::RUSSIAN_CA_META_KEY, json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        return $meta;
    }

    private function loadJsonSetting($key, $default) {
        $raw = $this->getSetting($key, '');
        $decoded = json_decode((string)$raw, true);
        return is_array($decoded) ? $decoded : $default;
    }

    private function getSetting($key, $default = '') {
        if ($this->registry) {
            try {
                $model = new Setting($this->registry);
                return $model->get($key, $default);
            } catch (\Throwable $e) {
                return $default;
            }
        }
        return array_key_exists($key, $this->memory_settings) ? $this->memory_settings[$key] : $default;
    }

    private function saveSetting($key, $value) {
        if ($this->registry) {
            try {
                $model = new Setting($this->registry);
                $model->set($key, (string)$value);
                return;
            } catch (\Throwable $e) {
                return;
            }
        }
        $this->memory_settings[$key] = (string)$value;
    }

    private function curlVersion() {
        if (!function_exists('curl_version')) {
            return '';
        }
        $version = curl_version();
        return is_array($version) ? (string)($version['version'] ?? '') : '';
    }
}
