<?php
/**
 * Centralized logging service for BotFend Anti-Bot Firewall
 *
 * @package BotFend\Logging
 * @version 4.8.3 - Rebranded from SafeBot, file storage location fixed
 */

namespace BotFend\Logging;

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

defined('ABSPATH') || exit;

/**
 * Class Logger
 *
 * Handles centralized logging with multiple severity levels and queue processing.
 */
class Logger {

    private $config;
    private $database;
    private $log_queue = [];
    const MAX_QUEUE_SIZE = 1000;

    const LEVEL_DEBUG = 'debug';
    const LEVEL_INFO = 'info';
    const LEVEL_WARNING = 'warning';
    const LEVEL_ERROR = 'error';
    const LEVEL_CRITICAL = 'critical';

    private $action_groups = [
        'rate_limited' => 'rate',
        'challenged' => 'challenge',
        'waf_detection' => 'waf',
        'blocked' => 'block',
        '404_abuse' => 'abuse',
        'honeypot_triggered' => 'honeypot',
        'js_challenge' => 'challenge',
        'tor_blocked' => 'tor',
        'tor_challenge' => 'tor',
        'crawler_impersonation' => 'impersonation',
        'sql_injection' => 'waf',
        'xss' => 'waf',
        'file_inclusion' => 'waf',
        'brute_force' => 'attack',
        'session_hijack' => 'attack',
        'sampled' => 'sampled',
        'allowed' => 'allowed',
        'auto_unblock' => 'unblock',
        'google_crawler' => 'crawler',
        'known_crawler' => 'crawler',
    ];

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

        // Process queue at shutdown for better performance
        add_action('shutdown', [$this, 'process_log_queue']);

        // Process queue on admin pages immediately
        if (is_admin()) {
            add_action('admin_init', [$this, 'process_log_queue_immediate']);
        }
    }

    /**
     * Get safe data directory path using WordPress uploads directory
     * FIXED: No longer uses WP_CONTENT_DIR directly
     *
     * @return string
     */
    private function get_data_dir(): string {
        $upload_dir = wp_upload_dir();
        $plugin_dir = $upload_dir['basedir'] . '/botfend-anti-bot-firewall/';
        
        if (!file_exists($plugin_dir)) {
            wp_mkdir_p($plugin_dir);
            
            // Add index.php for security
            if (!file_exists($plugin_dir . 'index.php')) {
                $index_content = "<?php\n// Silence is golden.\n";
                file_put_contents($plugin_dir . 'index.php', $index_content);
            }
            
            // Add .htaccess to prevent direct access
            if (!file_exists($plugin_dir . '.htaccess')) {
                $htaccess_content = "Deny from all\n";
                file_put_contents($plugin_dir . '.htaccess', $htaccess_content);
            }
        }
        
        return $plugin_dir;
    }

    public function info(string $message, array $context = []): void {
        $this->log(self::LEVEL_INFO, $message, $context);
    }

    public function debug(string $message, array $context = []): void {
        if (!$this->config->get_bool('debug_mode', false)) {
            return;
        }
        $this->log(self::LEVEL_DEBUG, $message, $context);
    }

    public function warning(string $message, array $context = []): void {
        $this->log(self::LEVEL_WARNING, $message, $context);
    }

    public function error(string $message, array $context = []): void {
        $this->log(self::LEVEL_ERROR, $message, $context);
    }

    public function critical(string $message, array $context = []): void {
        $this->log(self::LEVEL_CRITICAL, $message, $context);
    }

    private function log(string $level, string $message, array $context = []): void {
        if (defined('WP_DEBUG') && WP_DEBUG && $this->config->get_bool('debug_mode', false)) {
            error_log(sprintf(
                '[BotFend %s] %s %s',
                strtoupper($level),
                $message,
                !empty($context) ? wp_json_encode($context) : ''
            ));
        }

        if ($this->should_log_to_database($level)) {
            $this->queue_system_log($level, $message, $context);
        }
    }

    private function should_log_to_database(string $level): bool {
        $critical_levels = [self::LEVEL_ERROR, self::LEVEL_CRITICAL, self::LEVEL_WARNING];
        return in_array($level, $critical_levels, true);
    }

    private function queue_system_log(string $level, string $message, array $context = []): void {
        $log_key = 'system_' . md5($level . $message . wp_json_encode($context));

        if (count($this->log_queue) >= self::MAX_QUEUE_SIZE) {
            $this->process_log_queue();
        }

        $this->log_queue[$log_key] = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => time(),
            'source' => $context['source'] ?? 'system',
            'ip' => $context['ip'] ?? $this->get_client_ip(),
            'user_agent' => $context['user_agent'] ?? $this->get_user_agent(),
        ];

        if ($level === self::LEVEL_CRITICAL) {
            $this->process_log_immediate($log_key);
        }
    }

    public function queue_log($ip, $score, $action, $factors = [], $user_agent = '', $is_google_crawler = false, $country = null, $throttle_seconds = null): bool {
        if (!$this->validate_ip($ip)) {
            return false;
        }

        if ($throttle_seconds === null) {
            $throttle_seconds = $this->get_throttle_seconds();
        }

        if (count($this->log_queue) >= self::MAX_QUEUE_SIZE) {
            $this->process_log_queue();
        }

        $log_key = $this->get_log_key($ip, $action, $score);

        if ($this->should_throttle($log_key, $throttle_seconds)) {
            $this->merge_into_queue($log_key, [
                'ip' => $ip,
                'score' => $score,
                'action' => $action,
                'factors' => $factors,
                'user_agent' => $user_agent,
                'is_google_crawler' => $is_google_crawler,
                'country' => $country,
                'timestamp' => time(),
                'throttle_seconds' => $throttle_seconds
            ]);
            return true;
        }

        $this->log_queue[$log_key] = [
            'ip' => $ip,
            'score' => $score,
            'action' => $action,
            'factors' => $factors,
            'user_agent' => $user_agent,
            'is_google_crawler' => $is_google_crawler,
            'country' => $country,
            'timestamp' => time(),
            'throttle_seconds' => $throttle_seconds
        ];

        if ($this->is_critical_action($action) || $score >= 8) {
            $this->process_log_immediate($log_key);
        }

        return true;
    }

    private function process_log_immediate(string $log_key): void {
        if (!isset($this->log_queue[$log_key])) {
            return;
        }

        $log = $this->log_queue[$log_key];
        
        if (isset($log['level'])) {
            $this->write_system_log($log);
        } else {
            $this->write_request_log($log);
        }

        unset($this->log_queue[$log_key]);
    }

    public function process_log_queue(): void {
        if (empty($this->log_queue)) {
            return;
        }

        // Copy queue to prevent infinite loops if an error occurs during processing
        $queue_to_process = $this->log_queue;
        $this->log_queue = [];

        foreach ($queue_to_process as $log_key => $log) {
            if (isset($log['level'])) {
                $this->write_system_log($log);
            } else {
                $this->write_request_log($log);
            }
        }
    }

    public function process_log_queue_immediate(): void {
        $this->process_log_queue();
    }

    private function write_system_log(array $log): bool {
        global $wpdb;

        $table = $wpdb->prefix . 'botfend_system_logs';
        
        // The table should exist from activation; if not, fail gracefully
        $result = $wpdb->insert(
            $table,
            [
                'level' => sanitize_text_field($log['level']),
                'message' => sanitize_textarea_field($log['message']),
                'context' => wp_json_encode($log['context']),
                'source' => sanitize_text_field($log['source']),
                'ip' => sanitize_text_field($log['ip']),
                'user_agent' => sanitize_text_field($log['user_agent']),
                'created_at' => current_time('mysql', true),
            ],
            ['%s', '%s', '%s', '%s', '%s', '%s', '%s']
        );

        if ($result === false) {
            error_log('BotFend Logger: Failed to write system log - ' . $wpdb->last_error);
            return false;
        }

        return true;
    }

    private function write_request_log(array $log): bool {
        if (!$this->database || !method_exists($this->database, 'log_request')) {
            $this->debug('Database not available for logging', $log);
            return false;
        }

        $factors_str = $this->prepare_factors($log['factors']);

        $result = $this->database->log_request(
            sanitize_text_field($log['ip']),
            absint($log['score']),
            sanitize_text_field($log['action']),
            $factors_str,
            sanitize_text_field($log['user_agent']),
            (bool) $log['is_google_crawler'],
            sanitize_text_field($log['country']),
            absint($log['throttle_seconds'])
        );

        if (false === $result) {
            $this->debug('Failed to log request to database', $log);
        }

        return $result;
    }

    private function should_throttle(string $log_key, int $throttle_seconds): bool {
        if ($throttle_seconds <= 0) {
            return false;
        }

        $transient_key = 'botfend_log_throttle_' . $log_key;
        $last_time = get_transient($transient_key);

        if (false === $last_time) {
            set_transient($transient_key, time(), $throttle_seconds);
            return false;
        }

        return (time() - (int) $last_time) < $throttle_seconds;
    }

    private function merge_into_queue(string $log_key, array $log): void {
        if (!isset($this->log_queue[$log_key])) {
            $this->log_queue[$log_key] = $log;
            return;
        }

        $existing = $this->log_queue[$log_key];
        $merged_factors = $this->merge_factors($existing['factors'], $log['factors']);
        $merged_score = max($existing['score'], $log['score']);
        $merged_user_agent = !empty($log['user_agent']) ? $log['user_agent'] : $existing['user_agent'];

        $this->log_queue[$log_key] = [
            'ip' => $log['ip'],
            'score' => $merged_score,
            'action' => $log['action'],
            'factors' => $merged_factors,
            'user_agent' => $merged_user_agent,
            'is_google_crawler' => $log['is_google_crawler'] || $existing['is_google_crawler'],
            'country' => $log['country'] ?? $existing['country'],
            'timestamp' => time(),
            'throttle_seconds' => $log['throttle_seconds']
        ];
    }

    private function get_log_key(string $ip, string $action, int $score): string {
        $action_group = $this->get_action_group($action);
        $score_group = $this->get_score_group($score);
        return 'log_' . md5(sanitize_text_field($ip . '_' . $action_group . '_' . $score_group));
    }

    private function get_action_group(string $action): string {
        return $this->action_groups[$action] ?? $action;
    }

    private function get_score_group(int $score): string {
        if ($score >= 9) return 'critical';
        if ($score >= 7) return 'high';
        if ($score >= 5) return 'medium';
        if ($score >= 3) return 'low';
        return 'safe';
    }

    private function is_critical_action(string $action): bool {
        $critical_actions = [
            'blocked', 'tor_blocked', 'sql_injection',
            'xss', 'file_inclusion', 'brute_force', 'session_hijack',
        ];
        return in_array($action, $critical_actions, true);
    }

    private function prepare_factors($factors): string {
        if (is_array($factors)) {
            $factors = array_filter(array_unique($factors));
            return implode(', ', array_map('sanitize_text_field', $factors));
        } elseif (is_string($factors)) {
            return sanitize_text_field($factors);
        }
        return '';
    }

    private function merge_factors($existing, $new): array {
        if (empty($existing)) {
            return is_array($new) ? $new : [$new];
        }

        if (empty($new)) {
            return is_array($existing) ? $existing : [$existing];
        }

        $existing_array = is_array($existing) ? $existing : [$existing];
        $new_array = is_array($new) ? $new : [$new];

        $merged = array_merge($existing_array, $new_array);
        return array_filter(array_unique($merged));
    }

    private function validate_ip(string $ip): bool {
        if (empty($ip) || !is_string($ip)) {
            return false;
        }

        $ip = trim(sanitize_text_field($ip));

        $dangerous_patterns = [
            '--', ';', '/*', '*/', 'UNION', 'SELECT', 'INSERT',
            'DELETE', 'UPDATE', 'DROP', 'CREATE', 'ALTER',
        ];

        $ip_upper = strtoupper($ip);
        foreach ($dangerous_patterns as $pattern) {
            if (strpos($ip_upper, $pattern) !== false) {
                return false;
            }
        }

        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    private function get_client_ip(): string {
        $headers = [
            'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 
            'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'
        ];

        foreach ($headers as $header) {
            if (isset($_SERVER[$header])) {
                $ip = sanitize_text_field(wp_unslash($_SERVER[$header]));
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }

        return '0.0.0.0';
    }

    private function get_user_agent(): string {
        return isset($_SERVER['HTTP_USER_AGENT'])
            ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT']))
            : '';
    }

    private function get_throttle_seconds(): int {
        return $this->config->get_int('log_throttle_seconds', 10);
    }

    public function debug_log(string $message, $data = null, string $level = 'info'): void {
        $this->debug($message, ['data' => $data, 'original_level' => $level]);
    }

    public function get_log_statistics(int $hours = 24): array {
        $cache_key = 'botfend_log_stats_' . $hours;
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $stats = [
            'total_logs' => 0,
            'unique_ips' => 0,
            'by_level' => [],
            'by_source' => [],
        ];

        set_transient($cache_key, $stats, 300);
        return $stats;
    }

    public function clear_log_cache(string $ip = ''): void {
        global $wpdb;

        // Only query DB if not using external object cache to prevent desync
        if (!wp_using_ext_object_cache()) {
            $likes = [];
            if (!empty($ip) && $this->validate_ip($ip)) {
                $ip_hash = md5($ip);
                // Escape the SQL wildcards
                $like_str = $wpdb->esc_like('_transient_botfend_log_throttle_log_' . $ip_hash) . '%';
                $likes[] = $wpdb->prepare("option_name LIKE %s", $like_str);
            } else {
                // Escape the SQL wildcards
                $like_stats = $wpdb->esc_like('_transient_botfend_log_stats_') . '%';
                $like_throttle = $wpdb->esc_like('_transient_botfend_log_throttle_') . '%';
                
                $likes[] = $wpdb->prepare("option_name LIKE %s", $like_stats);
                $likes[] = $wpdb->prepare("option_name LIKE %s", $like_throttle);
            }

            $where = implode(' OR ', $likes);
            
            // Fetch keys safely to avoid LONGTEXT option_value full table scans
            $keys = $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE {$where}");
            
            if (!empty($keys)) {
                foreach ($keys as $key) {
                    $transient_name = str_replace('_transient_', '', $key);
                    if (strpos($transient_name, 'timeout_') !== 0) {
                        delete_transient($transient_name);
                    }
                }
            }
        }

        $this->log_queue = [];
    }

    public function get_current_throttle_setting(): int {
        return $this->get_throttle_seconds();
    }

    public function get_current_dedup_setting(): int {
        return $this->config->get_int('log_deduplication_window', 300);
    }
}