<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Service;

use SecurWP\Includes\SettingsStore;

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


/**
 * ResourcePressure
 *
 * Centralized pressure computation for scanner phases.
 *
 */
class ResourcePressure {

	private const DEFAULT_MEMORY_LIMIT = 80;
	private const QUEUE_CRITICAL = 50;
	private const QUEUE_OPTIMAL = 20;
	private const BUFFER_MODERATE = 20971520;
	// Asymmetric EMA: react fast to rising pressure, slow to release.
	// Prevents premature exit from elevated states due to single clean readings.
	private const EMA_ALPHA_UP   = 0.7;
	private const EMA_ALPHA_DOWN = 0.3;
	private const CRITICAL_OFF_RATIO = 0.70;
	private const MODERATE_ON_RATIO  = 0.85;
	private const OPTIMAL_MEM_RATIO  = 0.80;
	// Absolute free-memory floor. Triggers moderate regardless of ratio.
	// Protects constrained hosts (e.g. 64MB PHP limit) where the cap leaves
	// very little headroom in absolute terms.
	private const MIN_FREE_MEMORY_MB = 20;

	/**
	 * Compute pressure state from metrics and external signals.
	 *
	 * CPU load average (sys_getloadavg) is intentionally excluded from pressure
	 * decisions. On shared hosting it reflects the entire server's aggregate load
	 * across all tenants, not this process's usage - making it an unreliable and
	 * frequently misleading signal that causes false CRITICAL on busy servers.
	 * Pressure is derived from memory % (process-specific), cloud queue depth,
	 * and upload buffer size (application-level signals).
	 *
	 * @param array $metrics Metrics from AIMD sensing.
	 * @param array $signals Optional external pressure signals.
	 * @param string|null $scan_id Scan ID for hysteresis persistence.
	 * @return array{memory_cap: int, near_limit: bool, critical: bool, moderate: bool, optimal: bool, level: string}
	 */
	public static function compute( array $metrics, array $signals = [], ?string $scan_id = null ): array {
		$limits  = self::get_limits();
		$mem_cap = $limits['memory_percent'];

		$mem    = (float) ( $metrics['memory'] ?? 0 );
		$queue  = (int)   ( $metrics['cloud_queue'] ?? 0 );
		$buffer = (int)   ( $metrics['buffer_size'] ?? 0 );

		$mem_metric = $mem;
		$state      = self::load_state( $scan_id );

		if ( $state ) {
			$mem_metric = self::ema( $mem, (float) ( $state['mem_ema'] ?? $mem ) );
		}

		$mem_ratio = $mem_cap > 0 ? ( $mem_metric / $mem_cap ) : 0;

		// Pressure levels driven by memory % and application-level signals only.
		$critical_now = ( $mem_ratio >= 1.0 ) || ( $queue > self::QUEUE_CRITICAL );
		$moderate_now = ( $mem_ratio >= self::MODERATE_ON_RATIO )
			|| ( $buffer > self::BUFFER_MODERATE );

		// Absolute free-memory floor: protect low-memory hosts where ratio
		// alone underestimates danger (e.g. 64MB limit, 60% cap = only ~18MB free).
		$free_mb = (int) ( $metrics['memory_free_mb'] ?? PHP_INT_MAX );
		if ( $free_mb < self::MIN_FREE_MEMORY_MB ) {
			$moderate_now = true;
		}

		if ( ! empty( $signals['upload_failures'] ) ) {
			$moderate_now = true;
		}

		$critical_off = ( $mem_ratio < self::CRITICAL_OFF_RATIO )
			&& ( $queue < (int) ( self::QUEUE_CRITICAL * 0.8 ) );

		// Moderate has no separate off-threshold: exit as soon as none of the
		// on-conditions are met. A memory off-threshold would permanently trap
		// scans in moderate when WP baseline memory already exceeds that threshold.
		$moderate_off = ! $moderate_now;

		$optimal_now = ( $mem_ratio < self::OPTIMAL_MEM_RATIO )
			&& ( $queue < self::QUEUE_OPTIMAL )
			&& ! $moderate_now;

		$level = $state['level'] ?? 'normal';
		if ( $level === 'critical' && ! $critical_off ) {
			$level = 'critical';
		} elseif ( $level === 'moderate' && ! $moderate_off && ! $critical_now ) {
			$level = 'moderate';
		} elseif ( $critical_now ) {
			$level = 'critical';
		} elseif ( $moderate_now ) {
			$level = 'moderate';
		} elseif ( $optimal_now ) {
			$level = 'optimal';
		} else {
			$level = 'normal';
		}

		self::save_state( $scan_id, $mem_metric, $level );

		$near_limit = ( $mem_ratio >= self::MODERATE_ON_RATIO );
		$critical   = ( $level === 'critical' );
		$moderate   = ( $level === 'moderate' );
		$optimal    = ( $level === 'optimal' );

		return [
			'memory_cap' => $mem_cap,
			'near_limit' => $near_limit,
			'critical'   => $critical,
			'moderate'   => $moderate,
			'optimal'    => $optimal,
			'level'      => $level,
		];
	}

	/**
	 * Persist EMA state for hysteresis.
	 *
	 * @param string|null $scan_id Scan ID.
	 * @param float $mem_metric Smoothed memory percent.
	 * @param string $level Pressure level.
	 * @return void
	 */
	private static function save_state( ?string $scan_id, float $mem_metric, string $level ): void {
		if ( empty( $scan_id ) ) {
			return;
		}

		SettingsStore::set_field(
			SettingsStore::COL_STATE,
			'pressure_state_' . $scan_id,
			[
				'mem_ema'    => $mem_metric,
				'level'      => $level,
				'updated_at' => time(),
			]
		);
	}

	/**
	 * Load EMA state for hysteresis.
	 *
	 * @param string|null $scan_id Scan ID.
	 * @return array<string, mixed>
	 */
	private static function load_state( ?string $scan_id ): array {
		if ( empty( $scan_id ) ) {
			return [];
		}

		$state = SettingsStore::get_field( SettingsStore::COL_STATE, 'pressure_state_' . $scan_id, [] );
		return is_array( $state ) ? $state : [];
	}

	/**
	 * Asymmetric exponential moving average for stable pressure signals.
	 *
	 * Uses a higher alpha when pressure rises (fast detection) and a lower
	 * alpha when pressure falls (slow release). This prevents premature exit
	 * from elevated states caused by a single clean reading between spikes.
	 *
	 * @param float $current Current measurement.
	 * @param float $previous Prior EMA.
	 * @return float Smoothed value.
	 */
	private static function ema( float $current, float $previous ): float {
		$alpha = $current > $previous ? self::EMA_ALPHA_UP : self::EMA_ALPHA_DOWN;
		return ( $alpha * $current ) + ( ( 1 - $alpha ) * $previous );
	}

	/**
	 * Return memory pressure cap.
	 *
	 * @return array{memory_percent: int}
	 */
	private static function get_limits(): array {
		return [
			'memory_percent' => self::DEFAULT_MEMORY_LIMIT,
		];
	}

	/**
	 * Clear persisted pressure state for a scan.
	 *
	 * @param string $scan_id Scan ID.
	 * @return void
	 */
	public static function clear_state( string $scan_id ): void {
		if ( $scan_id === '' ) {
			return;
		}

		SettingsStore::delete_field( SettingsStore::COL_STATE, 'pressure_state_' . $scan_id );
	}
}
