<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Service;

use SecurWP\Includes\SettingsStore;
use SecurWP\Modules\Scanner\Repository\CloudJobRepository;
use SecurWP\Modules\Scanner\Repository\FileStateRepository;

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

/**
 * Class AIMDController
 *
 * Implements the Additive Increase, Multiplicative Decrease (AIMD) congestion control algorithm
 * for the Scan Orchestrator. Manages dynamic window sizes for hashing and uploading based on
 * process memory usage and cloud backpressure (queue depth, upload buffer).
 * CPU load average is intentionally excluded - on shared hosting it reflects the entire
 * server aggregate load across all tenants, not this process's usage.
 * - Hard limits on window sizes.
 * - Circuit breaker for upload failures.
 * - Persistent state across worker restarts.
 * - Adaptive polling intervals.
 *
 */
class AIMDController {

	private const STATE_KEY_PREFIX = 'aimd_state_';
	private const MAX_RETRIES = 5;

	// Hard Limits
	private const MIN_HASH_WINDOW     = 50;
	private const MAX_HASH_WINDOW     = 500;
	private const MIN_DISCOVERY_WINDOW = 50;
	private const MAX_DISCOVERY_WINDOW = 500;
	private const MIN_UPLOAD_WINDOW   = 1;
	private const MAX_UPLOAD_WINDOW   = 40;
	private const MAX_CLOUD_QUEUE_LIMIT = 75;
	private const HASH_SIZE_BASELINE = 262144;
	private const UPLOAD_SIZE_BASELINE = 524288;

	// Defaults
	private const DEFAULT_HASH_WINDOW   = 250;
	private const DEFAULT_DISCOVERY_WINDOW = 150;
	private const DEFAULT_UPLOAD_WINDOW = 25;

	/** @var FileStateRepository */
	private $file_repo;

	/** @var CloudJobRepository */
	private $job_repo;

	/** @var string */
	private $scan_id;

	// State Variables
	private $hash_window;
	private $discovery_window;
	private $upload_window;
	private $upload_failures = 0;

	// Metrics cache: avoid repeated DB queries within the same tick
	private const METRICS_CACHE_TTL = 5; // seconds
	private $cached_metrics      = null;
	private $cached_metrics_time = 0;

	/**
	 * AIMDController constructor.
	 *
	 * @param string $scan_id Scan ID.
	 */
	public function __construct( string $scan_id ) {
		$this->scan_id   = $scan_id;
		$this->file_repo = new FileStateRepository();
		$this->job_repo  = new CloudJobRepository();
		$this->load_state();
	}

	/**
	 * Load state from persistent storage (DB).
	 */
	private function load_state(): void {
		$state = SettingsStore::get_field( SettingsStore::COL_STATE, self::STATE_KEY_PREFIX . $this->scan_id, [] );
		if ( ! is_array( $state ) ) {
			$state = [];
		}

		$this->hash_window     = (int) ( $state['hash_window'] ?? self::DEFAULT_HASH_WINDOW );
		$this->discovery_window = (int) ( $state['discovery_window'] ?? self::DEFAULT_DISCOVERY_WINDOW );
		$this->upload_window   = (int) ( $state['upload_window'] ?? self::DEFAULT_UPLOAD_WINDOW );
		$this->upload_failures = (int) ( $state['upload_failures'] ?? 0 );

		// Enforce Hard Limits immediately on load
		$this->enforce_limits();
	}

	/**
	 * Save state to persistent storage.
	 */
	public function save_state(): void {
		SettingsStore::set_field(
			SettingsStore::COL_STATE,
			self::STATE_KEY_PREFIX . $this->scan_id,
			[
				'hash_window'      => $this->hash_window,
				'discovery_window' => $this->discovery_window,
				'upload_window'    => $this->upload_window,
				'upload_failures'  => $this->upload_failures,
				'updated_at'       => time(),
			]
		);
	}

	/**
	 * Enforce hard limits on window sizes.
	 */
	private function enforce_limits(): void {
		$this->hash_window   = max( self::MIN_HASH_WINDOW, min( self::MAX_HASH_WINDOW, $this->hash_window ) );
		$this->upload_window = max( self::MIN_UPLOAD_WINDOW, min( self::MAX_UPLOAD_WINDOW, $this->upload_window ) );
		$this->discovery_window = max( self::MIN_DISCOVERY_WINDOW, min( self::MAX_DISCOVERY_WINDOW, $this->discovery_window ) );

		$discovery_cap = $this->hash_window * 2;
		if ( $this->discovery_window > $discovery_cap ) {
			$this->discovery_window = $discovery_cap;
		}
	}

	/**
	 * Sense Phase: Get current resource metrics.
	 *
	 * @return array{cpu: float, memory: float, cloud_queue: int, buffer_size: int, buffer_count: int, upload_avg_size: int, hash_avg_size: int}
	 */
	public function sense_resources(): array {
		$now = time();
		if ( $this->cached_metrics !== null && ( $now - $this->cached_metrics_time ) < self::METRICS_CACHE_TTL ) {
			return $this->cached_metrics;
		}

		// CPU load - collected for logging only, not used for pressure decisions.
		$cpu_load = 0.0;
		if ( function_exists( 'sys_getloadavg' ) ) {
			$load     = sys_getloadavg();
			$cpu_load = $load[0] ?? 0.0;
		}

		// 2. Memory Usage (Percent of limit)
		$mem_usage     = memory_get_usage( true );
		$mem_limit_str = ini_get( 'memory_limit' );
		$mem_limit     = $this->parse_memory_limit( $mem_limit_str );
		$mem_percent   = ( $mem_limit > 0 ) ? ( $mem_usage / $mem_limit ) * 100 : 0;
		$mem_free_mb   = $mem_limit > 0 ? (int) floor( ( $mem_limit - $mem_usage ) / ( 1024 * 1024 ) ) : PHP_INT_MAX;

		// 3. Queue Depths
		// Cloud Queue: Jobs 'submitted' or 'processing' (not completed/failed)
		$cloud_queue = $this->job_repo->count_by_scan_and_statuses(
			$this->scan_id,
			[ 'submitted', 'processing' ]
		);

		// 4. Local Buffer (Pending Upload files)
		$buffer_metrics   = $this->file_repo->get_scan_state_metrics( $this->scan_id, 'pending_upload' );
		$buffer_size      = (int) $buffer_metrics['total_size'];
		$buffer_count     = (int) $buffer_metrics['count'];
		$upload_avg_size  = (int) $buffer_metrics['average_size'];
		$hash_avg_size    = $this->file_repo->get_average_file_size_by_states( $this->scan_id, [ 'discovered', 'hashing' ] );

		$this->cached_metrics      = [
			'cpu'             => $cpu_load,      // 0.0 - 1.0+ (collected for logging only)
			'memory'          => $mem_percent,   // 0 - 100
			'memory_free_mb'  => $mem_free_mb,   // absolute MB free (floor guard for constrained hosts)
			'cloud_queue'     => $cloud_queue,
			'buffer_size'     => $buffer_size,
			'buffer_count'    => $buffer_count,
			'upload_avg_size' => $upload_avg_size,
			'hash_avg_size'   => $hash_avg_size,
		];
		$this->cached_metrics_time = $now;

		return $this->cached_metrics;
	}

	/**
	 * Helper to parse memory limit string (e.g. '256M').
	 */
	private function parse_memory_limit( $str ): int {
		if ( empty( $str ) || '-1' === $str ) {
			return 1024 * 1024 * 1024;
		}
		$val  = trim( $str );
		$last = strtolower( $val[ strlen( $val ) - 1 ] );
		$val  = (int) $val;
		switch ( $last ) {
			case 'g':
				$val *= 1024;
				// no break
			case 'm':
				$val *= 1024;
				// no break
			case 'k':
				$val *= 1024;
		}
		return $val;
	}

	/**
	 * Run the AIMD Sensing and Evaluation Cycle.
	 * Updates window sizes based on current pressure.
	 */
	public function evaluate_aimd_cycle(): void {
		$metrics = $this->sense_resources();
		$pressure = $this->get_pressure_state_from_metrics(
			$metrics,
			[ 'upload_failures' => $this->upload_failures ]
		);

		$is_critical_pressure = $pressure['critical'];
		$is_moderate_pressure = $pressure['moderate'];
		$is_optimal           = $pressure['optimal'];

		// --- AIMD LOGIC ---

		if ( $this->upload_failures > self::MAX_RETRIES ) {
			// STOP SCAN CONDITION - Handled by should_abort_scan()
			return;
		}

		if ( $is_critical_pressure ) {
			$this->hash_window   = (int) ceil( $this->hash_window * 0.5 );
			$this->discovery_window = (int) ceil( $this->discovery_window * 0.5 );
			$this->upload_window = (int) ceil( $this->upload_window * 0.5 );
			
		} elseif ( $is_moderate_pressure ) {
			$this->hash_window = (int) ceil( $this->hash_window * 0.75 );
			$this->discovery_window = (int) ceil( $this->discovery_window * 0.75 );
			// Keep Upload Window steady or set to min if failures
			if ( $this->upload_failures > 0 ) {
				$this->upload_window = self::MIN_UPLOAD_WINDOW; // Retry carefully
			}

		} elseif ( $is_optimal ) {
			$this->hash_window   += (int) max( 1, ceil( $this->hash_window * 0.25 ) );
			$this->discovery_window += (int) max( 1, ceil( $this->discovery_window * 0.25 ) );
			$this->upload_window += (int) max( 1, ceil( $this->upload_window * 0.25 ) );
		}

		$this->enforce_limits();
		$this->save_state();
	}

	/**
	 * Get discovery window adjusted for live resource pressure.
	 *
	 * @return int
	 */
	public function get_discovery_window_for_phase(): int {
		$metrics = $this->sense_resources();
		$pressure = $this->get_pressure_state_from_metrics( $metrics );
		$window   = $this->discovery_window;

		if ( $pressure['critical'] ) {
			$window = (int) ceil( $window * 0.25 );
		} elseif ( $pressure['near_limit'] ) {
			$window = (int) ceil( $window * 0.5 );
		}

		return max( self::MIN_DISCOVERY_WINDOW, min( $window, $this->hash_window * 2 ) );
	}

	/**
	 * Get hash window adjusted for pressure.
	 *
	 * @param array|null $pressure Optional pressure state.
	 * @return int
	 */
	public function get_hash_window_for_phase( ?array $pressure = null, ?array $metrics = null ): int {
		$pressure = $pressure ?? $this->get_pressure_state();
		$metrics  = $metrics ?? $this->sense_resources();

		$window = $this->hash_window;
		if ( $pressure['critical'] ) {
			// Never block completely - allow a small batch to make progress.
			$window = self::MIN_HASH_WINDOW;
		} elseif ( $pressure['moderate'] ) {
			// Halve the window but never below MIN_HASH_WINDOW.
			$window = max( self::MIN_HASH_WINDOW, (int) ceil( $window * 0.5 ) );
		}

		$avg_size = (int) ( $metrics['hash_avg_size'] ?? 0 );
		$window   = $this->apply_size_cost( $window, $avg_size, self::HASH_SIZE_BASELINE );

		return max( 1, $window );
	}

	/**
	 * Get upload window adjusted for pressure.
	 *
	 * @param array|null $pressure Optional pressure state.
	 * @return int
	 */
	public function get_upload_window_for_phase( ?array $pressure = null, ?array $metrics = null ): int {
		$pressure = $pressure ?? $this->get_pressure_state();
		$metrics  = $metrics ?? $this->sense_resources();
		$window   = $this->get_upload_window();

		if ( $window <= 0 ) {
			return 0;
		}

		if ( $pressure['critical'] ) {
			// Allow minimum upload throughput instead of a full stop.
			return self::MIN_UPLOAD_WINDOW;
		}

		if ( $pressure['moderate'] ) {
			$window = (int) ceil( $window * 0.5 );
		}

		$avg_size = (int) ( $metrics['upload_avg_size'] ?? 0 );
		$window   = $this->apply_size_cost( $window, $avg_size, self::UPLOAD_SIZE_BASELINE );

		return max( self::MIN_UPLOAD_WINDOW, $window );
	}

	/**
	 * Get polling interval adjusted for pressure.
	 *
	 * @param array|null $pressure Optional pressure state.
	 * @return int
	 */
	public function get_poll_interval_for_phase( ?array $pressure = null ): int {
		$pressure = $pressure ?? $this->get_pressure_state();
		$base     = $this->get_poll_interval();

		if ( $pressure['critical'] ) {
			return max( 10, min( 30, $base * 3 ) );
		}

		if ( $pressure['moderate'] ) {
			return max( 10, min( 30, $base * 2 ) );
		}

		return max( 10, min( 30, $base ) );
	}

	/**
	 * Apply file-size-aware cost to a window size.
	 *
	 * @param int $window Window size to scale.
	 * @param int $avg_size Average file size in bytes.
	 * @param int $baseline Baseline size for full window.
	 * @return int
	 */
	private function apply_size_cost( int $window, int $avg_size, int $baseline ): int {
		if ( $window <= 0 ) {
			return 0;
		}

		if ( $avg_size <= 0 ) {
			return $window;
		}

		$factor = $baseline / max( 1, $avg_size );
		$factor = max( 0.25, min( 1.0, $factor ) );

		return max( 1, (int) floor( $window * $factor ) );
	}

	/**
	 * Get unified pressure state for this scan.
	 *
	 * @return array{memory_cap: int, near_limit: bool, critical: bool, moderate: bool, optimal: bool, level: string}
	 */
	public function get_pressure_state(): array {
		return $this->get_pressure_state_from_metrics(
			$this->sense_resources(),
			[ 'upload_failures' => $this->upload_failures ]
		);
	}

	/**
	 * Get unified pressure state using provided metrics.
	 *
	 * @param array $metrics Metrics from sensing.
	 * @param array $signals Optional external pressure signals.
	 * @return array{memory_cap: int, near_limit: bool, critical: bool, moderate: bool, optimal: bool, level: string}
	 */
	public function get_pressure_state_from_metrics( array $metrics, array $signals = [] ): array {
		if ( ! class_exists( 'SecurWP\\Modules\\Scanner\\Service\\ResourcePressure' ) ) {
			require_once __DIR__ . '/ResourcePressure.php';
		}

		return ResourcePressure::compute( $metrics, $signals, $this->scan_id );
	}
	/**
	 * Calculate Adaptive Polling Interval.
	 *
	 * Logic:
	 * - Queue > 40: 10s (Urgent)
	 * - Queue > 20: 15s (Moderate)
	 * - Else: 30s (Relaxed)
	 *
	 * @return int Interval in seconds.
	 */
	public function get_poll_interval(): int {
		$cloud_queue = $this->job_repo->count_by_scan_and_statuses(
			$this->scan_id,
			[ 'submitted', 'processing' ]
		);

		if ( $cloud_queue > 40 ) {
			return 10;
		} elseif ( $cloud_queue > 20 ) {
			return 15;
		} else {
			return 30;
		}
	}

	/**
	 * Log server resource state at scan start for diagnostics.
	 *
	 * Fires once per scan (caller must guard with a transient).
	 * Logs PHP version, memory limit, current usage, free headroom,
	 * AIMD cap, and the computed moderate/critical thresholds in MB.
	 */
	public function log_startup_state(): void {
		if ( ! class_exists( '\SecurWP\Modules\Scanner\Service\ScanLogger' ) ) {
			require_once __DIR__ . '/ScanLogger.php';
		}
		if ( ! class_exists( '\SecurWP\Modules\Scanner\Service\ScanLogger' ) ) {
			return;
		}

		$metrics  = $this->sense_resources();
		$pressure = $this->get_pressure_state_from_metrics( $metrics );

		$mem_limit_str = ini_get( 'memory_limit' );
		$mem_limit     = $this->parse_memory_limit( $mem_limit_str );
		$mem_limit_mb  = (int) round( $mem_limit / ( 1024 * 1024 ) );
		$mem_used_mb   = (int) ceil( memory_get_usage( true ) / ( 1024 * 1024 ) );
		$mem_free_mb   = (int) ( $metrics['memory_free_mb'] ?? 0 );
		$mem_percent   = round( (float) ( $metrics['memory'] ?? 0 ), 1 );

		$disk_free     = @disk_free_space( ABSPATH );
		if ( $disk_free !== false ) {
			$disk_gb   = $disk_free / ( 1024 * 1024 * 1024 );
			$disk_str  = $disk_gb >= 1.0
				? sprintf( '%.1fGB', $disk_gb )
				: sprintf( '%dMB', (int) floor( $disk_free / ( 1024 * 1024 ) ) );
		} else {
			$disk_str = 'unknown';
		}

		\SecurWP\Modules\Scanner\Service\ScanLogger::log(
			sprintf(
				'[Scan Start] Memory: %dMB / %dMB (%.1f%%) | Free: %dMB | Disk: %s | Pressure: %s',
				$mem_used_mb,
				$mem_limit_mb,
				$mem_percent,
				$mem_free_mb,
				$disk_str,
				$pressure['level']
			)
		);
	}

	/**
	 * Check if the scan should be aborted due to persistent failures.
	 *
	 * @return bool
	 */
	public function should_abort_scan(): bool {
		return $this->upload_failures > self::MAX_RETRIES;
	}

	/**
	 * Record an upload failure.
	 *
	 * Rate-limit (429): fixed 65s backoff, does NOT count toward abort.
	 * Other failures: exponential backoff (2^n seconds, capped at 120s) + abort counter.
	 *
	 * @param bool $is_rate_limit If true, triggers a 65s backoff and does not increment abort counter.
	 */
	public function record_upload_failure( bool $is_rate_limit = false ): void {
		if ( $is_rate_limit ) {
			set_transient( 'securwp_rate_limit_backoff_' . $this->scan_id, true, 65 );
			return;
		}

		$this->upload_failures++;
		$this->save_state();

		// Exponential backoff: 2^failures seconds (2s, 4s, 8s, 16s, 32s...), capped at 120s.
		// Gives transient network issues time to clear without burning the abort counter quickly.
		$backoff_secs = min( 120, (int) pow( 2, $this->upload_failures ) );
		set_transient( 'securwp_upload_backoff_' . $this->scan_id, true, $backoff_secs );
	}

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

		SettingsStore::delete_field( SettingsStore::COL_STATE, self::STATE_KEY_PREFIX . $scan_id );
		delete_transient( 'securwp_upload_backoff_' . $scan_id );
	}

	/**
	 * Record an upload success. Resets failure counter and clears any active backoff.
	 */
	public function record_upload_success(): void {
		$this->upload_failures = 0;
		delete_transient( 'securwp_upload_backoff_' . $this->scan_id );
		$this->save_state();
	}

	/**
	 * Get current upload failure count.
	 * @return int
	 */
	public function get_failure_count(): int {
		return $this->upload_failures;
	}
	/**
	 * Get current Upload Window size.
	 * Respects Hard Limit on Cloud Queue.
	 * 
	 * @return int
	 */
	public function get_upload_window(): int {
		$queue = $this->job_repo->count_by_scan_and_statuses(
			$this->scan_id,
			[ 'submitted', 'processing' ]
		);

		// Rate-limit or exponential backoff active - pause uploads.
		if ( get_transient( 'securwp_rate_limit_backoff_' . $this->scan_id ) ) {
			return 0;
		}
		if ( get_transient( 'securwp_upload_backoff_' . $this->scan_id ) ) {
			return 0;
		}

		if ( $queue > self::MAX_CLOUD_QUEUE_LIMIT ) {
			return 0; // Pause uploads
		}

		return $this->upload_window;
	}
}
