<?php

namespace WeeConnectPay\Integrations;

/**
 * Class Logger
 * Handles all logging functionality for WeeConnectPay
 * 
 * @package WeeConnectPay\Integrations
 */
class Logger {
    /**
     * @var string
     */
    private string $logPrefix = '[WeeConnectPay] ';

    /**
     * Maximum log file size in bytes (50MB)
     * @var int
     */
    private const MAX_LOG_SIZE = 52428800;

    /**
     * Number of logs per page
     * @var int
     */
    private const LOGS_PER_PAGE = 100;

    /**
     * Valid log levels - now includes critical level for security messages
     * @var array
     */
    private const VALID_LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'critical'];

    /**
     * Base log filename (without hash)
     * @var string
     */
    private const LOG_FILENAME_BASE = 'weeconnectpay-debug';

    /**
     * @var string|null Path to the log file
     */
    private ?string $logFilePath = null;

    /**
     * @var string|null Hashed log filename for security
     */
    private ?string $hashedLogFilename = null;

    /**
     * Logger constructor.
     */
    public function __construct() {
        $this->initializeLogFile();
    }

    /**
     * Create a new Logger instance
     * 
     * @return Logger
     */
    public static function create(): Logger {
        return new self();
    }

    /**
     * Check if debug mode is enabled
     * 
     * @return bool
     */
    private function isDebugEnabled(): bool {
        $integrationSettings = new IntegrationSettings();
        return $integrationSettings->getDebugModeOrDefault();
    }

    /**
     * Sanitize and validate log message
     * 
     * @param mixed $message The message to sanitize
     * @return string The sanitized message
     */
    private function sanitizeLogMessage($message): string {
        if ($message === null) {
            return '[Empty: null message]';
        }
        
        if (is_array($message) || is_object($message)) {
            if (empty($message)) {
                return '[Empty: ' . (is_array($message) ? 'array' : 'object') . ']';
            }
            
            // Protect against deeply nested structures
            $depth = 0;
            $message = $this->sanitizeStructure($message, $depth);
            
            // For arrays/objects, encode to JSON with proper formatting
            $message = json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if ($message === false) {
                return '[Error: Unable to encode data - ' . json_last_error_msg() . ']';
            }
        } else {
            // Convert to string and escape our control sequences
            $message = (string)$message;
            if (trim($message) === '') {
                return '[Empty: blank message]';
            }
            $message = str_replace(['[WeeConnectPay]', '[DEBUG]', '[INFO]', '[WARNING]', '[ERROR]', '[CRITICAL]'], 
                                 ['[escaped-WeeConnectPay]', '[escaped-DEBUG]', '[escaped-INFO]', '[escaped-WARNING]', '[escaped-ERROR]', '[escaped-CRITICAL]'], 
                                 $message);
        }
        
        // Remove only dangerous control characters, preserve normal whitespace
        $originalLength = mb_strlen($message);
        $message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]|\x7F/u', '', $message);
        $newLength = mb_strlen($message);
        
        if ($newLength === 0 && $originalLength > 0) {
            return '[Empty: message contained only control characters]';
        }
        
        // Limit message length to prevent DOS
        if (mb_strlen($message) > 10000) {
            return substr($message, 0, 10000) . ' [Truncated: message exceeded maximum length]';
        }
        
        return $message;
    }

    /**
     * Sanitize nested structures with depth limit and size checks
     * 
     * @param mixed $data The data to sanitize
     * @param int &$depth Current depth
     * @return mixed Sanitized data
     */
    private function sanitizeStructure($data, int &$depth, int $maxDepth = 5): mixed {
        $depth++;
        
        // Prevent deeply nested structures
        if ($depth > $maxDepth) {
            return '[Truncated: Maximum nesting depth exceeded]';
        }

        if (is_array($data)) {
            if (empty($data)) {
                return '[Empty: array]';
            }
            
            $result = [];
            // Limit number of elements
            $count = 0;
            foreach ($data as $key => $value) {
                if ($count++ > 100) { // Limit array size
                    $result['[truncated]'] = '[Truncated: Array exceeded 100 elements]';
                    break;
                }
                // Sanitize keys to prevent injection
                $safeKey = is_string($key) ? 
                    substr(preg_replace('/[^\w\-\.]/', '_', $key), 0, 64) : // Limit key length and characters
                    $key;
                $result[$safeKey] = $this->sanitizeStructure($value, $depth, $maxDepth);
            }
            return $result;
        } elseif (is_object($data)) {
            if (empty((array)$data)) {
                return '[Empty: object]';
            }
            // Convert object to array to apply same sanitization
            return $this->sanitizeStructure((array)$data, $depth, $maxDepth);
        } elseif (is_string($data)) {
            // Limit string length and escape control sequences
            if (mb_strlen($data) > 1000) {
                return substr($data, 0, 1000) . ' [Truncated: string exceeded 1000 characters]';
            }
            return $data === '' ? '[Empty: string]' : $data;
        }
        
        // Return other types as-is (numbers, booleans, null)
        return $data ?? '[Empty: null]';
    }

    /**
     * Validate log level
     * 
     * @param string $level The log level to validate
     * @return string The validated log level
     */
    private function validateLogLevel(string $level): string {
        $level = strtolower(trim($level));
        return in_array($level, self::VALID_LOG_LEVELS, true) ? $level : 'debug';
    }

    /**
     * Log a message with optional force logging for critical messages
     * 
     * @param mixed $message The message to log
     * @param string $level The log level (debug, info, warning, error, critical)
     * @param bool $force Force logging regardless of debug mode (used for critical level)
     * @return void
     */
    public function log($message, string $level = 'debug', bool $force = false): void {
        try {
            // Force logging for critical level or if explicitly forced
            if (!$force && $level !== 'critical' && !$this->isDebugEnabled()) {
                return;
            }

            // Sanitize and validate inputs
            $message = $this->sanitizeLogMessage($message);
            $level = $this->validateLogLevel($level);

            $logMessage = $this->logPrefix . '[' . strtoupper($level) . '] ' . $message;
            
            // Try to write to our custom log file first
            $written = $this->writeToLogFile($logMessage);
            
            // If writing to our file fails, or if it's an error/critical level message,
            // also write to WordPress error_log as a fallback/additional logging
            if (!$written || in_array($level, ['error', 'critical'])) {
                error_log($logMessage);
            }
        } catch (\Throwable $e) {
            // If logging fails, use basic error_log as fallback
            error_log($this->logPrefix . 'Logging failed: ' . esc_html($e->getMessage()));
        }
    }

    /**
     * Debug level log
     * 
     * @param mixed $message
     * @return void
     */
    public function debug($message): void {
        $this->log($message, 'debug');
    }

    /**
     * Info level log
     * 
     * @param mixed $message
     * @return void
     */
    public function info($message): void {
        $this->log($message, 'info');
    }

    /**
     * Warning level log
     * 
     * @param mixed $message
     * @return void
     */
    public function warning($message): void {
        $this->log($message, 'warning');
    }

    /**
     * Error level log
     * 
     * @param mixed $message
     * @return void
     */
    public function error($message): void {
        $this->log($message, 'error');
    }

    /**
     * Critical level log - always logged regardless of debug mode
     * 
     * @param mixed $message
     * @return void
     */
    public function critical($message): void {
        $this->log($message, 'critical', true);
    }

    /**
     * Get all logs from our custom log file
     * Only returns logs with our prefix
     * 
     * @param int $limit Maximum number of lines to return
     * @return array
     */
    public function getLogs(int $limit = 1000): array {
        try {
            if (!$this->isDebugEnabled()) {
                return [];
            }

            // Validate limit
            $limit = max(1, min((int)$limit, 1000));

            if (!$this->logFilePath || !file_exists($this->logFilePath) || !is_readable($this->logFilePath)) {
                return [];
            }

            // Check file size to prevent memory issues
            $fileSize = filesize($this->logFilePath);
            if ($fileSize === false || $fileSize > self::MAX_LOG_SIZE) {
                LogService::warning('Log file too large or unreadable');
                return [];
            }

            $logs = [];
            $handle = fopen($this->logFilePath, 'r');
            if ($handle) {
                while (($line = fgets($handle)) !== false) {
                    // Only process lines with our prefix
                    if (strpos($line, $this->logPrefix) !== false) {
                        // Sanitize the log line before adding it
                        $sanitizedLine = $this->sanitizeLogLine($line);
                        if ($sanitizedLine !== '') {
                            array_unshift($logs, $sanitizedLine);
                            if (count($logs) >= $limit) {
                                break;
                            }
                        }
                    }
                }
                fclose($handle);
            }

            return $logs;
        } catch (\Throwable $e) {
            // Keep using error_log here as fallback since this is in the logger itself
            error_log($this->logPrefix . 'Failed to get logs: ' . esc_html($e->getMessage()));
            return [];
        }
    }

    /**
     * Sanitize a log line for display
     * 
     * @param string $line The log line to sanitize
     * @return string The sanitized log line
     */
    private function sanitizeLogLine(string $line): string {
        // Only process our actual log lines
        if (strpos($line, $this->logPrefix) === false) {
            return '[Filtered: not a WeeConnectPay log entry]';
        }
        
        // Remove only dangerous control characters, preserve normal whitespace
        $originalLength = mb_strlen($line);
        $line = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]|\x7F/u', '', $line);
        $newLength = mb_strlen($line);
        
        if ($newLength === 0 && $originalLength > 0) {
            return '[Filtered: line contained only control characters]';
        }
        
        // Convert to UTF-8 if not already
        if (!mb_check_encoding($line, 'UTF-8')) {
            $line = mb_convert_encoding($line, 'UTF-8', 'auto');
            if ($line === false) {
                return '[Error: Invalid UTF-8 encoding]';
            }
        }
        
        // Extract the actual log message after our prefix
        $prefixPos = strpos($line, $this->logPrefix);
        if ($prefixPos !== false) {
            $line = substr($line, $prefixPos);
        }
        
        // Unescape our legitimate log markers
        $line = str_replace(['[escaped-WeeConnectPay]', '[escaped-DEBUG]', '[escaped-INFO]', '[escaped-WARNING]', '[escaped-ERROR]', '[escaped-CRITICAL]'],
                          ['[WeeConnectPay]', '[DEBUG]', '[INFO]', '[WARNING]', '[ERROR]', '[CRITICAL]'],
                          $line);
        
        $trimmed = trim($line);
        if ($trimmed === '') {
            return '[Empty: blank line]';
        }
        
        return $trimmed;
    }

    /**
     * Check if a string is valid JSON
     * 
     * @param string $string The string to check
     * @return bool Whether the string is valid JSON
     */
    private function isJson(string $string): bool {
        json_decode($string);
        return json_last_error() === JSON_ERROR_NONE;
    }

    /**
     * Get logs with filtering and pagination
     * 
     * @param array $filters Array of filters (level, date_from, date_to)
     * @param int $page Page number (1-based)
     * @param int $per_page Number of logs per page
     * @return array Array containing logs and pagination info
     */
    public function getFilteredLogs(array $filters = [], int $page = 1, int $per_page = 10): array {
        try {
            if (!$this->isDebugEnabled()) {
                return $this->emptyLogsResponse($per_page);
            }

            if (!$this->logFilePath || !file_exists($this->logFilePath) || !is_readable($this->logFilePath)) {
                return $this->emptyLogsResponse($per_page);
            }

            // Check file size
            $fileSize = filesize($this->logFilePath);
            if ($fileSize === false || $fileSize > self::MAX_LOG_SIZE) {
                $this->rotateLogFile($this->logFilePath);
            }

            // Initialize variables
            $logs = [];
            $totalLogs = 0;
            $filteredLogs = [];

            // Read and filter logs
            $handle = fopen($this->logFilePath, 'r');
            if ($handle) {
                while (($line = fgets($handle)) !== false) {
                    if (strpos($line, $this->logPrefix) !== false) {
                        $log = $this->parseLine($line);
                        if ($this->matchesFilters($log, $filters)) {
                            array_unshift($filteredLogs, $log);
                            $totalLogs++;
                        }
                    }
                }
                fclose($handle);
            }

            // No need to sort since we're already adding newest first
            // Calculate pagination
            $totalPages = ceil($totalLogs / $per_page);
            $page = max(1, min($page, $totalPages));
            $offset = ($page - 1) * $per_page;
            
            // Get logs for current page
            $logs = array_slice($filteredLogs, $offset, $per_page);

            return [
                'logs' => $logs,
                'pagination' => [
                    'current_page' => $page,
                    'total_pages' => $totalPages,
                    'total_logs' => $totalLogs,
                    'per_page' => $per_page
                ]
            ];
        } catch (\Throwable $e) {
            // Keep using error_log here as fallback since this is in the logger itself
            error_log($this->logPrefix . 'Failed to get filtered logs: ' . esc_html($e->getMessage()));
            return $this->emptyLogsResponse($per_page);
        }
    }

    /**
     * Parse a log line into structured data
     * 
     * @param string $line Log line to parse
     * @return array Structured log data
     */
    private function parseLine(string $line): array {
        $timestamp = strtotime(substr($line, 0, 19));
        preg_match('/\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]/', $line, $levelMatches);
        $level = isset($levelMatches[1]) ? strtolower($levelMatches[1]) : 'debug';
        
        return [
            'timestamp' => $timestamp,
            'datetime' => date('Y-m-d H:i:s', $timestamp),
            'level' => $level,
            'message' => $this->sanitizeLogLine($line),
            'raw' => $line
        ];
    }

    /**
     * Check if a log entry matches the given filters
     * 
     * @param array $log Log entry
     * @param array $filters Filters to apply
     * @return bool Whether the log matches the filters
     */
    private function matchesFilters(array $log, array $filters): bool {
        // Level filter
        if (!empty($filters['level']) && $log['level'] !== $filters['level']) {
            return false;
        }

        // Date range filter
        if (!empty($filters['date_from'])) {
            $fromTimestamp = strtotime($filters['date_from']);
            if ($fromTimestamp && $log['timestamp'] < $fromTimestamp) {
                return false;
            }
        }

        if (!empty($filters['date_to'])) {
            $toTimestamp = strtotime($filters['date_to']);
            if ($toTimestamp && $log['timestamp'] > $toTimestamp) {
                return false;
            }
        }

        return true;
    }

    /**
     * Rotate the log file if it exceeds the maximum size
     * 
     * @param string $logFile Path to log file
     * @return void
     */
    private function rotateLogFile(string $logFile): void {
        try {
            $backupFile = $logFile . '.' . date('Y-m-d-H-i-s') . '.bak';
            if (copy($logFile, $backupFile)) {
                // Clear the original file
                file_put_contents($logFile, '');
                // Keep only the last 5 backup files
                $backups = glob($logFile . '.*.bak');
                if (count($backups) > 5) {
                    usort($backups, function($a, $b) {
                        return filemtime($b) - filemtime($a);
                    });
                    $oldBackups = array_slice($backups, 5);
                    foreach ($oldBackups as $old) {
                        unlink($old);
                    }
                }
            }
        } catch (\Throwable $e) {
            // Keep using error_log here as fallback since this is in the logger itself
            error_log($this->logPrefix . 'Failed to rotate log file: ' . esc_html($e->getMessage()));
        }
    }

    /**
     * Get empty logs response structure
     * 
     * @param int $per_page Number of logs per page
     * @return array Empty logs response
     */
    private function emptyLogsResponse(int $per_page = 10): array {
        return [
            'logs' => [],
            'pagination' => [
                'current_page' => 1,
                'total_pages' => 0,
                'total_logs' => 0,
                'per_page' => $per_page
            ]
        ];
    }

    /**
     * Get available log levels
     * 
     * @return array Array of valid log levels
     */
    public function getLogLevels(): array {
        return self::VALID_LOG_LEVELS;
    }

    /**
     * Gather system information for logging context
     * 
     * @return string Formatted system information
     */
    private function getSystemInformation(): string {
        try {
            global $wp_version;
            
            $output = [];
            $output[] = "=== WeeConnectPay System Information ===";
            $output[] = "Generated: " . date('Y-m-d H:i:s T');
            $output[] = "";
            
            // Core Information
            $output[] = "--- Core Versions ---";
            $output[] = sprintf("WordPress Version: %s", $wp_version);
            if (class_exists('WooCommerce')) {
                $output[] = sprintf("WooCommerce Version: %s", WC()->version);
            }
            $output[] = sprintf("PHP Version: %s", PHP_VERSION);
            $output[] = "";
            
            // Site Information
            $output[] = "--- Site Information ---";
            $output[] = sprintf("Site URL: %s", get_site_url());
            $integrationSettings = new IntegrationSettings();

            // Integration UUID
            try {
                $output[] = sprintf("Integration UUID: %s", $integrationSettings->getIntegrationUuid());
            } catch (\Throwable $e) {
                $output[] = "Integration UUID: Not available";
            }

            // Merchant Information
            try {
                $merchant = $integrationSettings->getCloverMerchant();
                $output[] = sprintf("Merchant Name: %s", $merchant->getName());
                $output[] = sprintf("Merchant ID: %s", $merchant->getUuid());
            } catch (\Throwable $e) {
                $output[] = "Merchant Name: Not available";
                $output[] = "Merchant ID: Not available";
            }

            // Employee Information
            try {
                $employee = $integrationSettings->getCloverEmployee();
                $output[] = sprintf("Employee Name: %s", $employee->getName());
                $output[] = sprintf("Employee ID: %s", $employee->getUuid());
            } catch (\Throwable $e) {
                $output[] = "Employee Name: Not available";
                $output[] = "Employee ID: Not available";
            }

            $output[] = "";
            
            // Dependencies
            $output[] = "--- Dependencies ---";
            $dependencies = [
                new \WeeConnectPay\Dependency('WooCommerce', [3, 0, 0]),
                new \WeeConnectPay\Dependency('WordPress', [5, 0, 0]),
                new \WeeConnectPay\Dependency('PHP', [7, 4, 0]),
                new \WeeConnectPay\Dependency('WPDB_PREFIX', []),
                new \WeeConnectPay\Dependency('PERMALINK', []),
            ];
            
            foreach ($dependencies as $dependency) {
                $validator = new \WeeConnectPay\Validators\DependencyValidator($dependency);
                try {
                    $version = [];
                    switch ($dependency->name) {
                        case 'WooCommerce':
                            if (class_exists('WooCommerce')) {
                                $version = array_map('intval', explode('.', WC()->version));
                            }
                            break;
                        case 'WordPress':
                            $version = array_map('intval', explode('.', $wp_version));
                            break;
                        case 'PHP':
                            $version = array_map('intval', explode('.', PHP_VERSION));
                            break;
                        case 'WPDB_PREFIX':
                            global $wpdb;
                            $version = [$wpdb->prefix ? 1 : 0];
                            $output[] = sprintf("%s: %s (Required: non-empty, Current: '%s')",
                                $dependency->name,
                                $wpdb->prefix ? "OK" : "FAIL",
                                $wpdb->prefix
                            );
                            continue 2; // Skip the default output for this dependency
                        case 'PERMALINK':
                            $permalink_structure = get_option('permalink_structure');
                            $version = [$permalink_structure !== '' ? 1 : 0];
                            $output[] = sprintf("%s: %s (Required: non-plain, Current: %s)",
                                $dependency->name,
                                $permalink_structure !== '' ? "OK" : "FAIL",
                                $permalink_structure === '' ? "plain" : $permalink_structure
                            );
                            continue 2; // Skip the default output for this dependency
                    }
                    $validator->validate($version);
                    $output[] = sprintf("%s: OK (Required: %s, Current: %s)",
                        $dependency->name,
                        implode('.', $dependency->minVer),
                        implode('.', $version)
                    );
                } catch (\Throwable $e) {
                    $output[] = sprintf("%s: FAIL - %s",
                        $dependency->name,
                        $e->getMessage()
                    );
                }
            }
            $output[] = "";
            
            // Active Plugins
            $output[] = "--- Active Plugins ---";
            if (!function_exists('get_plugins')) {
                require_once ABSPATH . 'wp-admin/includes/plugin.php';
            }
            $active_plugins = get_option('active_plugins');
            $all_plugins = get_plugins();
            
            foreach ($active_plugins as $plugin) {
                if (isset($all_plugins[$plugin])) {
                    $plugin_data = $all_plugins[$plugin];
                    $output[] = sprintf("%s: %s",
                        $plugin_data['Name'],
                        $plugin_data['Version']
                    );
                }
            }
            $output[] = "";
            $output[] = str_repeat("=", 50);
            $output[] = "";
            
            return implode("\n", $output);
        } catch (\Throwable $e) {
            return "Error gathering system information: " . $e->getMessage() . "\n\n";
        }
    }

    /**
     * Download logs as a text file
     * 
     * @param array $filters Filters to apply
     * @return string Path to the generated file
     */
    public function downloadLogs(array $filters = []): string {
        try {
            if (!$this->logFilePath || !file_exists($this->logFilePath)) {
                return '';
            }

            // Get all logs without pagination by setting a very high per_page value
            $allLogs = $this->getFilteredLogs($filters, 1, PHP_INT_MAX)['logs'];
            $filename = 'weeconnectpay-logs-' . date('Y-m-d-H-i-s') . '.txt';
            $filepath = wp_upload_dir()['path'] . '/' . $filename;

            // Write system information and logs to the file
            $handle = fopen($filepath, 'w');
            if ($handle) {
                // Write system information first
                fwrite($handle, $this->getSystemInformation());
                
                // Write log entries
                fwrite($handle, "=== Log Entries ===\n\n");
                foreach ($allLogs as $log) {
                    $line = rtrim($log['raw']) . PHP_EOL;
                    fwrite($handle, $line);
                }
                fclose($handle);
            }

            return $filepath;
        } catch (\Throwable $e) {
            LogService::error(sprintf(
                'Failed to generate log download: %s',
                json_encode($e->getMessage())
            ));
            return '';
        }
    }

    /**
     * Clear the log file and remove existing backup files
     * 
     * @return bool True if successful, false otherwise
     */
    public function clearLogs(): bool {
        try {
            if (!$this->isDebugEnabled() || !$this->logFilePath) {
                return false;
            }

            if (!file_exists($this->logFilePath)) {
                // Even if main log doesn't exist, still try to clean up any backup files
                $this->removeExistingBackups();
                return true;
            }

            // Remove existing backup files first
            $this->removeExistingBackups();

            // Clear the main log file (no backup creation for manual clearing)
            if (file_put_contents($this->logFilePath, '') === false) {
                return false;
            }

            // Add audit log entry to record the clearing action
            $this->logClearingAction();

            return true;
        } catch (\Throwable $e) {
            // Keep using error_log here as fallback since this is in the logger itself
            error_log($this->logPrefix . 'Failed to clear logs: ' . esc_html($e->getMessage()));
            return false;
        }
    }

    /**
     * Log the clearing action for audit trail
     * 
     * @return void
     */
    private function logClearingAction(): void {
        try {
            // Get current user information
            $currentUser = wp_get_current_user();
            $username = $currentUser->exists() ? $currentUser->user_login : 'system';
            $userEmail = $currentUser->exists() ? $currentUser->user_email : 'unknown';
            
            // Get client IP
            $clientIp = $this->getClientIp();
            
            // Create audit log message
            $auditMessage = sprintf(
                'Logs cleared by user: %s (%s) from IP: %s at %s',
                $username,
                $userEmail,
                $clientIp,
                current_time('Y-m-d H:i:s T')
            );
            
            // Write audit entry directly to file (bypass normal logging to avoid infinite recursion)
            $this->writeAuditEntry($auditMessage);
            
        } catch (\Throwable $e) {
            // If audit logging fails, just record basic info
            error_log($this->logPrefix . 'Logs cleared - audit logging failed: ' . $e->getMessage());
        }
    }

    /**
     * Write audit entry directly to log file
     * 
     * @param string $message Audit message to write
     * @return void
     */
    private function writeAuditEntry(string $message): void {
        $this->writeLogEntry($message, 'INFO');
    }

    /**
     * Get client IP address with proxy support
     * 
     * @return string Client IP address
     */
    private function getClientIp(): string {
        // Check for various headers that might contain the real IP
        $headers = [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        ];
        
        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip = $_SERVER[$header];
                // Handle comma-separated IPs (take the first one)
                if (strpos($ip, ',') !== false) {
                    $ip = trim(explode(',', $ip)[0]);
                }
                // Basic IP validation
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $ip;
                }
            }
        }
        
        // Fallback to REMOTE_ADDR or unknown
        return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    }

    /**
     * Remove existing backup files for the current log file
     * 
     * @return void
     */
    private function removeExistingBackups(): void {
        try {
            if (!$this->logFilePath) {
                return;
            }

            // Find all backup files for this log file
            $backupPattern = $this->logFilePath . '.*.bak';
            $backupFiles = glob($backupPattern);
            
            if ($backupFiles === false) {
                return; // glob() failed
            }

            // Remove each backup file
            foreach ($backupFiles as $backupFile) {
                if (file_exists($backupFile) && is_file($backupFile)) {
                    if (unlink($backupFile)) {
                        // Log successful backup removal (only in debug mode to avoid noise)
                        if ($this->isDebugEnabled()) {
                            $backupName = basename($backupFile);
                            error_log($this->logPrefix . "Removed backup file: {$backupName}");
                        }
                    } else {
                        // Log failed backup removal
                        $backupName = basename($backupFile);
                        error_log($this->logPrefix . "Failed to remove backup file: {$backupName}");
                    }
                }
            }
        } catch (\Throwable $e) {
            error_log($this->logPrefix . 'Failed to remove backup files: ' . esc_html($e->getMessage()));
        }
    }

    /**
     * Get the update-safe directory where log files are stored.
     *
     * Logs live under wp-content/uploads/ (which survives plugin updates) rather than
     * inside the plugin directory, which WordPress deletes and recreates on every update,
     * wiping the logs. Uses the uploads basedir (not path, which includes a rotating
     * year/month subfolder). Falls back to the legacy in-plugin location only if
     * wp_upload_dir() reports an error. Mirrors the wp_upload_dir() usage in downloadLogs().
     *
     * @return string Absolute path with a trailing slash
     */
    private function getLogDirectory(): string {
        $uploadDir = wp_upload_dir();
        if (empty($uploadDir['error']) && !empty($uploadDir['basedir'])) {
            return trailingslashit($uploadDir['basedir']) . 'weeconnectpay-logs/';
        }

        // Fallback: legacy in-plugin location (does not survive updates)
        return $this->getLegacyLogDirectory();
    }

    /**
     * Legacy in-plugin log directory used before logs were relocated to uploads.
     *
     * Retained only so logs written before the relocation can be migrated out into the
     * update-safe directory before WordPress wipes the plugin folder. See getLogDirectory().
     *
     * @return string Absolute path with a trailing slash
     */
    private function getLegacyLogDirectory(): string {
        return WEECONNECTPAY_PLUGIN_PATH . 'logs/';
    }

    /**
     * Initialize the log file
     *
     * @return void
     */
    private function initializeLogFile(): void {
        $this->hashedLogFilename = $this->generateHashedFilename();
        $this->logFilePath = $this->getLogDirectory() . $this->hashedLogFilename;
        
        // Validate existing log file and migrate if necessary
        $this->validateAndMigrateLogFile();
        
        // Ensure log directory exists and create .htaccess protection
        $this->ensureLogFileExists();
        $this->createHtaccessProtection();
        
        // Add initialization log entry to verify logging is working
        $this->logInitialization();
    }

    /**
     * Log the initialization for verification that logging is working
     * 
     * @return void
     */
    private function logInitialization(): void {
        try {
            // Only log initialization if debug mode is enabled and we have a valid log path
            if (!$this->isDebugEnabled() || !$this->logFilePath) {
                return;
            }
            
            // Check if file is empty (new file) or was just cleared
            $fileSize = file_exists($this->logFilePath) ? filesize($this->logFilePath) : 0;
            
            // Only add initialization entry for new/empty files to avoid spam
            if ($fileSize === 0) {
                global $wp_version;
                
                $initMessage = sprintf(
                    'Logger initialized successfully - WordPress %s, WeeConnectPay v%s, PHP %s',
                    $wp_version,
                    defined('WEECONNECT_VERSION') ? WEECONNECT_VERSION : 'unknown',
                    PHP_VERSION
                );
                
                // Write directly to avoid potential recursion during initialization
                $this->writeLogEntry($initMessage, 'INFO');
            }
        } catch (\Throwable $e) {
            // If initialization logging fails, use error_log as fallback
            error_log($this->logPrefix . 'Logger initialization logging failed: ' . $e->getMessage());
        }
    }

    /**
     * Write log entry directly to file
     * 
     * @param string $message Log message
     * @param string $level Log level
     * @return void
     */
    private function writeLogEntry(string $message, string $level): void {
        try {
            $timestamp = date('Y-m-d H:i:s');
            $logEntry = "[$timestamp] " . $this->logPrefix . "[$level] " . $message . PHP_EOL;
            
            file_put_contents(
                $this->logFilePath,
                $logEntry,
                FILE_APPEND | LOCK_EX
            );
        } catch (\Throwable $e) {
            error_log($this->logPrefix . 'Failed to write log entry: ' . $e->getMessage());
        }
    }

    /**
     * Validate existing log file hash and migrate if necessary (security requirement)
     * 
     * @return void
     */
    private function validateAndMigrateLogFile(): void {
        try {
            // Check if our expected file already exists and has content
            if (file_exists($this->logFilePath) && filesize($this->logFilePath) > 0) {
                return; // Current hash file exists with content, no migration needed
            }

            // Scan for a migratable log in priority order: the current (uploads) directory
            // first, then the legacy in-plugin directory. The legacy directory is scanned so
            // logs written before the relocation are rescued into the update-safe location
            // before WordPress wipes the plugin folder on the next update.
            $logsDir = $this->getLogDirectory();
            $searchDirs = [$logsDir];
            $legacyDir = $this->getLegacyLogDirectory();
            if ($legacyDir !== $logsDir) {
                $searchDirs[] = $legacyDir;
            }

            foreach ($searchDirs as $searchDir) {
                // Look for old predictable filename (immediate security migration)
                $oldPredictableFile = $searchDir . 'weeconnectpay-debug.log';
                if (file_exists($oldPredictableFile) && filesize($oldPredictableFile) > 0) {
                    $this->migrateLogFile($oldPredictableFile, 'predictable filename');
                    return;
                }

                // Look for the most recent hash-based file with content (not the current expected one)
                $logFiles = glob($searchDir . 'weeconnectpay-debug-*.log') ?: [];
                $bestCandidateFile = null;
                $bestCandidateTime = 0;
                $bestCandidateSize = 0;

                foreach ($logFiles as $oldFile) {
                    if ($oldFile !== $this->logFilePath) { // Skip the current expected file
                        $fileSize = filesize($oldFile);
                        $fileTime = filemtime($oldFile);

                        // Prefer files with content, then most recent
                        if ($fileSize > 0 && ($fileSize > $bestCandidateSize ||
                            ($fileSize == $bestCandidateSize && $fileTime > $bestCandidateTime))) {
                            $bestCandidateFile = $oldFile;
                            $bestCandidateTime = $fileTime;
                            $bestCandidateSize = $fileSize;
                        }
                    }
                }

                // Migrate the best candidate if found, then stop searching further directories
                if ($bestCandidateFile) {
                    $this->migrateLogFile($bestCandidateFile, 'hash mismatch - migrating from most recent log with content');
                    return;
                }
            }
        } catch (\Throwable $e) {
            error_log('[WeeConnectPay] Error during log file validation: ' . $e->getMessage());
        }
    }

    /**
     * Migrate an existing log file to the new hash format
     * 
     * @param string $oldFilePath Path to the old log file
     * @param string $reason Reason for migration (for logging)
     * @return void
     */
    private function migrateLogFile(string $oldFilePath, string $reason): void {
        try {
            // Ensure the destination directory exists before copying. When migrating from the
            // legacy in-plugin dir into a fresh uploads dir, the target dir may not exist yet.
            $destDir = dirname($this->logFilePath);
            if (!file_exists($destDir)) {
                wp_mkdir_p($destDir);
            }

            // Copy old file to new location
            if (copy($oldFilePath, $this->logFilePath)) {
                // Log the migration event with redacted hash for security
                $redactedHash = substr(basename($this->hashedLogFilename, '.log'), -8); // Last 8 chars only
                error_log("[WeeConnectPay] Migrated log file from {$reason} to new secure filename ending in: {$redactedHash}");
                
                // Remove old file after successful migration
                unlink($oldFilePath);
            } else {
                // Migration failed, create new file and log the issue
                $this->handleHashMismatch();
            }
        } catch (\Throwable $e) {
            error_log("[WeeConnectPay] Failed to migrate log file from {$reason}: " . $e->getMessage());
            $this->handleHashMismatch();
        }
    }

    /**
     * Handle hash mismatch - placeholder for domain/hash changes
     * Creates new log file and logs the issue for future implementation
     * 
     * @return void
     */
    private function handleHashMismatch(): void {
        try {
            // Create new log file
            file_put_contents($this->logFilePath, '');
            
            // Log the event with redacted hash for security
            $redactedHash = substr(basename($this->hashedLogFilename, '.log'), -8); // Last 8 chars only
            error_log("[WeeConnectPay] Created new log file due to hash validation failure. New file ending in: {$redactedHash}");
            
            // TODO: Future enhancement - implement logic to:
            // 1. Detect domain name changes
            // 2. Re-create logs with new domain-name + Salt
            // 3. Preserve log history where appropriate
            
        } catch (\Throwable $e) {
            error_log('[WeeConnectPay] Failed to handle hash mismatch: ' . $e->getMessage());
        }
    }

    /**
     * Validate if current log file matches expected hash (for future domain change detection)
     * 
     * @return bool True if hash is valid, false otherwise
     */
    private function validateLogFileHash(): bool {
        try {
            return file_exists($this->logFilePath);
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Generate a hashed filename using salt and WordPress host for security
     * Uses salt from IntegrationSettings + site host for unpredictable filename
     * 
     * @return string Complete filename with hash and extension
     */
    private function generateHashedFilename(): string {
        try {
            $integrationSettings = new IntegrationSettings();
            $salt = $integrationSettings->getLogFilenameSalt();
            $host = $this->getHashHost();
            
            // Validate salt is not empty
            if (empty($salt) || $salt === 'SALT_NOT_FOUND') {
                throw new \Exception('Invalid or missing salt from database');
            }
            
            // Generate 32-character hash using salt + host
            $hash = md5($salt . $host);
            
            return self::LOG_FILENAME_BASE . '-' . $hash . '.log';
            
        } catch (\Throwable $e) {
            // Comprehensive error handling for various failure scenarios
            $this->handleSaltRetrievalError($e);
            
            // Generate fallback filename that's still somewhat secure
            $fallbackData = self::LOG_FILENAME_BASE . time() . $this->getHashHost();
            $fallbackHash = md5($fallbackData);
            
            return self::LOG_FILENAME_BASE . '-' . $fallbackHash . '.log';
        }
    }

    /**
     * Handle salt retrieval errors with proper logging and error categorization
     * 
     * @param \Throwable $e The exception that occurred
     * @return void
     */
    private function handleSaltRetrievalError(\Throwable $e): void {
        $errorType = 'Unknown error';
        $errorDetails = $e->getMessage();
        
        // Categorize error types for better debugging
        if (strpos($errorDetails, 'database') !== false || strpos($errorDetails, 'SQL') !== false) {
            $errorType = 'Database connectivity issue';
        } elseif (strpos($errorDetails, 'salt') !== false || strpos($errorDetails, 'SALT_NOT_FOUND') !== false) {
            $errorType = 'Salt generation/retrieval issue';
        } elseif (strpos($errorDetails, 'permission') !== false || strpos($errorDetails, 'access') !== false) {
            $errorType = 'File system permission issue';
        }
        
        // Log error with categorization (using basic error_log since Logger might not be fully initialized)
        error_log("[WeeConnectPay] {$errorType} during log filename generation: {$errorDetails}");
        
        // For critical errors, also try to create a basic log entry
        if ($errorType === 'Database connectivity issue') {
            error_log('[WeeConnectPay] CRITICAL: Database unavailable - using fallback log filename generation');
        }
    }

    /**
     * Get host for hash generation with WordPress functions and fallbacks
     * 
     * @return string Host string for hash generation
     */
    private function getHashHost(): string {
        // Option 1: Use WordPress get_site_url() and extract host
        $host = parse_url(get_site_url(), PHP_URL_HOST);
        
        // Fallback to $_SERVER if WordPress functions fail
        if (!$host && isset($_SERVER['HTTP_HOST'])) {
            $host = $_SERVER['HTTP_HOST'];
        }
        
        // Ultimate fallback
        if (!$host) {
            $host = 'localhost';
        }
        
        return $host;
    }

    /**
     * Ensure log file exists and directory is created
     * 
     * @return void
     */
    private function ensureLogFileExists(): void {
        try {
            // Ensure directory exists
            $dir = dirname($this->logFilePath);
            if (!file_exists($dir)) {
                wp_mkdir_p($dir);
            }
            
            // Create empty log file if it doesn't exist
            if (!file_exists($this->logFilePath)) {
                file_put_contents($this->logFilePath, '');
            }
        } catch (\Throwable $e) {
            error_log('[WeeConnectPay] Failed to ensure log file exists: ' . $e->getMessage());
        }
    }

    /**
     * Create .htaccess protection in logs directory to block web access
     * 
     * @return void
     */
    private function createHtaccessProtection(): void {
        try {
            $logsDir = dirname($this->logFilePath);
            $htaccessPath = $logsDir . '/.htaccess';
            
            // Only create if it doesn't exist to avoid overwriting custom rules
            if (!file_exists($htaccessPath)) {
                $htaccessContent = "# Deny all access to log files\n";
                $htaccessContent .= "# WeeConnectPay Security Protection\n\n";
                $htaccessContent .= "# Apache 2.4+ configuration\n";
                $htaccessContent .= "<RequireAll>\n";
                $htaccessContent .= "    Require all denied\n";
                $htaccessContent .= "</RequireAll>\n\n";
                $htaccessContent .= "# Apache 2.2 compatibility\n";
                $htaccessContent .= "Order deny,allow\n";
                $htaccessContent .= "Deny from all\n\n";
                $htaccessContent .= "# Additional protection for specific file patterns\n";
                $htaccessContent .= "<FilesMatch \"\\.(log|txt|bak)$\">\n";
                $htaccessContent .= "    Order deny,allow\n";
                $htaccessContent .= "    Deny from all\n";
                $htaccessContent .= "</FilesMatch>\n";
                
                file_put_contents($htaccessPath, $htaccessContent);
            }

            // Defense-in-depth against directory listing on servers that ignore .htaccess
            // (e.g. nginx). The unpredictable hashed filename remains the primary protection.
            $indexPath = $logsDir . '/index.php';
            if (!file_exists($indexPath)) {
                file_put_contents($indexPath, "<?php // Silence is golden\n");
            }
        } catch (\Throwable $e) {
            error_log('[WeeConnectPay] Failed to create .htaccess protection: ' . $e->getMessage());
        }
    }

    /**
     * Write to the custom log file
     * 
     * @param string $message The message to write
     * @return bool Whether the write was successful
     */
    private function writeToLogFile(string $message): bool {
        try {
            if (!$this->logFilePath) {
                return false;
            }

            // Check file size and rotate if necessary
            if (file_exists($this->logFilePath)) {
                $fileSize = filesize($this->logFilePath);
                if ($fileSize && $fileSize > self::MAX_LOG_SIZE) {
                    $this->rotateLogFile($this->logFilePath);
                }
            }

            // Ensure directory exists
            $dir = dirname($this->logFilePath);
            if (!file_exists($dir)) {
                wp_mkdir_p($dir);
            }

            // Add timestamp to the message
            $timestamp = date('Y-m-d H:i:s');
            $message = "[$timestamp] $message" . PHP_EOL;

            // Write to file with proper locking
            $result = file_put_contents(
                $this->logFilePath,
                $message,
                FILE_APPEND | LOCK_EX
            );

            return $result !== false;
        } catch (\Throwable $e) {
            error_log($this->logPrefix . 'Failed to write to log file: ' . esc_html($e->getMessage()));
            return false;
        }
    }

    /**
     * Get the path to the custom log file
     * 
     * @return string|null
     */
    public function getLogFilePath(): ?string {
        return $this->logFilePath;
    }
} 