<?php
/**
 * Performance Monitor
 *
 * Monitors module performance during init. Hook deltas are tracked when the
 * Performance Monitor is enabled in Settings; init time and memory use the same gate.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

/**
 * Performance Monitor Class
 *
 * Tracks hook budgets on every request (via {@see HookTracker}) and optional
 * init time / memory when {@see PerformanceBudgetConfig::isDetailedMetricsEnabled()}.
 * Fatal-error shutdown handling runs only when
 * {@see PerformanceBudgetConfig::shouldRegisterFatalErrorHandler()} is true.
 *
 * @since 1.0.0
 */
class PerformanceMonitor {

	/**
	 * Performance Monitor Instance
	 *
	 * Singleton instance of the PerformanceMonitor class.
	 *
	 * @var PerformanceMonitor|null
	 * @since 1.0.0
	 */
	private static ?PerformanceMonitor $instance = null;

	/**
	 * Whether the fatal-error shutdown handler has been registered this request.
	 */
	private static bool $shutdown_registered = false;

	/**
	 * Is Detailed Metrics Mode Active
	 *
	 * Whether init time and memory are tracked (Performance Monitor enabled).
	 *
	 * @var bool
	 * @since 1.0.0
	 */
	private bool $detailed_metrics = false;

	/**
	 * Module Hook Counts
	 *
	 * Tracks hook count per module.
	 *
	 * @var array<string, int>
	 * @since 1.0.0
	 */
	private array $module_hook_counts = [];

	/**
	 * Module Execution Times
	 *
	 * Tracks execution time per module (debug mode only).
	 *
	 * @var array<string, float>
	 * @since 1.0.0
	 */
	private array $module_execution_times = [];

	/**
	 * Module Memory Usage
	 *
	 * Tracks memory usage per module (debug mode only).
	 *
	 * @var array<string, int>
	 * @since 1.0.0
	 */
	private array $module_memory_usage = [];

	/**
	 * Module Start Times
	 *
	 * Tracks when module initialization started (debug mode only).
	 *
	 * @var array<string, float>
	 * @since 1.0.0
	 */
	private array $module_start_times = [];

	/**
	 * Module Start Memory
	 *
	 * Tracks memory usage at module start (debug mode only).
	 *
	 * @var array<string, int>
	 * @since 1.0.0
	 */
	private array $module_start_memory = [];

	/**
	 * Maximum Hooks Per Module
	 *
	 * Maximum number of hooks allowed per module.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	private int $max_hooks_per_module = 40;

	/**
	 * Maximum Execution Time (ms)
	 *
	 * Maximum execution time allowed per module in milliseconds.
	 *
	 * @var float
	 * @since 1.0.0
	 */
	private float $max_execution_time = 100.0;

	/**
	 * Maximum Memory Usage (bytes)
	 *
	 * Maximum memory usage allowed per module in bytes.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	private int $max_memory_usage = 5242880; // 5MB

	/**
	 * Disabled Modules
	 *
	 * Modules that have been auto-disabled due to errors.
	 *
	 * @var array<string, string> Module ID => Error message
	 * @since 1.0.0
	 */
	private array $disabled_modules = [];

	/**
	 * Get Performance Monitor Instance
	 *
	 * Returns the singleton instance of the PerformanceMonitor class.
	 *
	 * @since 1.0.0
	 * @return PerformanceMonitor PerformanceMonitor instance.
	 */
	public static function getInstance(): PerformanceMonitor {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 *
	 * Private constructor to enforce singleton pattern.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		$this->max_hooks_per_module = PerformanceBudgetConfig::maxHooksPerModule();
		$this->max_execution_time   = PerformanceBudgetConfig::maxInitMsPerModule();
		$this->detailed_metrics     = PerformanceBudgetConfig::isDetailedMetricsEnabled();

		self::maybeRegisterShutdownHandler();
	}

	/**
	 * Register one process-wide shutdown handler for module fatal errors.
	 */
	private static function maybeRegisterShutdownHandler(): void {
		if ( self::$shutdown_registered || ! PerformanceBudgetConfig::shouldRegisterFatalErrorHandler() ) {
			return;
		}

		register_shutdown_function( [ self::class, 'handleShutdown' ] );
		self::$shutdown_registered = true;
	}

	/**
	 * Prevent Cloning
	 *
	 * Prevents cloning of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function __clone() {
		// Prevent cloning
	}

	/**
	 * Prevent Unserialization
	 *
	 * Prevents unserialization of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function __wakeup(): void {
		throw new \Exception( 'Cannot unserialize singleton' );
	}

	/**
	 * Start Module Monitoring
	 *
	 * Initializes hook count tracking. Init time and memory are recorded only when
	 * detailed metrics mode is active.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return void
	 */
	public function startModuleMonitoring( string $module_id ): void {
		if ( ! PerformanceBudgetConfig::shouldTrackModuleHooks() ) {
			return;
		}

		$this->module_hook_counts[ $module_id ] = 0;

		if ( $this->detailed_metrics ) {
			$this->module_start_times[ $module_id ]  = microtime( true );
			$this->module_start_memory[ $module_id ] = memory_get_usage( false );
		}
	}

	/**
	 * End Module Monitoring
	 *
	 * Checks runtime hook budget (when tracking is enabled) and records init
	 * time / memory when detailed metrics mode is active.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return void
	 */
	public function endModuleMonitoring( string $module_id ): void {
		if ( PerformanceBudgetConfig::shouldTrackModuleHooks() ) {
			$hook_count = $this->module_hook_counts[ $module_id ] ?? 0;
			if ( $hook_count > $this->max_hooks_per_module ) {
				error_log(
					sprintf(
						'WP PowerSuite: Module "%s" hook count (%d) exceeds maximum (%d).',
						$module_id,
						$hook_count,
						$this->max_hooks_per_module
					)
				);
			}
		}

		if ( ! $this->detailed_metrics ) {
			return;
		}

		$start_time   = $this->module_start_times[ $module_id ] ?? null;
		$start_memory = $this->module_start_memory[ $module_id ] ?? null;

		if ( $start_time !== null ) {
			$execution_time = ( microtime( true ) - $start_time ) * 1000; // Convert to milliseconds
			$this->module_execution_times[ $module_id ] = $execution_time;

			if ( $execution_time > $this->max_execution_time ) {
				$this->logWarning(
					$module_id,
					sprintf(
						'Execution time (%.2f ms) exceeds maximum (%.2f ms).',
						$execution_time,
						$this->max_execution_time
					)
				);
			}
		}

		if ( $start_memory !== null ) {
			// Use real memory usage (not allocated) for more accurate tracking
			$end_memory  = memory_get_usage( false );
			$memory_used = $end_memory - $start_memory;
			$this->module_memory_usage[ $module_id ] = max( 0, $memory_used );

			if ( $memory_used > $this->max_memory_usage ) {
				$this->logWarning(
					$module_id,
					sprintf(
						'Memory usage (%s) exceeds maximum (%s).',
						$this->formatBytes( $memory_used ),
						$this->formatBytes( $this->max_memory_usage )
					)
				);
			}
		}
	}

	/**
	 * Increment Hook Count
	 *
	 * Increments the hook count for a module. Called when a hook is registered.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return void
	 */
	public function incrementHookCount( string $module_id ): void {
		if ( ! isset( $this->module_hook_counts[ $module_id ] ) ) {
			$this->module_hook_counts[ $module_id ] = 0;
		}
		++$this->module_hook_counts[ $module_id ];
	}

	/**
	 * Get Hook Count
	 *
	 * Returns the hook count for a module.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return int Hook count.
	 */
	public function getHookCount( string $module_id ): int {
		return $this->module_hook_counts[ $module_id ] ?? 0;
	}

	/**
	 * Handle Module Error
	 *
	 * Handles errors during module initialization or execution.
	 * Auto-disables the module if it's a fatal error.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @param \Throwable $error Error object.
	 * @return void
	 */
	public function handleModuleError( string $module_id, \Throwable $error ): void {
		$error_message = $error->getMessage();
		$error_type = get_class( $error );

		error_log(
			sprintf(
				'WP PowerSuite: Module "%s" error (%s): %s in %s:%d',
				$module_id,
				$error_type,
				$error_message,
				$error->getFile(),
				$error->getLine()
			)
		);

		// Auto-disable module on fatal errors
		if ( $error instanceof \Error || $error instanceof \ParseError ) {
			$this->autoDisableModule( $module_id, $error_message );
		}
	}

	/**
	 * Auto-Disable Module
	 *
	 * Automatically disables a module due to a fatal error.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @param string $error_message Error message.
	 * @return void
	 */
	private function autoDisableModule( string $module_id, string $error_message ): void {
		$this->disabled_modules[ $module_id ] = $error_message;

		try {
			$option_store = OptionStore::getInstance();
			$option_store->disableModule( $module_id );

			$module_registry = ModuleRegistry::getInstance();
			$module_registry->init();
			$module_metadata = $module_registry->getModuleMetadata( $module_id );
			do_action( 'wp_powersuite_module_toggled', $module_id, false, $module_metadata ?: [] );

			error_log(
				sprintf(
					'WP PowerSuite: Auto-disabled module "%s" due to fatal error: %s',
					$module_id,
					$error_message
				)
			);
		} catch ( \Exception $e ) {
			error_log(
				sprintf(
					'WP PowerSuite: Failed to auto-disable module "%s": %s',
					$module_id,
					$e->getMessage()
				)
			);
		}
	}

	/**
	 * Handle Shutdown
	 *
	 * Handles PHP fatal errors during shutdown when the handler is registered.
	 * Lightweight early returns when there is no fatal error.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function handleShutdown(): void {
		$error = error_get_last();

		if ( $error === null ) {
			return;
		}

		if ( ! in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ], true ) ) {
			return;
		}

		$error_file = $error['file'] ?? '';
		if ( strpos( $error_file, 'modules' ) === false ) {
			return;
		}

		if ( ! preg_match( '/modules[\/\\\\]([^\/\\\\]+)/', $error_file, $matches ) ) {
			return;
		}

		self::getInstance()->autoDisableModule(
			sanitize_key( $matches[1] ),
			$error['message'] ?? 'Fatal error'
		);
	}

	/**
	 * Log Warning
	 *
	 * Logs a performance warning for a module. Only logs when detailed metrics are enabled.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @param string $message Warning message.
	 * @return void
	 */
	private function logWarning( string $module_id, string $message ): void {
		if ( ! $this->detailed_metrics ) {
			return;
		}

		error_log(
			sprintf(
				'WP PowerSuite Performance: Module "%s" - %s',
				$module_id,
				$message
			)
		);
	}

	/**
	 * Format Bytes
	 *
	 * Formats bytes into human-readable format.
	 *
	 * @since 1.0.0
	 * @param int $bytes Bytes to format.
	 * @return string Formatted string.
	 */
	private function formatBytes( int $bytes ): string {
		$units = [ 'B', 'KB', 'MB', 'GB' ];
		$bytes = max( $bytes, 0 );
		$pow = floor( ( $bytes ? log( $bytes ) : 0 ) / log( 1024 ) );
		$pow = min( $pow, count( $units ) - 1 );
		$bytes /= pow( 1024, $pow );

		return round( $bytes, 2 ) . ' ' . $units[ $pow ];
	}

	/**
	 * Get Module Metrics
	 *
	 * Returns hook count (when tracked this request) and optional init time / memory.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return array<string, mixed> Module metrics.
	 */
	public function getModuleMetrics( string $module_id ): array {
		$metrics = [
			'hook_count' => $this->getHookCount( $module_id ),
		];

		// Only include timing/memory when detailed metrics are enabled.
		if ( $this->detailed_metrics ) {
			$metrics['execution_time'] = $this->module_execution_times[ $module_id ] ?? null;
			$metrics['memory_usage'] = $this->module_memory_usage[ $module_id ] ?? null;
		}

		return $metrics;
	}

	/**
	 * Get All Metrics
	 *
	 * Returns metrics collected for modules initialized during this request.
	 *
	 * @since 1.0.0
	 * @return array<string, array> All module metrics.
	 */
	public function getAllMetrics(): array {
		$all_metrics = [];

		foreach ( array_keys( $this->module_hook_counts ) as $module_id ) {
			$all_metrics[ $module_id ] = $this->getModuleMetrics( $module_id );
		}

		return $all_metrics;
	}

	/**
	 * Check if Module is Disabled
	 *
	 * Checks if a module has been auto-disabled due to errors.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return bool True if disabled, false otherwise.
	 */
	public function isModuleDisabled( string $module_id ): bool {
		return isset( $this->disabled_modules[ $module_id ] );
	}

	/**
	 * Get Disabled Module Error
	 *
	 * Returns the error message for a disabled module.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return string|null Error message or null if not disabled.
	 */
	public function getDisabledModuleError( string $module_id ): ?string {
		return $this->disabled_modules[ $module_id ] ?? null;
	}

	/**
	 * Set Maximum Hooks Per Module
	 *
	 * Sets the maximum number of hooks allowed per module.
	 *
	 * @since 1.0.0
	 * @param int $max Maximum hooks.
	 * @return void
	 */
	public function setMaxHooksPerModule( int $max ): void {
		$this->max_hooks_per_module = max( 1, $max );
	}

	/**
	 * Get Maximum Hooks Per Module
	 *
	 * Returns the maximum number of hooks allowed per module.
	 *
	 * @since 1.0.0
	 * @return int Maximum hooks.
	 */
	public function getMaxHooksPerModule(): int {
		return $this->max_hooks_per_module;
	}

	/**
	 * Check if detailed metrics mode is active (Performance Monitor enabled).
	 *
	 * @since 1.0.0
	 * @return bool True when init time and memory are tracked.
	 */
	public function isDebugMode(): bool {
		return $this->detailed_metrics;
	}

	/**
	 * @alias isDebugMode()
	 */
	public function isDetailedMetricsEnabled(): bool {
		return $this->detailed_metrics;
	}
}

