<?php

declare(strict_types=1);
/**
 * Scan logging service.
 */

namespace SecurWP\Modules\Scanner\Service;

use SecurWP\Includes\DataDirectoryProtection;
use SecurWP\Includes\GuardedDataFile;
use SecurWP\Modules\Scanner\Repository\ScanStatusRepository;
use SecurWP\Modules\Scanner\Queue\ScanQueue;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class ScanLogger {

    const FILE_PREFIX = 'securwp-scan-';
    const FILE_SUFFIX = '.log.php';
    const TOKEN_OPTION = 'securwp_scan_log_file_token';
    const AJAX_ACTION = 'securwp_get_scan_log';
    const MAX_LOG_SIZE = 5242880; // 5MB - prevent unbounded growth during scan
    const MAX_DB_ROWS = 10000; // Max database log entries per scan
    const MAX_ROTATED_LOGS = 3; // Maximum number of rotated log files to keep

    /** @var string|null Cached log file path for the current request. */
    private static ?string $cached_log_path = null;

    /**
     * PHP process peak memory captured before scan work begins.
     * Used to calculate scan-only incremental memory usage.
     * @var int
     */
    private static $memory_baseline = 0;

    /**
     * Cache for current scan ID to avoid repeated ScanQueue::get() calls.
     * @var string|null
     */
    private static $cached_scan_id = null;
    private static $cache_timestamp = 0;
    private static $cache_ttl = 60; // Cache for 60 seconds

    /**
     * Filename embeds a per-install token so the path cannot be guessed if a
     * server-config gap exposes the scan-logs directory.
     */
    private static function get_log_path() {
        if ( null !== self::$cached_log_path ) {
            return self::$cached_log_path;
        }

        $base_dir = WP_CONTENT_DIR . '/securwp-data/scan-logs';
        DataDirectoryProtection::ensure_local_protection( $base_dir );

        $token = get_option( self::TOKEN_OPTION, '' );
        if ( empty( $token ) || ! is_string( $token ) ) {
            $token = wp_generate_password( 16, false, false );
            update_option( self::TOKEN_OPTION, $token, false );
        }

        $path = $base_dir . '/' . self::FILE_PREFIX . $token . self::FILE_SUFFIX;

        self::$cached_log_path = $path;
        return $path;
    }

    /**
     * Register AJAX handlers.
     */
    public static function register() {
         add_action( 'wp_ajax_' . self::AJAX_ACTION, [ __CLASS__, 'handle_ajax_get_log' ] );
    }

    /**
     * Get current scan ID with caching to avoid excessive ScanQueue::get() calls.
     *
     * @return string Scan ID or 'system'
     */
    private static function get_cached_scan_id() {
        $now = time();

        // Return cached value if still valid
        if ( self::$cached_scan_id !== null && ( $now - self::$cache_timestamp ) < self::$cache_ttl ) {
            return self::$cached_scan_id;
        }

        // Refresh cache
        try {
            $scan = ScanQueue::get();
            if ( $scan && ! empty( $scan['id'] ) ) {
                self::$cached_scan_id = $scan['id'];
            } else {
                // Fall back to last published scan ID to keep tail logs attached to the correct scan.
                $published_id = \SecurWP\Includes\SettingsStore::get_field( \SecurWP\Includes\SettingsStore::COL_STATE, 'published_scan_id', '' );
                self::$cached_scan_id = ! empty( $published_id ) ? $published_id : 'system';
            }
            self::$cache_timestamp = $now;
        } catch ( \Throwable $e ) {
            // If scan queue fails, use fallback
            $published_id = \SecurWP\Includes\SettingsStore::get_field( \SecurWP\Includes\SettingsStore::COL_STATE, 'published_scan_id', '' );
            self::$cached_scan_id = ! empty( $published_id ) ? $published_id : 'system';
            self::$cache_timestamp = $now;
        }

        return self::$cached_scan_id;
    }


    /**
     * Log a message to the scan log.
     *
     * @param string $message The message to log.
     * @param int    $level   Log level (1=info, 2=warning, 3=error)
     * @param string $type    Log type (info, warning, error, etc.)
     */
    public static function log( $message, $level = 1, $type = 'info' ) {
        self::write_log( $message, $level, $type, null );
    }

    /**
     * Log a message for a specific scan ID even after the active queue row is terminal.
     *
     * @param string $scan_id
     * @param string $message
     * @param int    $level
     * @param string $type
     * @return void
     */
    public static function log_for_scan( string $scan_id, $message, $level = 1, $type = 'info' ): void {
        self::write_log( $message, $level, $type, $scan_id );
    }

    /**
     * Core logger implementation shared by automatic and explicit scan contexts.
     *
     * @param string      $message
     * @param int         $level
     * @param string      $type
     * @param string|null $scan_id_override
     * @return void
     */
    private static function write_log( $message, $level, $type, ?string $scan_id_override = null ): void {
        // Prevent infinite loops if logger itself errors
        static $logging_in_progress = false;
        if ( $logging_in_progress ) {
            return;
        }
        $logging_in_progress = true;

        $file_success = false;
        $db_success = false;

        // === FILE LOGGING (Primary) ===
        try {
            $log_path = self::get_log_path();

            // Rotate log if it exceeds max size (keep up to MAX_ROTATED_LOGS rotated files)
            if ( @file_exists( $log_path ) ) {
                $size = @filesize( $log_path );
                if ( $size !== false && $size > self::MAX_LOG_SIZE ) {
                    self::rotate_log( $log_path );
                }
            }

            $timestamp = current_time( 'H:i:s' );
            $line = "[$timestamp] $message" . PHP_EOL;

            // Use error suppression + check return value
            $file_success = GuardedDataFile::append_payload( $log_path, $line );

        } catch ( \Throwable $e ) {
            $file_success = false;
        }

        // === DATABASE LOGGING (Secondary) ===
        try {
            $scan_id = ! empty( $scan_id_override ) ? sanitize_text_field( $scan_id_override ) : self::get_cached_scan_id();

            if ( ! class_exists( 'SecurWP\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                require_once dirname( __DIR__ ) . '/Repository/ScanStatusRepository.php';
            }

            if ( class_exists( 'SecurWP\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                ScanStatusRepository::add( $scan_id, $level, $type, $message );
                $db_success = true;

                // Periodic cleanup: 2% chance to trim if over limit
                if ( wp_rand( 1, 100 ) <= 2 ) {
                    ScanStatusRepository::trim_by_count( self::MAX_DB_ROWS );
                }
            }

        } catch ( \Throwable $e ) {
            // DB logging failed.
            $db_success = false;
        }

        // Last-resort error log if both sinks fail.
        if ( ! $file_success && ! $db_success ) {
            @securwp_log( 'SecurWP Logger: Both file and DB logging failed for message: ' . $message );
        }

        $logging_in_progress = false;
    }


    /**
     * Rotate the log file instead of truncating.
     *
     * Renames current log to .1, shifts existing rotated logs (.1 -> .2, .2 -> .3),
     * and removes the oldest if it exceeds MAX_ROTATED_LOGS. Then starts a fresh log.
     *
     * @param string $log_path Absolute path to the current log file.
     */
    private static function rotate_log( $log_path ) {
        // Shift existing rotated logs (.3 is deleted, .2 -> .3, .1 -> .2)
        for ( $i = self::MAX_ROTATED_LOGS; $i >= 1; $i-- ) {
            $rotated_path = GuardedDataFile::rotated_path( $log_path, '.' . $i );
            if ( ! @file_exists( $rotated_path ) ) {
                continue;
            }
            if ( $i === self::MAX_ROTATED_LOGS ) {
                // Delete the oldest rotated log
                wp_delete_file( $rotated_path );
            } else {
                // Shift: .N -> .N+1
                self::move_file( $rotated_path, GuardedDataFile::rotated_path( $log_path, '.' . ( $i + 1 ) ) );
            }
        }

        // Rotate current log to .1
        self::move_file( $log_path, GuardedDataFile::rotated_path( $log_path, '.1' ) );

        // Start fresh
        GuardedDataFile::clear_payload( $log_path );
    }

    /**
     * Clear all logs (file and database).
     * Call this when starting a new scan.
     */
    public static function clear() {
        try {
            // Clear file log
            $log_path = self::get_log_path();
            GuardedDataFile::clear_payload( $log_path );

            // Clear database logs
            if ( class_exists( 'SecurWP\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                ScanStatusRepository::clear_all();
            }

            // Clear cache
            self::$cached_scan_id = null;
            self::$cache_timestamp = 0;

        } catch ( \Throwable $e ) {
            // Silently fail - clearing is best effort
        }
    }

    /**
     * Called at the start of every worker iteration.
     *
     * Records the WP baseline on the first call (before any scan work), and
     * updates the stored high-water mark on every subsequent call so that the
     * peak from memory-intensive iterations (hashing, file reads) is preserved
     * across PHP requests in the loopback chain.
     */
    public static function capture_memory_baseline(): void {
        $peak    = memory_get_peak_usage( true );
        $scan_id = self::get_cached_scan_id();

        if ( empty( $scan_id ) || $scan_id === 'system' ) {
            self::$memory_baseline = $peak;
            return;
        }

        $base_key = 'securwp_scan_mem_base_' . $scan_id;
        $peak_key = 'securwp_scan_mem_peak_' . $scan_id;

        // Store baseline once - the very first iteration before any scan work.
        if ( ! get_transient( $base_key ) ) {
            set_transient( $base_key, $peak, 3 * HOUR_IN_SECONDS );
        }

        // Always track the highest peak seen across all iterations.
        $stored_max = (int) get_transient( $peak_key );
        if ( $peak > $stored_max ) {
            set_transient( $peak_key, $peak, 3 * HOUR_IN_SECONDS );
        }

        self::$memory_baseline = $peak;
    }

    /**
     * Capture the final peak for the current worker iteration.
     *
     * Called at the end of every worker request so cross-request peak memory is
     * preserved until terminal completion/failure logs it and clears the state.
     *
     * @param string|null $scan_id_override
     * @return void
     */
    public static function capture_memory_end( ?string $scan_id_override = null ): void {
        try {
            $peak    = memory_get_peak_usage( true );
            $scan_id = ! empty( $scan_id_override ) ? sanitize_text_field( $scan_id_override ) : self::get_cached_scan_id();

            if ( empty( $scan_id ) || $scan_id === 'system' ) {
                return;
            }

            $base_key = 'securwp_scan_mem_base_' . $scan_id;
            $peak_key   = 'securwp_scan_mem_peak_' . $scan_id;
            if ( false === get_transient( $base_key ) ) {
                return;
            }
            $stored_max = (int) get_transient( $peak_key );
            if ( $peak > $stored_max ) {
                set_transient( $peak_key, $peak, 3 * HOUR_IN_SECONDS );
            }
        } catch ( \Throwable $e ) {
            // Best-effort metrics only.
        }
    }

    public static function log_memory_usage( ?string $scan_id_override = null ): void {
        try {
            $peak    = memory_get_peak_usage( true );
            $scan_id = ! empty( $scan_id_override ) ? sanitize_text_field( $scan_id_override ) : self::get_cached_scan_id();

            $base_key = 'securwp_scan_mem_base_' . $scan_id;
            $peak_key = 'securwp_scan_mem_peak_' . $scan_id;

            // Update peak one final time for the completion iteration.
            $stored_max = (int) get_transient( $peak_key );
            $max_peak   = max( $peak, $stored_max );
            $baseline   = (int) get_transient( $base_key );

            // Clean up transients now that scan is done.
            delete_transient( $base_key );
            delete_transient( $peak_key );

            if ( $baseline > 0 ) {
                $scan_overhead = max( 0, $max_peak - $baseline );
                self::write_log( sprintf(
                    '[Memory] Peak process memory: %s • WP baseline: ~%s • Scan overhead: %s',
                    size_format( $max_peak ),
                    size_format( $baseline ),
                    $scan_overhead >= 1024 * 1024 ? size_format( $scan_overhead ) : '<1 MB'
                ), 1, 'info', $scan_id );
            } else {
                self::write_log( '[Memory] Peak process memory: ' . size_format( $peak ), 1, 'info', $scan_id );
            }
        } catch ( \Throwable $e ) {
            // Silently fail if memory functions unavailable
        }
    }

    /**
     * Format log entries for user-friendly display with reliable error handling.
     *
     * @param int $limit Maximum number of entries to return (default: 100)
     * @return array Formatted log entries for UI
     */
    public static function get_formatted_entries( $limit = 100 ) {
        try {
            // PRIMARY: Try Database First
            if ( class_exists( 'SecurWP\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                $events = ScanStatusRepository::get_recent( $limit );
                if ( ! empty( $events ) ) {
                     $entries = [];
                     foreach ( $events as $event ) {
                        // ScanStatus object access might need array or object check
                        // Assuming it's an object from generic casting or specific class
                        $props = is_object($event) ? get_object_vars($event) : $event;
                        
                        // Fallback keys if Entity not fully hydrated
                        $msg = $props['message'] ?? '';
                        $level = $props['level'] ?? 1;
                        $time = isset($props['ctime']) ? gmdate( 'H:i:s', (int) $props['ctime'] ) : ''; // ctime is microtime float

                        $level_name = 'INFO';
                        if ($level == 2) $level_name = 'WARNING';
                        if ($level == 3) $level_name = 'ERROR';
                        if ($level == 4) $level_name = 'ACTION';

                        $color = self::get_level_color( $level_name );
                        if ( strpos( $msg, '[Memory]' ) !== false ) {
                            $color = 'purple';
                        } elseif ( strpos( $msg, 'Scan completed.' ) === 0 ) {
                            $color = 'green';
                        }
                        
                        $entries[] = [
                            'time'    => $time,
                            'level'   => $level_name,
                            'message' => self::format_message( $msg ),
                            'raw'     => $msg,
                            'icon'    => self::get_level_icon( $level_name ),
                            'color'   => $color,
                        ];
                     }
                     return $entries;
                }
            }

            // FALLBACK: File Logging
            $path = self::get_log_path();

            if ( ! @file_exists( $path ) ) {
                return [];
            }

            // Performance optimization: Read only last N lines instead of entire file
            // This prevents hanging when log file grows large
            $lines = self::tail_file( $path, $limit );
            $entries = [];

            foreach ( $lines as $line ) {
                $line = trim( $line );
                if ( empty( $line ) ) continue;

                // Parse format: [HH:MM:SS] Message
                if ( preg_match( '/^\[(\d{1,2}:\d{2}:\d{2})\]\s*(.+)$/', $line, $matches ) ) {
                    $entries[] = [
                        'time'    => $matches[1],
                        'level'   => 'INFO',
                        'message' => self::format_message( $matches[2] ),
                        'raw'     => $matches[2],
                        'icon'    => null,
                        'color'   => strpos( $matches[2], '[Memory]' ) !== false ? 'purple' : 'blue',
                    ];
                }
            }

            return $entries; // Most recent entries

        } catch ( \Throwable $e ) {
            // Return empty array on any error.
            return [];
        }
    }

    /**
     * Read last N lines from file efficiently with reliable error handling.
     *
     * @param string $file File path
     * @param int $lines Number of lines to read
     * @return array Array of lines
     */
    private static function tail_file( $file, $lines = 100 ) {
        try {
            $content = GuardedDataFile::read_payload( $file );
            if ( false === $content || '' === $content ) {
                return [];
            }

            $normalized = str_replace( [ "\r\n", "\r" ], "\n", $content );
            $normalized = rtrim( $normalized, "\n" );
            if ( '' === $normalized ) {
                return [];
            }

            $all_lines = explode( "\n", $normalized );
            return array_slice( $all_lines, -1 * max( 1, (int) $lines ) );

        } catch ( \Throwable $e ) {
            return [];
        }
    }

    /**
     * Move a file without relying on raw rename().
     */
    private static function move_file( string $source, string $destination ): bool {
        if ( ! @file_exists( $source ) ) {
            return false;
        }

        if ( @copy( $source, $destination ) ) {
            wp_delete_file( $source );
            return ! @file_exists( $source );
        }

        return false;
    }

    /**
     * Format technical messages into user-friendly text.
     */
    private static function format_message( $message ) {
        // Map technical terms to user-friendly language
        $replacements = [
            'Starting scan collection for type: standard' => 'Initializing Standard Security Scan...',
            'Starting scan collection for type: high_sensitive' => 'Initializing High-Sensitivity Security Scan...',
            'Process: Checking Server State...' => 'Analyzing Server Environment State...',
            'Phase: Running differential analysis...' => 'Performing Differential File Analysis...',
            'Phase: Queuing files for analysis...' => 'Building Analysis Queue...',
            'Phase: Collecting scan targets...' => 'Identifying Scan Targets...',
            'Phase: Queuing files for analysis...' => 'Building Analysis Queue...',
            'Phase: Collecting scan targets...' => 'Identifying Scan Targets...',
            'Clearing scan history...' => 'Purging Previous Audit Records...',
        ];

        // Professional Format for Memory Log
        if ( strpos( $message, '[Memory] Peak Usage:' ) !== false ) {
            return str_replace( '[Memory] Peak Usage:', 'Performance Audit: Peak RAM Usage', $message );
        }

        // Direct replacement
        if ( isset( $replacements[ $message ] ) ) {
            return $replacements[ $message ];
        }

        // Return original if no formatting applies
        return $message;
    }

    /**
     * Get icon for log level.
     */
    private static function get_level_icon( $level ) {
        $icons = [
            'DEBUG'   => '🔧',
            'INFO'    => '📋',
            'WARNING' => '⚠️',
            'ERROR'   => '❌',
            'ACTION'  => '👤',
        ];
        return $icons[ $level ] ?? '📋';
    }

    /**
     * Get color class for log level.
     */
    private static function get_level_color( $level ) {
        $colors = [
            'DEBUG'   => 'gray',
            'INFO'    => 'blue',
            'WARNING' => 'yellow',
            'ERROR'   => 'red',
            'ACTION'  => 'green',
        ];
        return $colors[ $level ] ?? 'gray';
    }

    /**
     * AJAX Handler: Get log content with reliable error handling.
     */
    public static function handle_ajax_get_log() {
        try {
            check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

            if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
                wp_send_json_error( [ 'message' => 'Unauthorized' ] );
                return;
            }

            $format = sanitize_text_field( wp_unslash( $_REQUEST['format'] ?? 'formatted' ) );

            if ( $format === 'raw' ) {
                $log_content = '';
                $path = self::get_log_path();

                if ( @file_exists( $path ) ) {
                    $content = GuardedDataFile::read_payload( $path );
                    if ( $content !== false ) {
                        $log_content = $content;
                    }
                }

                wp_send_json_success( [ 'log' => $log_content ] );
            } else {
                // Formatted for UI display
                $entries = self::get_formatted_entries();
                wp_send_json_success( [ 'entries' => $entries ] );
            }

        } catch ( \Throwable $e ) {
            wp_send_json_error( [
                'message' => 'Failed to retrieve logs',
                'error' => $e->getMessage()
            ] );
        }
    }
}
