<?php

declare(strict_types=1);

namespace SecurWP\Modules\Vulnerability\Services;

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

use SecurWP\Includes\SettingsStore;
use SecurWP\Modules\Scanner\Repository\IssueRepository;

class VulnerabilityRefreshService {

	private const REFRESH_LOCK_OPTION = 'securwp_vulnerability_refresh_lock';
	private const REFRESH_LOCK_TTL = 120;

	public function refresh() {
		if ( ! $this->acquire_refresh_lock() ) {
			return new \WP_Error(
				'vulnerability_scan_locked',
				'A vulnerability sync is already running. Please wait a few seconds and retry.'
			);
		}

		try {
			if ( $this->isFullScanActive() ) {
				return new \WP_Error(
					'vulnerability_scan_busy',
					'A scan is already running. Wait for it to finish before refreshing vulnerabilities.'
				);
			}

			if ( ! $this->ensure_dependencies() ) {
				return new \WP_Error( 'scanner_unavailable', 'Vulnerability scanner components are unavailable.' );
			}

			if ( ! $this->sync_cloud_vulnerabilities() ) {
				return new \WP_Error(
					'vulnerability_sync_failed',
					$this->get_last_sync_error_message()
				);
			}

			$previous_vulnerability_issues = self::getActiveVulnerabilityIssues();
			$scan_id = 'update_vuln_' . wp_generate_uuid4();

			$controller = new \SecurWP\Modules\Scanner\ScanController();
			$controller->register_scanner( 'vulnerabilities', new \SecurWP\Modules\Scanner\VulnerabilityScanner( false ) );

			try {
				$scan_result = $controller->run_scan( 'vulnerabilities', $scan_id );
			} catch ( \Throwable $e ) {
				return new \WP_Error(
					'vulnerability_scan_failed',
					'Vulnerability sync failed: ' . $e->getMessage()
				);
			}
			if ( is_array( $scan_result ) && ! empty( $scan_result['failed'] ) ) {
				return new \WP_Error( 'vulnerability_scan_failed', 'Vulnerability refresh could not complete.' );
			}

			$published = IssueRepository::publish( $scan_id, [ 'vulnerability' ] );
			if ( $published === false ) {
				return new \WP_Error( 'vulnerability_publish_failed', 'Failed to publish vulnerability scan results.' );
			}

			$current_vulnerability_issues = self::getActiveVulnerabilityIssues();
			$active_count = self::syncActiveIssueCount();

			SettingsStore::set_field( SettingsStore::COL_STATE, 'vulnerability_results', [
				'timestamp' => time(),
				'issues'    => $current_vulnerability_issues,
			] );

			do_action( 'securwp_scan_results_changed', [
				'action'       => 'vulnerability_refresh',
				'scan_id'      => $scan_id,
				'scope'        => 'vulnerability',
				'active_count' => $active_count,
			] );

			self::dispatchNewVulnerabilityNotifications( $previous_vulnerability_issues, $current_vulnerability_issues );

			return [
				'scan_id'      => $scan_id,
				'issues'       => $current_vulnerability_issues,
				'active_count' => $active_count,
			];
		} finally {
			$this->release_refresh_lock();
		}
	}

	public static function getActiveVulnerabilityIssues(): array {
		if ( ! self::ensure_issue_repository_loaded() ) {
			return [];
		}

		return IssueRepository::getActive( 5000, 0, 'vulnerability' );
	}

	public static function syncActiveIssueCount(): int {
		if ( ! self::ensure_issue_repository_loaded() ) {
			return 0;
		}

		$counts = IssueRepository::getCounts();
		$active_count = is_array( $counts ) ? (int) ( $counts['active'] ?? 0 ) : 0;
		$system_error_count = 0;

		if ( is_array( $counts ) && isset( $counts['by_type'] ) && is_array( $counts['by_type'] ) ) {
			$system_error_count = (int) ( $counts['by_type']['system_error'] ?? 0 );
		}

		$active_count = max( 0, $active_count - $system_error_count );
		SettingsStore::set_field( SettingsStore::COL_STATE, 'scan_issues_count', $active_count );

		return $active_count;
	}

	public static function dispatchNewVulnerabilityNotifications( array $previous, array $current ): void {
		$known_hashes = [];
		foreach ( $previous as $issue ) {
			$hash = (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? '' );
			if ( $hash !== '' ) {
				$known_hashes[ $hash ] = true;
			}
		}

		$new_payloads = [];
		foreach ( $current as $issue ) {
			$hash = (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? '' );
			if ( $hash === '' || isset( $known_hashes[ $hash ] ) ) {
				continue;
			}

			// Filter at source: the batched event has no per-item suppression.
			if ( ! empty( $issue['is_ignored'] ) ) {
				continue;
			}

			$new_payloads[] = self::build_notification_payload( $issue );
		}

		if ( empty( $new_payloads ) ) {
			return;
		}

		do_action( 'securwp_vulns_found_batch', $new_payloads );
	}

	private function ensure_dependencies(): bool {
		if ( ! class_exists( '\SecurWP\Modules\Cloud\Services\VulnerabilitySync' ) ) {
			$path = dirname( dirname( __DIR__ ) ) . '/Cloud/Services/VulnerabilitySync.php';
			if ( file_exists( $path ) ) {
				require_once $path;
			}
		}
		if ( ! class_exists( '\SecurWP\Modules\Scanner\ScanController' ) ) {
			$path = dirname( dirname( __DIR__ ) ) . '/Scanner/ScanController.php';
			if ( file_exists( $path ) ) {
				require_once $path;
			}
		}
		if ( ! class_exists( '\SecurWP\Modules\Scanner\VulnerabilityScanner' ) ) {
			$path = dirname( dirname( __DIR__ ) ) . '/Scanner/VulnerabilityScanner.php';
			if ( file_exists( $path ) ) {
				require_once $path;
			}
		}

		return class_exists( '\SecurWP\Modules\Cloud\Services\VulnerabilitySync' )
			&& class_exists( '\SecurWP\Modules\Scanner\ScanController' )
			&& class_exists( '\SecurWP\Modules\Scanner\VulnerabilityScanner' )
			&& self::ensure_issue_repository_loaded();
	}

	private function sync_cloud_vulnerabilities(): bool {
		try {
			$syncer = new \SecurWP\Modules\Cloud\Services\VulnerabilitySync();
			return $syncer->run() === true;
		} catch ( \Throwable $e ) {
			SettingsStore::set_field(
				SettingsStore::COL_STATE,
				'vulnerability_last_sync_error',
				$e->getMessage()
			);
			return false;
		}
	}

	private function get_last_sync_error_message(): string {
		$message = (string) SettingsStore::get_field(
			SettingsStore::COL_STATE,
			'vulnerability_last_sync_error',
			''
		);

		return $message !== '' ? $message : 'Vulnerability intelligence sync failed.';
	}

	private static function build_notification_payload( array $issue ): array {
		$data = is_array( $issue['data'] ?? null ) ? $issue['data'] : [];
		$severity = strtolower( (string) ( $issue['severity'] ?? 'medium' ) );
		if ( ! in_array( $severity, [ 'low', 'medium', 'high', 'critical' ], true ) ) {
			$severity = 'medium';
		}

		$component_name = trim( (string) ( $data['component_name'] ?? $data['asset_name'] ?? '' ) );
		if ( $component_name === '' ) {
			$component_name = trim( (string) ( $issue['name'] ?? $issue['title'] ?? $issue['message'] ?? 'Unknown Component' ) );
		}

		$cve = trim( (string) ( $data['cve'] ?? $issue['cve'] ?? '' ) );
		$issue_hash = trim( (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? '' ) );
		$is_ignored = ! empty( $issue['is_ignored'] );

		return [
			'site_url'       => home_url(),
			'plugin_name'    => $component_name,
			'component_name' => $component_name,
			'cve_id'         => $cve,
			'severity'       => $severity,
			'issue_hash'     => $issue_hash,
			'disposition'    => $is_ignored ? 'ignored' : 'active',
			'is_ignored'     => $is_ignored,
		];
	}

	private static function ensure_issue_repository_loaded(): bool {
		if ( class_exists( IssueRepository::class ) ) {
			return true;
		}

		$path = dirname( dirname( __DIR__ ) ) . '/Scanner/Repository/IssueRepository.php';
		if ( file_exists( $path ) ) {
			require_once $path;
		}

		return class_exists( IssueRepository::class );
	}

	private function acquire_refresh_lock(): bool {
		$now = time();
		$new_expiry = (string) ( $now + self::REFRESH_LOCK_TTL );

		if ( add_option( self::REFRESH_LOCK_OPTION, $new_expiry, '', 'no' ) ) {
			return true;
		}

		$current_raw = (string) get_option( self::REFRESH_LOCK_OPTION, '' );
		$current_expiry = is_numeric( $current_raw ) ? (int) $current_raw : 0;
		if ( $current_expiry >= $now ) {
			return false;
		}

		global $wpdb;
		if ( ! isset( $wpdb ) || ! isset( $wpdb->options ) ) {
			return false;
		}

		$options_table = self::table_identifier( $wpdb->options );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		$sql = $wpdb->prepare(
			"UPDATE {$options_table} SET option_value = %s WHERE option_name = %s AND option_value = %s",
			$new_expiry,
			self::REFRESH_LOCK_OPTION,
			$current_raw
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Lock reclamation updates a single options row atomically.
		$updated = $wpdb->query( $sql );

		return $updated === 1;
	}

	private function release_refresh_lock(): void {
		delete_option( self::REFRESH_LOCK_OPTION );
	}

	private function isFullScanActive(): bool {
		if ( ! class_exists( '\SecurWP\Modules\Scanner\Queue\ScanQueue' ) ) {
			$path = dirname( dirname( __DIR__ ) ) . '/Scanner/Queue/ScanQueue.php';
			if ( file_exists( $path ) ) {
				require_once $path;
			}
		}

		return class_exists( '\SecurWP\Modules\Scanner\Queue\ScanQueue' )
			&& \SecurWP\Modules\Scanner\Queue\ScanQueue::is_active();
	}

	private static function table_identifier( string $table_name ): string {
		if ( $table_name === '' || strlen( $table_name ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table_name ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . $table_name . '`';
	}
}
