<?php
if (!defined('ABSPATH')) {
    exit;
}

class BotBlocker_Engine {
    public static function init() {
        add_action('init', [__CLASS__, 'handle_request'], 1);
    }

    public static function handle_request() {
        if (self::should_skip()) {
            return;
        }

        $settings = BotBlocker_Settings::get();
        if (empty($settings['enforcement_enabled']) && empty($settings['logging_enabled'])) {
            return;
        }

        $ip = self::client_ip($settings);
        $ip_hash = BotBlocker_Storage::hash_ip($ip);
        $path = self::current_path();
        $ua = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
        $lists = $settings['lists'];

        if (self::matches_ip_list($ip, $lists['allow_ips']) || self::matches_text_list($ua, $lists['allow_agents']) || self::matches_path_list($path, $lists['allow_paths'])) {
            return;
        }

        if (self::matches_ip_list($ip, $lists['block_ips']) || self::matches_text_list($ua, $lists['block_agents']) || self::matches_path_list($path, $lists['block_paths'])) {
            $manual_action = !empty($settings['enforcement_enabled']) ? 'block' : 'log';
            self::log_event_if_enabled($settings, self::build_log_event($settings, [
                'ip'                 => $ip,
                'ip_hash'            => $ip_hash,
                'user_agent'         => $ua,
                'bot_name'           => __('Manual block', 'sera-bot-blocker'),
                'category'           => 'manual_block',
                'claimed_identity'   => '',
                'verification_state' => '',
                'path'               => $path,
                'action_taken'       => $manual_action,
                'matched_rule'       => 'manual-block-list',
            ]));

            if (!empty($settings['enforcement_enabled'])) {
                self::emit_block('block');
            }
            return;
        }

        if (!empty($settings['enforcement_enabled'])) {
            $lock_reason = self::locked_reason($ip, $ua, $ip_hash);
            if ($lock_reason !== '') {
                self::log_event_if_enabled($settings, [
                    'occurred_at'        => gmdate('Y-m-d H:i:s'),
                    'ip'                 => !empty($settings['store_full_ip']) ? $ip : '',
                    'ip_hash'            => $ip_hash,
                    'masked_ip'          => BotBlocker_Storage::mask_ip($ip),
                    'user_agent'         => $ua,
                    'bot_name'           => __('Temporary lock', 'sera-bot-blocker'),
                    'category'           => 'manual_block',
                    'claimed_identity'   => '',
                    'verification_state' => '',
                    'path'               => $path,
                    'query_string'       => self::logged_query_string($settings),
                    'referrer'           => self::logged_referrer($settings),
                    'action_taken'       => 'temporary_block',
                    'matched_rule'       => $lock_reason,
                ]);
                self::emit_block('temporary_block');
            }
        }

        $detection = BotBlocker_Detector::detect($ua, $path, $ip);
        if (!$detection['matched']) {
            return;
        }

        $action = $settings['rules'][$detection['category']] ?? 'log';
        $burst = 1;

        if ($action === 'rate_limit') {
            $counter_key = 'bot_blocker_rate_' . md5($ip_hash . '|' . $detection['category']);
            $hits = (int) get_transient($counter_key) + 1;
            set_transient($counter_key, $hits, MINUTE_IN_SECONDS);
            $burst = $hits;
            $limit = max(1, (int) ($settings['rate_limits'][$detection['category']] ?? 20));
            if ($hits > $limit) {
                $action = 'temporary_block';
            }
        }

        if ($action === 'temporary_block' && !empty($settings['enforcement_enabled'])) {
            foreach (self::lock_keys($ip, $ua, $ip_hash, $detection['category']) as $lock_key) {
                BotBlocker_Storage::set_lock($lock_key, $detection['category'], 30);
            }
        }

        $logged_action = $action;
        if (empty($settings['enforcement_enabled']) && in_array($logged_action, ['block', 'temporary_block'], true)) {
            $logged_action = 'log';
        }

        self::log_event_if_enabled($settings, self::build_log_event($settings, [
            'occurred_at'        => gmdate('Y-m-d H:i:s'),
            'ip'                 => $ip,
            'ip_hash'            => $ip_hash,
            'user_agent'         => $ua,
            'bot_name'           => $detection['bot_name'],
            'category'           => $detection['category'],
            'claimed_identity'   => $detection['claimed_identity'],
            'verification_state' => $detection['verification_state'],
            'path'               => $path,
            'action_taken'       => $logged_action,
            'matched_rule'       => $detection['matched_rule'],
            'burst_count'        => $burst,
        ]));

        if (!empty($settings['enforcement_enabled']) && in_array($action, ['block', 'temporary_block'], true)) {
            self::emit_block($action);
        }
    }

    private static function should_skip() {
        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '');
        $script = (string) ($_SERVER['PHP_SELF'] ?? '');

        if ((function_exists('is_user_logged_in') && is_user_logged_in()) || is_admin() || (defined('WP_ADMIN') && WP_ADMIN) || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
            return true;
        }

        if (defined('REST_REQUEST') && REST_REQUEST) {
            if (function_exists('current_user_can') && current_user_can('manage_options')) {
                return true;
            }
            if (strpos($uri, '/wp-json/bot-blocker/v1/monitor') !== false) {
                return true;
            }
        }

        foreach (['/wp-admin/', '/wp-login.php', '/wp-register.php', '/wp-signup.php', '/wp-activate.php', '/admin-ajax.php', '/admin-post.php'] as $path) {
            if (($uri !== '' && strpos($uri, $path) !== false) || ($script !== '' && strpos($script, $path) !== false)) {
                return true;
            }
        }

        return false;
    }

    private static function build_log_event($settings, array $event) {
        $ip = (string) ($event['ip'] ?? '');
        $base = [
            'occurred_at'        => gmdate('Y-m-d H:i:s'),
            'ip'                 => !empty($settings['store_full_ip']) ? $ip : '',
            'ip_hash'            => (string) ($event['ip_hash'] ?? BotBlocker_Storage::hash_ip($ip)),
            'masked_ip'          => BotBlocker_Storage::mask_ip($ip),
            'user_agent'         => (string) ($event['user_agent'] ?? ''),
            'bot_name'           => (string) ($event['bot_name'] ?? ''),
            'category'           => (string) ($event['category'] ?? ''),
            'claimed_identity'   => (string) ($event['claimed_identity'] ?? ''),
            'verification_state' => (string) ($event['verification_state'] ?? ''),
            'path'               => (string) ($event['path'] ?? '/'),
            'query_string'       => self::logged_query_string($settings),
            'referrer'           => self::logged_referrer($settings),
            'action_taken'       => (string) ($event['action_taken'] ?? 'log'),
            'matched_rule'       => (string) ($event['matched_rule'] ?? ''),
            'country'            => '',
            'region'             => '',
            'city'               => '',
            'latitude'           => null,
            'longitude'          => null,
            'geo_source'         => '',
            'burst_count'        => max(1, (int) ($event['burst_count'] ?? 1)),
        ];

        $geo = BotBlocker_Geo::prime($ip);
        return wp_parse_args($geo, $base);
    }

    private static function log_event_if_enabled($settings, array $event) {
        if (empty($settings['logging_enabled'])) {
            return;
        }
        BotBlocker_Storage::insert_event($event);
    }

    private static function emit_block($action) {
        status_header($action === 'temporary_block' ? 429 : 403);
        nocache_headers();
        echo 'Blocked by Bot Blocker';
        exit;
    }

    private static function current_path() {
        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
        $path = parse_url($uri, PHP_URL_PATH);
        return $path ? $path : '/';
    }

    private static function matches_text_list($value, $list) {
        $value = (string) $value;
        foreach (self::split_lines($list) as $item) {
            if ($item !== '' && stripos($value, $item) !== false) {
                return true;
            }
        }
        return false;
    }

    private static function matches_path_list($path, $list) {
        foreach (self::split_lines($list) as $item) {
            if ($item !== '' && strpos($path, $item) !== false) {
                return true;
            }
        }
        return false;
    }

    private static function matches_ip_list($ip, $list) {
        foreach (self::split_lines($list) as $item) {
            if (self::ip_matches($ip, $item)) {
                return true;
            }
        }
        return false;
    }

    private static function split_lines($value) {
        $lines = preg_split('/\r\n|\r|\n/', (string) $value);
        $clean = [];
        foreach ($lines as $line) {
            $line = trim((string) $line);
            if ($line !== '') {
                $clean[] = $line;
            }
        }
        return $clean;
    }

    private static function ip_matches($ip, $rule) {
        $rule = trim((string) $rule);
        if ($rule === '') {
            return false;
        }
        if (strpos($rule, '/') === false) {
            return strcasecmp($ip, $rule) === 0;
        }

        [$subnet, $mask_bits] = array_pad(explode('/', $rule, 2), 2, null);
        $mask_bits = (int) $mask_bits;
        $ip_bin = @inet_pton($ip);
        $subnet_bin = @inet_pton($subnet);
        if ($ip_bin === false || $subnet_bin === false || strlen($ip_bin) !== strlen($subnet_bin)) {
            return false;
        }

        $max_bits = strlen($ip_bin) * 8;
        if ($mask_bits < 0 || $mask_bits > $max_bits) {
            return false;
        }

        $bytes = intdiv($mask_bits, 8);
        $remainder = $mask_bits % 8;

        if ($bytes > 0 && substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) {
            return false;
        }

        if ($remainder === 0) {
            return true;
        }

        $mask = (~((1 << (8 - $remainder)) - 1)) & 0xFF;
        return (ord($ip_bin[$bytes]) & $mask) === (ord($subnet_bin[$bytes]) & $mask);
    }

    private static function client_ip($settings) {
        $remote_addr = self::valid_ip($_SERVER['REMOTE_ADDR'] ?? '');
        if ($remote_addr === '') {
            return '0.0.0.0';
        }

        if (!self::is_trusted_proxy($remote_addr, $settings)) {
            return $remote_addr;
        }

        $candidates = [];
        if (!empty($settings['trust_cloudflare_headers'])) {
            $candidates[] = 'HTTP_CF_CONNECTING_IP';
        }
        $candidates[] = 'HTTP_X_FORWARDED_FOR';

        foreach ($candidates as $header) {
            if (empty($_SERVER[$header])) {
                continue;
            }
            $parts = explode(',', (string) $_SERVER[$header]);
            foreach ($parts as $part) {
                $candidate = self::valid_ip($part);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        return $remote_addr;
    }

    private static function valid_ip($value) {
        $value = trim((string) $value);
        return filter_var($value, FILTER_VALIDATE_IP) ? $value : '';
    }

    private static function is_trusted_proxy($remote_addr, $settings) {
        foreach (self::split_lines($settings['trusted_proxy_ips'] ?? '') as $rule) {
            if (self::ip_matches($remote_addr, $rule)) {
                return true;
            }
        }
        return false;
    }

    private static function lock_keys($ip, $ua, $ip_hash, $category) {
        $keys = ['ip:' . $ip_hash];
        if (!in_array($category, ['scanner_bots', 'spam_bots', 'sensitive_paths'], true)) {
            $keys[] = 'ipua:' . md5($ip_hash . '|' . $ua);
        }
        return array_values(array_unique($keys));
    }

    private static function locked_reason($ip, $ua, $ip_hash) {
        foreach (['ip:' . $ip_hash, 'ipua:' . md5($ip_hash . '|' . $ua)] as $key) {
            if (BotBlocker_Storage::is_locked($key)) {
                return $key;
            }
        }
        return '';
    }

    private static function logged_query_string($settings) {
        if (empty($settings['log_query_strings'])) {
            return '';
        }

        $query = (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_QUERY);
        return self::redact_query_string($query);
    }

    private static function logged_referrer($settings) {
        if (empty($settings['log_referrers'])) {
            return '';
        }

        return self::redact_url((string) ($_SERVER['HTTP_REFERER'] ?? ''));
    }

    private static function redact_url($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }

        $parts = wp_parse_url($url);
        if (!is_array($parts)) {
            return sanitize_text_field($url);
        }

        $rebuilt = '';
        if (!empty($parts['scheme'])) {
            $rebuilt .= $parts['scheme'] . '://';
        }
        if (!empty($parts['user'])) {
            $rebuilt .= $parts['user'];
            if (!empty($parts['pass'])) {
                $rebuilt .= ':***';
            }
            $rebuilt .= '@';
        }
        $rebuilt .= $parts['host'] ?? '';
        if (!empty($parts['port'])) {
            $rebuilt .= ':' . (int) $parts['port'];
        }
        $rebuilt .= $parts['path'] ?? '';
        if (!empty($parts['query'])) {
            $rebuilt .= '?' . self::redact_query_string($parts['query']);
        }

        return sanitize_text_field($rebuilt);
    }

    private static function redact_query_string($query) {
        $query = ltrim((string) $query, '?');
        if ($query === '') {
            return '';
        }

        parse_str($query, $params);
        if (!is_array($params)) {
            return '';
        }

        $sensitive_keys = ['password', 'pass', 'pwd', 'token', 'key', 'api_key', 'apikey', 'auth', 'authorization', 'session', 'sessid', 'email', 'mail', 'nonce', 'code', 'secret', 'access_token', 'refresh_token', 'client_secret', 'private_key', 'bearer', 'jwt', 'sig', 'signature'];
        foreach ($params as $key => $value) {
            if (in_array(strtolower((string) $key), $sensitive_keys, true)) {
                $params[$key] = 'REDACTED';
            }
        }

        return http_build_query($params, '', '&');
    }
}
