<?php

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

namespace SiteFort\Modules\Scanner\Service;

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

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

class ScanLogger {

    const FILE_PREFIX = 'sitefort-scan-';
    const FILE_SUFFIX = '.log.php';
    const TOKEN_OPTION = 'sitefort_scan_log_file_token';
    const AJAX_ACTION = 'sitefort_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;

    private static $memory_baseline = 0;
    private static $memory_shutdown_registered = [];
    private static $memory_finalized = [];

    /**
     * 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
    private static $throttle_cache = [];

    /**
     * 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 . '/sitefort-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 = \SiteFort\Includes\SettingsStore::get_field( \SiteFort\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 = \SiteFort\Includes\SettingsStore::get_field( \SiteFort\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 );
    }

    public static function should_log_throttled_for_scan( string $scan_id, string $key, int $interval_seconds = 5 ): bool {
        $scan_id = sanitize_text_field( $scan_id );
        if ( '' === $scan_id ) {
            return false;
        }

        $interval_seconds = max( 1, $interval_seconds );
        $state = self::get_throttle_state( $scan_id, $key );
        $last_at = (int) ( $state['last_at'] ?? 0 );

        return $last_at <= 0 || ( time() - $last_at ) >= $interval_seconds;
    }

    public static function log_throttled_for_scan( string $scan_id, string $key, $message, int $interval_seconds = 5, $level = 1, $type = 'info', bool $force = false ): bool {
        $scan_id = sanitize_text_field( $scan_id );
        $message = (string) $message;
        if ( '' === $scan_id || '' === $message ) {
            return false;
        }

        $interval_seconds = max( 1, $interval_seconds );
        $cache_key = self::get_throttle_cache_key( $scan_id, $key );
        $transient_key = self::get_throttle_transient_key( $cache_key );
        $state = self::get_throttle_state( $scan_id, $key );
        $now = time();
        $last_at = (int) ( $state['last_at'] ?? 0 );
        $last_hash = (string) ( $state['last_hash'] ?? '' );
        $message_hash = hash( 'sha256', $message );

        if ( ! $force && $last_at > 0 && ( $now - $last_at ) < $interval_seconds ) {
            return false;
        }

        if ( $force && $last_hash === $message_hash && $last_at > 0 && ( $now - $last_at ) < $interval_seconds ) {
            return false;
        }

        $next_state = [
            'last_at'   => $now,
            'last_hash' => $message_hash,
        ];
        self::$throttle_cache[ $cache_key ] = $next_state;
        set_transient( $transient_key, $next_state, max( 60, $interval_seconds * 4 ) );

        self::write_log( $message, $level, $type, $scan_id );
        return true;
    }

    public static function clear_throttles_for_scan( string $scan_id, array $keys = [] ): void {
        $scan_id = sanitize_text_field( $scan_id );
        if ( '' === $scan_id ) {
            return;
        }

        if ( empty( $keys ) ) {
            $keys = [
                'scan_progress',
                'upload_submission',
                'cloud_poll_pending',
                'skip_file',
                'skip_file_too_large',
                'skip_hash_failed',
                'skip_changed_during_hash',
                'cloud_skip_file',
                'cloud_skip_changed_during_scan',
                'cloud_skip_file_missing',
                'cloud_skip_orphan_no_job',
                'cloud_skip_runtime_artifact_ignored',
                'cloud_skip_scan_timeout',
                'cloud_skip_upload_cycle_exhausted',
                'cloud_skip_upload_failures_exceeded',
                'cloud_skip_upload_timeout',
            ];
        }

        foreach ( $keys as $key ) {
            $cache_key = self::get_throttle_cache_key( $scan_id, (string) $key );
            unset( self::$throttle_cache[ $cache_key ] );
            delete_transient( self::get_throttle_transient_key( $cache_key ) );
        }
    }

    private static function get_throttle_state( string $scan_id, string $key ): array {
        $cache_key = self::get_throttle_cache_key( $scan_id, $key );
        if ( ! array_key_exists( $cache_key, self::$throttle_cache ) ) {
            $stored = get_transient( self::get_throttle_transient_key( $cache_key ) );
            self::$throttle_cache[ $cache_key ] = is_array( $stored ) ? $stored : [];
        }

        return is_array( self::$throttle_cache[ $cache_key ] ) ? self::$throttle_cache[ $cache_key ] : [];
    }

    private static function get_throttle_cache_key( string $scan_id, string $key ): string {
        $key = sanitize_key( $key );
        if ( '' === $key ) {
            $key = 'default';
        }

        return hash( 'sha256', $scan_id . '|' . $key );
    }

    private static function get_throttle_transient_key( string $cache_key ): string {
        return 'sitefort_scan_log_throttle_' . substr( $cache_key, 0, 32 );
    }

    /**
     * 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( 'SiteFort\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                require_once dirname( __DIR__ ) . '/Repository/ScanStatusRepository.php';
            }

            if ( class_exists( 'SiteFort\\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 ) {
            @sitefort_log( 'SiteFort 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( 'SiteFort\\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
        }
    }

    private static function memory_keys( string $scan_id ): array {
        return [
            'sitefort_scan_mem_base_' . $scan_id,
            'sitefort_scan_mem_peak_' . $scan_id,
        ];
    }

    private static function resolve_memory_scan_id( ?string $scan_id_override = null ): string {
        return ! empty( $scan_id_override )
            ? sanitize_text_field( $scan_id_override )
            : self::get_cached_scan_id();
    }

    private static function persist_memory_peak( string $scan_id, int $peak ): void {
        if ( empty( $scan_id ) || $scan_id === 'system' || isset( self::$memory_finalized[ $scan_id ] ) ) {
            return;
        }

        [ $base_key, $peak_key ] = self::memory_keys( $scan_id );

        $baseline = self::$memory_baseline > 0 ? min( self::$memory_baseline, $peak ) : $peak;
        $stored_baseline = get_transient( $base_key );
        if ( false === $stored_baseline || $baseline < (int) $stored_baseline ) {
            set_transient( $base_key, $baseline, 3 * HOUR_IN_SECONDS );
        }

        $stored_max = (int) get_transient( $peak_key );
        if ( $peak > $stored_max ) {
            set_transient( $peak_key, $peak, 3 * HOUR_IN_SECONDS );
        }
    }

    private static function register_memory_shutdown_capture( string $scan_id ): void {
        if ( empty( $scan_id ) || isset( self::$memory_shutdown_registered[ $scan_id ] ) ) {
            return;
        }

        self::$memory_shutdown_registered[ $scan_id ] = true;
        register_shutdown_function( static function () use ( $scan_id ): void {
            self::capture_memory_end( $scan_id );
        } );
    }

    public static function capture_memory_baseline( ?string $scan_id_override = null ): void {
        try {
            $peak    = memory_get_peak_usage( true );
            $scan_id = self::resolve_memory_scan_id( $scan_id_override );

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

            unset( self::$memory_finalized[ $scan_id ] );
            self::persist_memory_peak( $scan_id, $peak );
            self::register_memory_shutdown_capture( $scan_id );
        } catch ( \Throwable $e ) {
            self::$memory_baseline = 0;
        }
    }

    public static function capture_memory_end( ?string $scan_id_override = null ): void {
        try {
            $peak    = memory_get_peak_usage( true );
            $scan_id = self::resolve_memory_scan_id( $scan_id_override );

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

            self::persist_memory_peak( $scan_id, $peak );
        } catch ( \Throwable $e ) {
        }
    }

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

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

            self::persist_memory_peak( $scan_id, $peak );
            [ $base_key, $peak_key ] = self::memory_keys( $scan_id );

            $stored_max = (int) get_transient( $peak_key );
            $max_peak   = max( $peak, $stored_max );
            $baseline   = (int) get_transient( $base_key );

            delete_transient( $base_key );
            delete_transient( $peak_key );
            self::$memory_finalized[ $scan_id ] = true;

            if ( $baseline > 0 ) {
                $scan_overhead = max( 0, $max_peak - $baseline );
                self::write_log( sprintf(
                    '[Memory] Observed PHP worker peak: %s | WP bootstrap baseline: ~%s | Observed 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] Observed PHP worker peak: ' . size_format( $peak ), 1, 'info', $scan_id );
            }
        } catch ( \Throwable $e ) {
        }
    }

    /**
     * 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 {
            if ( class_exists( 'SiteFort\\Modules\\Scanner\\Repository\\ScanStatusRepository' ) ) {
                $events = ScanStatusRepository::get_recent( $limit );
                if ( ! empty( $events ) ) {
                     $entries = [];
                     foreach ( $events as $event ) {
                        $props = is_object($event) ? get_object_vars($event) : $event;
                        
                        $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;
                }
            }

            $path = self::get_log_path();

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

            $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 = [
            '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...',
            'Clearing scan history...' => 'Purging Previous Audit Records...',
        ];

        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( 'sitefort_nonce', '_ajax_nonce' );

            if ( ! \SiteFort\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()
            ] );
        }
    }
}
