<?php
/**
 * Runtime utilities used by Ultraguard Pro.
 *
 * This class was trimmed down to the helpers that are actually consumed by the
 * plugin at runtime. Dead system-inspection and formatting helpers were removed
 * to keep the codebase smaller and easier to maintain.
 *
 * @package UltraguardPro\core
 */

namespace UltraguardPro\core;

use Exception;

class Utils {

    /**
     * Lightweight in-memory cache for hot helpers.
     *
     * @var array<string, mixed>
     */
    private static $caches = [
        'client_ip'           => null,
        'cloudflare_request'  => null,
        'sensitive_patterns'  => null,
        'debug_mode'          => null,
        'current_user_admin'  => null,
        'is_https'            => null,
        'user_agent'          => null,
        'request_uri'         => null,
    ];

    /**
     * Static Cloudflare ranges used for trusted proxy detection.
     *
     * @var string[]|null
     */
    private static $cloudflare_ranges = null;

    /**
     * Return the best available client IP.
     *
     * @param array<string, mixed> $config
     * @return string
     */
    public static function get_client_ip($config = []) {
        if (self::$caches['client_ip'] !== null) {
            return self::$caches['client_ip'];
        }

        $config = wp_parse_args($config, [
            'trust_cloudflare' => true,
            'validate_private' => true,
            'fallback_ip'      => '0.0.0.0',
        ]);

        $trusted_headers = ['REMOTE_ADDR'];

        if ($config['trust_cloudflare'] && self::is_cloudflare_request()) {
            array_unshift($trusted_headers, 'HTTP_CF_CONNECTING_IP');
        }

        foreach ($trusted_headers as $header) {
            if (empty($_SERVER[$header])) {
                continue;
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $header comes from the trusted proxy header allowlist above.
            $ip_candidates = explode(',', (string) wp_unslash( $_SERVER[ $header ] ) );
            $ip = self::normalize_ip(trim((string) $ip_candidates[0]));

            if (!self::validate_ip($ip)) {
                continue;
            }

            if ($header === 'REMOTE_ADDR' || !$config['validate_private'] || !self::is_private_ip($ip)) {
                self::$caches['client_ip'] = $ip;
                return $ip;
            }
        }

        $fallback = (PHP_SAPI === 'cli') ? '127.0.0.1' : (string) $config['fallback_ip'];
        self::$caches['client_ip'] = self::normalize_ip($fallback);

        return self::$caches['client_ip'];
    }

    /**
     * Normalize local aliases into a consistent IP value.
     *
     * @param string $ip
     * @return string
     */
    public static function normalize_ip($ip) {
        $ip = trim((string) $ip);

        if ($ip === '::1' || $ip === '0:0:0:0:0:0:0:1' || strtolower($ip) === 'localhost') {
            return '127.0.0.1';
        }

        return $ip;
    }

    /**
     * Check whether the current request is coming from Cloudflare.
     *
     * @return bool
     */
    private static function is_cloudflare_request() {
        if (self::$caches['cloudflare_request'] !== null) {
            return (bool) self::$caches['cloudflare_request'];
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REMOTE_ADDR is normalized and validated immediately below.
        $remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) ? self::normalize_ip( (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        if (!self::validate_ip($remote_addr)) {
            self::$caches['cloudflare_request'] = false;
            return false;
        }

        if (self::$cloudflare_ranges === null) {
            self::$cloudflare_ranges = [
                '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',
            ];
        }

        foreach (self::$cloudflare_ranges as $range) {
            if (self::ip_in_range($remote_addr, $range)) {
                self::$caches['cloudflare_request'] = true;
                return true;
            }
        }

        self::$caches['cloudflare_request'] = false;
        return false;
    }

    /**
     * Check whether an IP falls within a CIDR range.
     *
     * @param string $ip
     * @param string $cidr
     * @return bool
     */
    public static function ip_in_range($ip, $cidr) {
        if (strpos((string) $cidr, '/') === false) {
            return $ip === $cidr;
        }

        list($subnet, $mask) = explode('/', (string) $cidr, 2);
        $mask = (int) $mask;

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);

            if ($ip_long === false || $subnet_long === false) {
                return false;
            }

            $mask_long = -1 << (32 - $mask);
            return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $ip_binary = inet_pton($ip);
            $subnet_binary = inet_pton($subnet);

            if ($ip_binary === false || $subnet_binary === false) {
                return false;
            }

            $mask_binary = self::ipv6_mask_to_binary($mask);

            for ($i = 0; $i < 16; $i++) {
                if ((ord($ip_binary[$i]) & ord($mask_binary[$i])) !== (ord($subnet_binary[$i]) & ord($mask_binary[$i]))) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Convert an IPv6 prefix length into a binary mask.
     *
     * @param int $mask
     * @return string
     */
    private static function ipv6_mask_to_binary($mask) {
        $mask = max(0, min(128, (int) $mask));
        $binary = '';

        for ($i = 0; $i < 16; $i++) {
            $byte_mask = 0;

            if ($mask > 0) {
                if ($mask >= 8) {
                    $byte_mask = 0xFF;
                } else {
                    $byte_mask = (0xFF << (8 - $mask)) & 0xFF;
                }

                $mask -= 8;
            }

            $binary .= chr($byte_mask);
        }

        return $binary;
    }

    /**
     * Validate an IP address.
     *
     * @param string $ip
     * @return bool
     */
    public static function validate_ip($ip) {
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    /**
     * Detect private or reserved addresses.
     *
     * @param string $ip
     * @return bool
     */
    public static function is_private_ip($ip) {
        if (!self::validate_ip($ip)) {
            return true;
        }

        return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    }

    /**
     * Return the sanitized current user agent.
     *
     * @return string
     */
    public static function get_user_agent() {
        if (self::$caches['user_agent'] === null) {
            self::$caches['user_agent'] = isset($_SERVER['HTTP_USER_AGENT'])
                ? sanitize_text_field( (string) wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
                : 'Unknown';
        }

        return (string) self::$caches['user_agent'];
    }

    /**
     * Return the sanitized current request URI.
     *
     * @return string
     */
    public static function get_request_uri() {
        if (self::$caches['request_uri'] === null) {
            self::$caches['request_uri'] = isset($_SERVER['REQUEST_URI'])
                ? esc_url_raw( (string) wp_unslash( $_SERVER['REQUEST_URI'] ) )
                : '/';
        }

        return (string) self::$caches['request_uri'];
    }

    /**
     * Format bytes in a human-readable form.
     *
     * @param int|float $size
     * @param int $precision
     * @return string
     */
    public static function format_bytes($size, $precision = 2) {
        $size = (float) $size;

        if ($size <= 0) {
            return '0 B';
        }

        $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
        $base = log($size, 1024);
        $index = (int) floor($base);
        $index = min($index, count($units) - 1);

        return round($size / pow(1024, $index), (int) $precision) . ' ' . $units[$index];
    }

    /**
     * Check whether the current request is HTTPS.
     *
     * @return bool
     */
    public static function is_https() {
        if (self::$caches['is_https'] === null) {
            self::$caches['is_https'] = is_ssl();
        }

        return (bool) self::$caches['is_https'];
    }

    /**
     * Validate a URL.
     *
     * @param string $url
     * @return bool
     */
    public static function validate_url($url) {
        if (!is_string($url) || $url === '') {
            return false;
        }

        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }

    /**
     * Resolve a best-effort country code for a public IP.
     *
     * @param string $ip
     * @param array<string, mixed> $config
     * @return string
     */
    public static function get_country_from_ip($ip, $config = []) {
        if (!self::validate_ip($ip) || self::is_private_ip($ip)) {
            return 'XX';
        }

        $config = wp_parse_args($config, [
            'trust_cloudflare' => true,
            'use_wp_geo'       => true,
            'skip_external'    => false,
        ]);

        if ($config['trust_cloudflare'] && isset($_SERVER['HTTP_CF_IPCOUNTRY']) && self::is_cloudflare_request()) {
            $country = strtoupper( sanitize_text_field( (string) wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) );
            if (strlen($country) === 2 && ctype_alpha($country)) {
                return $country;
            }
        }

        if ($config['use_wp_geo'] && function_exists('wp_get_ip_geolocation')) {
            $geo = wp_get_ip_geolocation($ip);
            if (!empty($geo['country_code']) && is_string($geo['country_code'])) {
                return strtoupper(substr(sanitize_text_field($geo['country_code']), 0, 2));
            }
        }

        $transient_key = 'ultraguard_geo_' . md5($ip);
        $cached = CacheManager::get_instance()->get_db($transient_key);
        if ($cached !== false) {
            return (string) $cached;
        }

        $bundled = self::lookup_bundled_geo($ip);
        if ($bundled !== null) {
            CacheManager::get_instance()->set($transient_key, $bundled, 'asset');
            return $bundled;
        }

        if (empty($config['skip_external']) && !(defined('ULTRAGUARD_SKIP_EXTERNAL_GEO') && ULTRAGUARD_SKIP_EXTERNAL_GEO)) {
            $country = self::fetch_geo_from_ipapi($ip);
            if ($country !== null) {
                CacheManager::get_instance()->set($transient_key, $country, 'asset');
                return $country;
            }
        }

        CacheManager::get_instance()->set($transient_key, 'XX', 'asset');
        return 'XX';
    }

    /**
     * Resolve a best-effort country name from an ISO alpha-2 code.
     *
     * @param string $country_code
     * @return string
     */
    public static function get_country_name_from_code($country_code) {
        static $country_map = null;

        $country_code = strtoupper(substr(sanitize_text_field((string) $country_code), 0, 2));
        if ($country_code === '' || $country_code === 'XX') {
            return '';
        }

        if ($country_map === null) {
            $country_map = [];

            $json_file = defined('ULTRAGUARD_PRO_DIR')
                ? ULTRAGUARD_PRO_DIR . 'assets/json/country-codes.json'
                : dirname(dirname(__DIR__)) . '/assets/json/country-codes.json';

            if (file_exists($json_file)) {
                $raw = file_get_contents($json_file);
                $decoded = json_decode((string) $raw, true);

                if (is_array($decoded)) {
                    foreach (($decoded['countries'] ?? []) as $entry) {
                        $code = strtoupper((string) ($entry['code'] ?? ''));
                        $name = sanitize_text_field((string) ($entry['name'] ?? ''));
                        if ($code !== '' && $name !== '') {
                            $country_map[$code] = $name;
                        }
                    }
                }
            }

            if (empty($country_map) && class_exists('WC_Countries')) {
                $country_map = array_map(
                    static fn($name) => sanitize_text_field((string) $name),
                    (new \WC_Countries())->get_countries()
                );
            }
        }

        return (string) ($country_map[$country_code] ?? '');
    }

    /**
     * Check the bundled geolocation data for a direct match.
     *
     * @param string $ip
     * @return string|null
     */
    private static function lookup_bundled_geo($ip) {
        static $bundled_data = null;

        if ($bundled_data === null) {
            $json_file = defined('ULTRAGUARD_PRO_DIR')
                ? ULTRAGUARD_PRO_DIR . 'assets/json/geolocation_data.json'
                : dirname(dirname(__DIR__)) . '/assets/json/geolocation_data.json';

            $bundled_data = [];

            if (file_exists($json_file)) {
                $raw = file_get_contents($json_file);
                $decoded = json_decode((string) $raw, true);

                if (is_array($decoded)) {
                    foreach ($decoded as $entry) {
                        if (!empty($entry['ip_address'])) {
                            $bundled_data[(string) $entry['ip_address']] = strtoupper((string) ($entry['country_code'] ?? 'XX'));
                        }
                    }
                }
            }
        }

        return $bundled_data[$ip] ?? null;
    }

    /**
     * Query ip-api.com for a country code.
     *
     * @param string $ip
     * @return string|null
     */
    private static function fetch_geo_from_ipapi($ip) {
        $response = wp_remote_get(
            'http://ip-api.com/json/' . rawurlencode($ip) . '?fields=status,countryCode',
            [
                'timeout'   => 3,
                'sslverify' => false,
                'headers'   => ['Accept' => 'application/json'],
            ]
        );

        if (is_wp_error($response)) {
            return null;
        }

        if ((int) wp_remote_retrieve_response_code($response) !== 200) {
            return null;
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);
        if (!is_array($body) || ($body['status'] ?? '') !== 'success' || empty($body['countryCode'])) {
            return null;
        }

        $country = strtoupper(sanitize_text_field((string) $body['countryCode']));
        return strlen($country) === 2 ? $country : null;
    }

    /**
     * Check whether WordPress debug mode is enabled.
     *
     * @return bool
     */
    public static function is_debug_mode() {
        if (self::$caches['debug_mode'] === null) {
            self::$caches['debug_mode'] = defined('WP_DEBUG') && WP_DEBUG;
        }

        return (bool) self::$caches['debug_mode'];
    }

    /**
     * Log a security event through the shared logger.
     *
     * @param string $event_type
     * @param string $message
     * @param array<string, mixed> $data
     * @param string $level
     * @return bool
     */
    public static function log_security_event($event_type, $message, $data = [], $level = 'info') {
        $event_type = sanitize_key((string) $event_type);
        $message = sanitize_text_field((string) $message);
        $level = sanitize_key((string) $level);
        $sanitized_data = self::sanitize_log_data(is_array($data) ? $data : []);

        $result = false;

        try {
            $logger = Logger::get_instance();

            switch ($level) {
                case 'error':
                    $result = $logger->error($message, array_merge($sanitized_data, ['event_type' => $event_type]));
                    break;
                case 'warning':
                    $result = $logger->warning($message, array_merge($sanitized_data, ['event_type' => $event_type]));
                    break;
                case 'debug':
                    $result = $logger->debug($message, array_merge($sanitized_data, ['event_type' => $event_type]));
                    break;
                case 'info':
                default:
                    $result = $logger->info($message, array_merge($sanitized_data, ['event_type' => $event_type]));
                    break;
            }
        } catch (Exception $e) {
            if (self::is_debug_mode()) {
                \UltraguardPro\core\Logger::error( 'Ultraguard Pro Security Event [' . $event_type . ']: ' . $message, $sanitized_data );
            }

            return false;
        }

        do_action('ultraguard_utils/security_event', $event_type, $message, $sanitized_data, $level);

        return (bool) $result;
    }

    /**
     * Remove sensitive values before logging.
     *
     * @param array<string, mixed> $data
     * @return array<string, mixed>
     */
    private static function sanitize_log_data($data) {
        if (empty($data)) {
            return [];
        }

        if (self::$caches['sensitive_patterns'] === null) {
            self::$caches['sensitive_patterns'] = apply_filters('ultraguard_sensitive_log_keys', [
                'password',
                'pass',
                'pwd',
                'secret',
                'key',
                'token',
                'auth',
                'nonce',
            ]);
        }

        $sanitized = [];

        foreach ($data as $key => $value) {
            $key_lower = strtolower((string) $key);
            $is_sensitive = false;

            foreach ((array) self::$caches['sensitive_patterns'] as $sensitive_key) {
                if ($sensitive_key !== '' && strpos($key_lower, (string) $sensitive_key) !== false) {
                    $is_sensitive = true;
                    break;
                }
            }

            if ($is_sensitive) {
                $sanitized[$key] = '***REDACTED***';
            } elseif (is_array($value)) {
                $sanitized[$key] = self::sanitize_log_data($value);
            } elseif (is_string($value)) {
                $sanitized[$key] = sanitize_text_field($value);
            } else {
                $sanitized[$key] = $value;
            }
        }

        return $sanitized;
    }

    /**
     * Check whether the current user can manage options.
     *
     * @return bool
     */
    public static function is_current_user_admin() {
        if (self::$caches['current_user_admin'] === null) {
            self::$caches['current_user_admin'] = \UltraguardPro\ultraguard_current_user_can_manage();
        }

        return (bool) self::$caches['current_user_admin'];
    }

    /**
     * Clear hot in-memory caches.
     *
     * @param string|null $cache_key
     * @return void
     */
    public static function clear_caches($cache_key = null) {
        if ($cache_key === null) {
            foreach (array_keys(self::$caches) as $key) {
                self::$caches[$key] = null;
            }

            return;
        }

        if (array_key_exists($cache_key, self::$caches)) {
            self::$caches[$cache_key] = null;
        }
    }
}
