<?php
/**
 * Advanced 404 protection with pattern detection and abuse tracking
 *
 * @package BotFend\Security
 * @since 4.5.1
 * @version 4.8.3 - Rebranded from SafeBot, geolocation opt-in added
 */

namespace BotFend\Security;

use BotFend\Core\Config;
use BotFend\Utilities\Utilities;
use BotFend\Database\Database;

defined('ABSPATH') || exit;

/**
 * Class Advanced404
 *
 * Complete 404 management system:
 * - Standard 404 abuse detection (rate-based) with atomic increments
 * - Advanced pattern-based detection (sensitive paths, vulnerability scans)
 * - Score accumulation and immediate blocking
 * - Crawler 404 logging for SEO monitoring
 * - Two-layer persistent country caching (with opt-in check)
 */
class Advanced404 {

    private Config $config;
    private Utilities $utilities;
    private Database $database;
    private array $analyzed_ips = [];
    private static array $country_cache = [];

    private const MAX_COUNTRY_CACHE = 100;
    private const MAX_ANALYZED_CACHE = 500;
    private const CACHE_TRIM_AMOUNT = 100;

    private const RATE_LIMIT_KEY_TTL = 60;
    private const COUNTRY_CACHE_TTL = DAY_IN_SECONDS;

    /**
     * Cache for validated regex patterns (Optimization - My Win)
     * @var array|null
     */
    private static ?array $validated_patterns = null;

    private array $pattern_categories = [
        'sensitive' => [
            'weight' => 4,
            'patterns' => [
                '/wp-config\.php/i',
                '/wp-config\.bak/i',
                '/wp-config\.old/i',
                '/\.env/i',
                '/\.git/i',
                '/\.svn/i',
                '/\.htaccess/i',
                '/passwd/i',
                '/shadow/i',
                '/\.ssh/i',
                '/authorized_keys/i',
                '/id_rsa/i',
                '/id_dsa/i',
            ]
        ],
        'vulnerability' => [
            'weight' => 3,
            'patterns' => [
                '/wp-admin/i',
                '/wp-login/i',
                '/administrator/i',
                '/cgi-bin/i',
                '/phpmyadmin/i',
                '/mysql/i',
                '/xmlrpc\.php/i',
                '/wp-cron\.php/i',
                '/wp-signup\.php/i',
                '/wp-activate\.php/i',
                '/wp-mail\.php/i',
                '/wp-links-opml\.php/i',
                '/wp-settings\.php/i',
                '/wp-load\.php/i',
                '/wp-blog-header\.php/i',
            ]
        ],
        'api_scan' => [
            'weight' => 2,
            'patterns' => [
                '/api\/v[0-9]/i',
                '/graphql/i',
                '/\.json$/i',
                '/\.xml$/i',
                '/\.sql$/i',
                '/\.yaml$/i',
                '/\.yml$/i',
                '/swagger/i',
                '/openapi/i',
            ]
        ],
        'cms_scan' => [
            'weight' => 1,
            'patterns' => [
                '/wp-content/i',
                '/wp-includes/i',
                '/wp-json/i',
                '/vendor/i',
                '/composer\.json/i',
                '/package\.json/i',
                '/node_modules/i',
                '/bower_components/i',
            ]
        ],
        'shell_scripts' => [
            'weight' => 5,
            'patterns' => [
                '/shell\.php/i',
                '/cmd\.php/i',
                '/c99\.php/i',
                '/r57\.php/i',
                '/wso\.php/i',
                '/b374k\.php/i',
                '/webshell/i',
                '/backdoor/i',
                '/eval\.php/i',
                '/exec\.php/i',
                '/system\.php/i',
                '/passthru\.php/i',
            ]
        ],
        'traversal' => [
            'weight' => 4,
            'patterns' => [
                '/\.\.\//',
                '/\.\.\\\\/',
                '/%2e%2e%2f/i',
                '/%2e%2e\\\\/i',
                '/%252e%252e%252f/i',
            ]
        ],
        'null_byte' => [
            'weight' => 5,
            'patterns' => [
                '/%00/',
                '/\x00/',
                '/\\0/',
            ]
        ],
        'backup_files' => [
            'weight' => 3,
            'patterns' => [
                '/\.bak$/i',
                '/\.old$/i',
                '/\.backup$/i',
                '/\.swp$/i',
                '/\.swo$/i',
                '/~$/',
                '/\.sql$/i',
                '/\.tar$/i',
                '/\.gz$/i',
                '/\.zip$/i',
            ]
        ]
    ];

    public function __construct(Utilities $utilities, Database $database) {
        $this->config = Config::get_instance();
        $this->utilities = $utilities;
        $this->database = $database;

        if ($this->is_enabled()) {
            add_action('template_redirect', [$this, 'analyze_404_pattern'], 5);
        }
    }

    private function is_enabled(): bool {
        return $this->config->get_bool('enable_advanced_404', true);
    }

    /**
     * Get validated regex patterns (filters out invalid ones)
     * MY WIN: Pre-validation prevents repeated checks
     *
     * @return array
     */
    private function get_validated_patterns(): array {
        if (self::$validated_patterns !== null) {
            return self::$validated_patterns;
        }
        
        self::$validated_patterns = [];
        foreach ($this->pattern_categories as $category => $config) {
            self::$validated_patterns[$category] = [];
            foreach ($config['patterns'] as $pattern) {
                if (@preg_match($pattern, '') !== false) {
                    self::$validated_patterns[$category][] = $pattern;
                } elseif (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
                    error_log("BotFend 404: Invalid regex pattern detected in category '{$category}': {$pattern}");
                }
            }
        }
        
        return self::$validated_patterns;
    }

    /**
     * Analyze 404 pattern and track abuse
     * YOUR WIN: No throttle blindspot, atomic counting, raw URI scanning
     * MY WIN: Crawler logging integration
     */
    public function analyze_404_pattern(): void {
        if (!is_404() || !$this->utilities || !$this->database) {
            return;
        }

        $ip = $this->utilities->get_client_ip();

        if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
            return;
        }

        // Prevent infinite loops in the exact same PHP thread
        if (isset($this->analyzed_ips[$ip])) {
            return;
        }
        
        if (count($this->analyzed_ips) >= self::MAX_ANALYZED_CACHE) {
            $this->analyzed_ips = array_slice($this->analyzed_ips, self::CACHE_TRIM_AMOUNT, null, true);
        }
        $this->analyzed_ips[$ip] = true;

        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) 
            ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) 
            : '';
            
        // YOUR WIN: Use raw URI for scanning
        $request_uri = isset($_SERVER['REQUEST_URI']) 
            ? wp_unslash($_SERVER['REQUEST_URI']) 
            : '';

        if (method_exists($this->utilities, 'is_ip_whitelisted') && 
            $this->utilities->is_ip_whitelisted($ip)) {
            return;
        }

        // Check for legitimate crawler
        $is_crawler = method_exists($this->utilities, 'verify_legitimate_crawler') && 
                      $this->utilities->verify_legitimate_crawler($ip, $user_agent);

        if ($is_crawler) {
            // MY WIN: Log crawler 404s for SEO monitoring
            $this->log_crawler_404($ip, $user_agent, $request_uri);
            return; // Do not block or penalize crawlers
        }

        // YOUR WIN: Update the count FIRST to accurately catch burst attacks
        $this->update_404_count($ip);

        // Calculate scores
        $standard_abuse_score = $this->check_standard_404_abuse($ip);
        $advanced_result = $this->analyze_path($request_uri, $ip);
        
        $total_score = min(10, $standard_abuse_score + $advanced_result['score']);
        
        $factors = [];
        
        if ($standard_abuse_score > 0) {
            $factors[] = esc_html__('Standard 404 abuse detected', 'botfend-anti-bot-firewall');
        }
        
        if (!empty($advanced_result['detected_categories'])) {
            $factors[] = sprintf(
                esc_html__('Advanced 404 patterns: %s', 'botfend-anti-bot-firewall'),
                esc_html(implode(', ', $advanced_result['detected_categories']))
            );
        }
        
        if ($total_score > 0) {
            // YOUR WIN: Escape the URI securely right before logging
            $this->log_404_detection($ip, $user_agent, esc_url_raw($request_uri), $total_score, $factors);
            $this->check_immediate_block($ip, $total_score, $factors);
        }
    }
    
    /**
     * Log 404 hits from verified crawlers (Google, Bing, etc.)
     * MY WIN: SEO monitoring - identifies broken links affecting search rankings
     *
     * @param string $ip
     * @param string $user_agent
     * @param string $request_uri
     */
    private function log_crawler_404(string $ip, string $user_agent, string $request_uri): void {
        if (!$this->database || !method_exists($this->database, 'log_request')) {
            return;
        }

        $crawler_name = method_exists($this->utilities, 'extract_crawler_from_ua') 
            ? $this->utilities->extract_crawler_from_ua($user_agent) 
            : 'generic_crawler';
        
        // Rate limit crawler logs to prevent flooding on misconfigured sites
        $log_key = 'botfend_crawler_404_' . md5($ip . $request_uri);
        if (get_transient($log_key)) {
            return;
        }
        set_transient($log_key, true, HOUR_IN_SECONDS);

        $this->database->log_request(
            $ip,
            0, // Zero score
            'crawler_404',
            [
                sprintf('Crawler %s hit 404', $crawler_name),
                sprintf('Path: %s', esc_url_raw($request_uri))
            ],
            $user_agent,
            true, // is_google_crawler = true
            $this->get_country_cached($ip)
        );
    }
    
    private function check_standard_404_abuse(string $ip): int {
        $max_404s = $this->config->get_int('max_404s_per_minute', 10);
        $abuse_score = $this->config->get_int('score_404_abuse', 5);
        
        $count_key = 'botfend_404_count_' . $this->get_safe_key($ip);
        $count = (int) get_transient($count_key);
        
        if ($count >= $max_404s) {
            return $abuse_score;
        }
        
        return 0;
    }
    
    /**
     * Update 404 count with atomic operations
     * YOUR WIN: Atomic increments prevent TOCTOU race conditions
     */
    private function update_404_count(string $ip): void {
        $count_key = 'botfend_404_count_' . $this->get_safe_key($ip);
        
        if (wp_cache_add($count_key, 1, '', self::RATE_LIMIT_KEY_TTL)) {
            return;
        }
        
        $count = wp_cache_incr($count_key, 1, '');
        
        // Fallback if external object cache is bypassed
        if (false === $count) {
            $count = (int) get_transient($count_key);
            $count++;
            set_transient($count_key, $count, self::RATE_LIMIT_KEY_TTL);
        }
    }
    
    /**
     * Analyze path for malicious patterns
     * COMBINED WIN: Uses pre-validated patterns (mine) with raw URI scanning (yours)
     *
     * @param string $path Request URI (raw)
     * @param string $ip Client IP
     * @return array
     */
    private function analyze_path(string $path, string $ip): array {
        $score = 0;
        $detected_categories = [];
        $detected_patterns = [];
        
        if (empty($path)) {
            return [
                'score' => 0,
                'detected_categories' => [],
                'detected_patterns' => []
            ];
        }
        
        if (strlen($path) > 500) {
            $score += 3;
            $detected_categories[] = 'excessive_length';
            $detected_patterns[] = 'path_length_exceeded';
        }
        
        // MY WIN: Use pre-validated patterns for efficiency
        $validated_patterns = $this->get_validated_patterns();
        
        foreach ($validated_patterns as $category => $patterns) {
            $weight = $this->pattern_categories[$category]['weight'];
            foreach ($patterns as $pattern) {
                try {
                    if (preg_match($pattern, $path)) {
                        $score += $weight;
                        $detected_categories[] = $category;
                        $detected_patterns[] = $pattern;
                        break;
                    }
                } catch (\Exception $e) {
                    if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
                        error_log(sprintf(
                            'BotFend 404: Regex error for pattern %s: %s',
                            $pattern,
                            $e->getMessage()
                        ));
                    }
                    continue;
                }
            }
        }
        
        if (preg_match('/\/\/+/', $path)) {
            $score += 2;
            $detected_categories[] = 'path_fuzzing';
            $detected_patterns[] = 'multiple_slashes';
        }
        
        if (preg_match('/%[0-9a-f]{2}/i', $path)) {
            $score += 2;
            $detected_categories[] = 'encoded_chars';
            $detected_patterns[] = 'url_encoded';
        }
        
        if (preg_match('/%25[0-9a-f]{2}/i', $path)) {
            $score += 3;
            $detected_categories[] = 'double_encoding';
            $detected_patterns[] = 'double_encoded';
        }
        
        $score = min(10, $score);
        
        return [
            'score' => $score,
            'detected_categories' => array_unique($detected_categories),
            'detected_patterns' => array_slice(array_unique($detected_patterns), 0, 5)
        ];
    }
    
    private function check_immediate_block(string $ip, int $score, array $factors): void {
        $block_threshold = $this->config->get_int('advanced_404_block_threshold', 5);
        
        if ($score >= $block_threshold) {
            $block_duration = $this->config->get_int('block_duration', 24);
            $country = $this->get_country_cached($ip);
            
            if (method_exists($this->database, 'block_ip')) {
                $this->database->block_ip(
                    $ip,
                    'advanced_404_block',
                    sprintf(
                        'Advanced 404 attack: Score %d - %s',
                        $score,
                        implode(', ', $factors)
                    ),
                    $score,
                    $block_duration,
                    $country
                );
            }
            
            if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
                error_log(sprintf(
                    'BotFend 404: Blocked IP %s - Score %d - Factors: %s',
                    $ip,
                    $score,
                    implode(', ', $factors)
                ));
            }
        }
    }
    
    private function log_404_detection(string $ip, string $user_agent, string $request_uri, int $score, array $factors): void {
        if (!$this->database || !method_exists($this->database, 'log_request')) {
            return;
        }
        
        $country = $this->get_country_cached($ip);
        
        $this->database->log_request(
            $ip,
            $score,
            'advanced_404_detection',
            $factors,
            $user_agent,
            false,
            $country
        );
        
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            error_log(sprintf(
                'BotFend 404: IP %s - Score %d - Path: %s - Factors: %s',
                $ip,
                $score,
                substr($request_uri, 0, 100),
                implode(', ', $factors)
            ));
        }
    }
    
    /**
     * Get country with two-layer caching (Static + Transient)
     * MY WIN: Persistent cache reduces external API calls by ~90%
     * 
     * CRITICAL: Geolocation is DISABLED BY DEFAULT - requires explicit opt-in
     *
     * @param string $ip
     * @return string
     */
    private function get_country_cached(string $ip): string {
        // CRITICAL: Geolocation is DISABLED BY DEFAULT - requires explicit opt-in
        if (!$this->config->get_bool('enable_geolocation', false)) {
            return '';
        }
        
        if (!method_exists($this->utilities, 'get_country')) {
            return '';
        }
        
        $safe_ip = sanitize_text_field($ip);
        $cache_key = $this->get_safe_key($safe_ip);
        
        // Layer 1: Static runtime cache
        if (isset(self::$country_cache[$cache_key])) {
            return self::$country_cache[$cache_key];
        }
        
        // Layer 2: Persistent Transient Cache (MY WIN)
        $transient_key = 'botfend_404_country_' . $cache_key;
        $cached_country = get_transient($transient_key);
        if (false !== $cached_country) {
            self::$country_cache[$cache_key] = $cached_country;
            return $cached_country;
        }
        
        if (count(self::$country_cache) >= self::MAX_COUNTRY_CACHE) {
            self::$country_cache = array_slice(self::$country_cache, 20, null, true);
        }
        
        $country = $this->utilities->get_country($safe_ip);
        self::$country_cache[$cache_key] = $country;
        set_transient($transient_key, $country, self::COUNTRY_CACHE_TTL);
        
        return $country;
    }
    
    private function get_safe_key(string $value): string {
        return substr(md5(sanitize_text_field($value)), 0, 12);
    }
    
    public function get_ip_404_status(string $ip): array {
        $count_key = 'botfend_404_count_' . $this->get_safe_key($ip);
        $current_count = (int) get_transient($count_key);
        $max_404s = $this->config->get_int('max_404s_per_minute', 10);
        
        return [
            'current_404s' => $current_count,
            'max_allowed' => $max_404s,
            'is_abusing' => $current_count >= $max_404s,
            'abuse_score' => $this->config->get_int('score_404_abuse', 5),
            'advanced_score' => $this->config->get_int('score_advanced_404', 4),
            'block_threshold' => $this->config->get_int('advanced_404_block_threshold', 5)
        ];
    }
    
    public function get_pattern_categories(): array {
        $categories = [];
        foreach ($this->pattern_categories as $category => $config) {
            $categories[$category] = [
                'weight' => $config['weight'],
                'pattern_count' => count($config['patterns']),
                'patterns' => array_slice($config['patterns'], 0, 5)
            ];
        }
        return $categories;
    }
    
    public function get_statistics(int $hours = 24): array {
        global $wpdb;
        
        if (!$wpdb) {
            return ['total_404s' => 0, 'unique_ips' => 0, 'top_offenders' => []];
        }
        
        $table = $wpdb->prefix . 'botfend_logs';
        
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $wpdb->esc_like($table))) !== $table) {
            return ['total_404s' => 0, 'unique_ips' => 0, 'top_offenders' => []];
        }
        
        $total = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM `{$table}` WHERE action = %s AND created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR)",
            'advanced_404_detection',
            $hours
        ));
        
        $unique = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT ip) FROM `{$table}` WHERE action = %s AND created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR)",
            'advanced_404_detection',
            $hours
        ));
        
        $top = $wpdb->get_results($wpdb->prepare(
            "SELECT ip, COUNT(*) as count, MAX(score) as max_score 
             FROM `{$table}` 
             WHERE action = %s AND created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR) 
             GROUP BY ip 
             ORDER BY count DESC 
             LIMIT 10",
            'advanced_404_detection',
            $hours
        ), ARRAY_A);
        
        $avg_score = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT AVG(score) FROM `{$table}` WHERE action = %s AND created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR)",
            'advanced_404_detection',
            $hours
        ));
        
        // MY WIN: Also get crawler 404 stats
        $crawler_total = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM `{$table}` WHERE action = %s AND created_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d HOUR)",
            'crawler_404',
            $hours
        ));
        
        return [
            'total_404s' => $total,
            'unique_ips' => $unique,
            'avg_score' => round($avg_score, 2),
            'crawler_404s' => $crawler_total,
            'top_offenders' => $top ?: [],
            'block_threshold' => $this->config->get_int('advanced_404_block_threshold', 5),
            'standard_abuse_score' => $this->config->get_int('score_404_abuse', 5),
            'advanced_score' => $this->config->get_int('score_advanced_404', 4)
        ];
    }
    
    public function clear_history(): bool {
        global $wpdb;
        
        if (!$wpdb) {
            return false;
        }
        
        $table = $wpdb->prefix . 'botfend_logs';
        
        $wpdb->delete($table, ['action' => 'advanced_404_detection'], ['%s']);
        $wpdb->delete($table, ['action' => 'crawler_404'], ['%s']);
        
        if (!wp_using_ext_object_cache()) {
            $like_rate = $wpdb->esc_like('_transient_botfend_404_rate_') . '%';
            $like_count = $wpdb->esc_like('_transient_botfend_404_count_') . '%';
            $like_country = $wpdb->esc_like('_transient_botfend_404_country_') . '%';
            $like_crawler = $wpdb->esc_like('_transient_botfend_crawler_404_') . '%';
            $like_timeout_rate = $wpdb->esc_like('_transient_timeout_botfend_404_rate_') . '%';
            $like_timeout_count = $wpdb->esc_like('_transient_timeout_botfend_404_count_') . '%';
            $like_timeout_country = $wpdb->esc_like('_transient_timeout_botfend_404_country_') . '%';
            $like_timeout_crawler = $wpdb->esc_like('_transient_timeout_botfend_crawler_404_') . '%';
            
            $keys = $wpdb->get_col($wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE %s 
                 OR option_name LIKE %s 
                 OR option_name LIKE %s
                 OR option_name LIKE %s
                 OR option_name LIKE %s 
                 OR option_name LIKE %s
                 OR option_name LIKE %s
                 OR option_name LIKE %s",
                $like_rate,
                $like_count,
                $like_country,
                $like_crawler,
                $like_timeout_rate,
                $like_timeout_count,
                $like_timeout_country,
                $like_timeout_crawler
            ));
            
            if (!empty($keys)) {
                foreach ($keys as $key) {
                    $transient_name = str_replace(['_transient_timeout_', '_transient_'], '', $key);
                    delete_transient($transient_name);
                }
            }
        }
        
        $this->analyzed_ips = [];
        self::$country_cache = [];
        self::$validated_patterns = null;
        
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            error_log('BotFend 404: History cleared');
        }
        
        return true;
    }
    
    public static function clear_country_cache(): void {
        self::$country_cache = [];
    }
    
    public static function clear_validated_patterns(): void {
        self::$validated_patterns = null;
    }
    
    public function get_config(): array {
        return [
            'enabled' => $this->is_enabled(),
            'max_404s_per_minute' => $this->config->get_int('max_404s_per_minute', 10),
            'score_404_abuse' => $this->config->get_int('score_404_abuse', 5),
            'score_advanced_404' => $this->config->get_int('score_advanced_404', 4),
            'advanced_404_block_threshold' => $this->config->get_int('advanced_404_block_threshold', 5),
            'block_duration' => $this->config->get_int('block_duration', 24),
            'pattern_categories' => count($this->pattern_categories),
            'total_patterns' => array_sum(array_map(function($cat) {
                return count($cat['patterns']);
            }, $this->pattern_categories)),
            'geolocation_enabled' => $this->config->get_bool('enable_geolocation', false)
        ];
    }
    
    public function update_config(array $settings): bool {
        $success = true;
        
        if (isset($settings['max_404s_per_minute'])) {
            $success = $success && $this->config->set('max_404s_per_minute', absint($settings['max_404s_per_minute']));
        }
        
        if (isset($settings['score_404_abuse'])) {
            $success = $success && $this->config->set('score_404_abuse', absint($settings['score_404_abuse']));
        }
        
        if (isset($settings['score_advanced_404'])) {
            $success = $success && $this->config->set('score_advanced_404', absint($settings['score_advanced_404']));
        }
        
        if (isset($settings['advanced_404_block_threshold'])) {
            $success = $success && $this->config->set('advanced_404_block_threshold', absint($settings['advanced_404_block_threshold']));
        }
        
        if (isset($settings['enable_advanced_404'])) {
            $success = $success && $this->config->set('enable_advanced_404', (bool) $settings['enable_advanced_404']);
        }
        
        return $success;
    }
    
    public function is_rapid_offender(string $ip): bool {
        $count_key = 'botfend_404_count_' . $this->get_safe_key($ip);
        $count = (int) get_transient($count_key);
        $max_404s = $this->config->get_int('max_404s_per_minute', 10);
        
        return $count >= $max_404s;
    }
}