<?php
/**
 * Authenticated manual scan endpoints.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Infrastructure\WordPress;

use LocalGuardScanner\Application\Coverage\CoverageReporter;
use LocalGuardScanner\Application\Scan\ScanActivity;
use LocalGuardScanner\Application\Scan\ScanFailure;
use LocalGuardScanner\Application\Scan\ScanFailureCatalog;
use LocalGuardScanner\Application\Scan\ScanOrchestrator;
use LocalGuardScanner\Application\Scan\ScanProgress;
use Throwable;

/**
 * Exposes short AJAX requests for starting, advancing, and cancelling scans.
 */
final class ManualScanController {
	const NONCE_ACTION = 'localguard_scanner_scan';

	/** @var ScanOrchestrator */
	private $orchestrator;

	/** @param ScanOrchestrator $orchestrator Orchestrator. */
	public function __construct( ScanOrchestrator $orchestrator ) {
		$this->orchestrator = $orchestrator;
	}

	/** @return void */
	public function register() {
		add_action( 'wp_ajax_localguard_start_scan', array( $this, 'start' ) );
		add_action( 'wp_ajax_localguard_run_scan', array( $this, 'run' ) );
		add_action( 'wp_ajax_localguard_cancel_scan', array( $this, 'cancel' ) );
	}

	/** @return void */
	public function start() {
		$this->authorize();
		try {
			$scan_id = $this->orchestrator->start( 'manual' );
			wp_send_json_success( array( 'scan_id' => $scan_id ) );
		} catch ( Throwable $error ) {
			$failure = $error instanceof ScanFailure ? ScanFailureCatalog::describe( $error->failure_code() ) : ScanFailureCatalog::describe( 'LGS-UNEXPECTED' );
			wp_send_json_error(
				array(
					'message' => $failure['message'],
					'failure' => $failure,
				),
				500
			);
		}
	}

	/** @return void */
	public function run() {
		$this->authorize();
		$scan_id = isset( $_POST['scan_id'] ) ? absint( $_POST['scan_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- authorize() verifies immediately before this line.
		try {
			$scan = $this->orchestrator->run_slice( $scan_id );
			wp_send_json_success( $this->public_scan( $scan ) );
		} catch ( Throwable $error ) {
			try {
				$scan = $this->orchestrator->scans()->find( $scan_id );
				if ( is_array( $scan ) && 'failed' === $scan['status'] ) {
					wp_send_json_success( $this->public_scan( $scan ) );
				}
			} catch ( Throwable $read_error ) {
				unset( $read_error );
			}
			$failure = $error instanceof ScanFailure ? ScanFailureCatalog::describe( $error->failure_code() ) : ScanFailureCatalog::describe( 'LGS-UNEXPECTED' );
			wp_send_json_error(
				array(
					'message' => $failure['message'],
					'failure' => $failure,
				),
				500
			);
		}
	}

	/** @return void */
	public function cancel() {
		$this->authorize();
		$scan_id = isset( $_POST['scan_id'] ) ? absint( $_POST['scan_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- authorize() verifies immediately before this line.
		wp_send_json_success( array( 'cancelled' => $this->orchestrator->cancel( $scan_id ) ) );
	}

	/** @return void */
	private function authorize() {
		if ( ! Capabilities::current_user_can_manage() ) {
			wp_send_json_error( array( 'message' => __( 'You are not allowed to run security scans.', 'localguard-scanner' ) ), 403 );
		}
		check_ajax_referer( self::NONCE_ACTION, 'nonce' );
	}

	/**
	 * @param array<string,mixed> $scan Internal scan row.
	 * @return array<string,mixed>
	 */
	private function public_scan( array $scan ) {
		$phase           = isset( $scan['phase'] ) && is_string( $scan['phase'] ) ? sanitize_key( $scan['phase'] ) : 'queued';
		$progress_stored = isset( $scan['progress_json'] ) && is_string( $scan['progress_json'] ) && '' !== $scan['progress_json'];
		$progress        = ScanProgress::from_json( $progress_stored ? $scan['progress_json'] : '' )->public_data( $phase );
		if ( ! $progress_stored && ! in_array( $phase, array( 'completed', 'partial' ), true ) ) {
			$progress['percentage'] = null;
		}
		$last_checkpoint = isset( $scan['last_checkpoint_at'] ) && is_string( $scan['last_checkpoint_at'] ) ? $scan['last_checkpoint_at'] : '';
		$status          = isset( $scan['status'] ) && is_string( $scan['status'] ) ? sanitize_key( $scan['status'] ) : 'failed';
		$failure         = null;
		$coverage        = ( new CoverageReporter() )->from_json( isset( $scan['coverage_json'] ) && is_string( $scan['coverage_json'] ) ? $scan['coverage_json'] : '' );
		if ( 'failed' === $status ) {
			$failure_code          = isset( $scan['failure_code'] ) ? ScanFailureCatalog::sanitize_code( $scan['failure_code'] ) : 'LGS-UNEXPECTED';
			$failure               = ScanFailureCatalog::describe( $failure_code );
			$failure['stage']      = isset( $scan['failure_stage'] ) ? ScanFailureCatalog::sanitize_stage( $scan['failure_stage'] ) : $failure['stage'];
			$failure['diagnostic'] = $this->diagnostic( $scan, $failure );
		}
		return array(
			'id'                 => (int) $scan['id'],
			'status'             => $status,
			'phase'              => $phase,
			'error_count'        => (int) $scan['error_count'],
			'finished'           => in_array( $status, array( 'completed', 'partial', 'cancelled', 'failed' ), true ),
			'activity'           => $this->activity( $status, $last_checkpoint ),
			'last_checkpoint_at' => $last_checkpoint,
			'progress'           => $progress,
			'coverage'           => $coverage,
			'failure'            => $failure,
		);
	}

	/** @param string $status Scan status. @param string $checkpoint UTC MySQL datetime. @return string */
	private function activity( string $status, string $checkpoint ): string {
		$timestamp = '' !== $checkpoint ? strtotime( $checkpoint . ' UTC' ) : false;
		$age       = false === $timestamp ? 121 : max( 0, time() - $timestamp );
		return ScanActivity::derive( $status, $age );
	}

	/**
	 * Build a copyable diagnostic that never includes inspected content or paths.
	 *
	 * @param array<string,mixed> $scan    Scan row.
	 * @param array<string,string> $failure Safe failure.
	 * @return string
	 */
	private function diagnostic( array $scan, array $failure ) {
		return implode(
			"\n",
			array(
				'LocalGuard Scanner ' . LOCALGUARD_SCANNER_VERSION,
				'WordPress ' . get_bloginfo( 'version' ) . ' / PHP ' . PHP_VERSION,
				'Scan ' . (int) $scan['id'] . ' / Schema ' . \LocalGuardScanner\Infrastructure\Database\Schema::VERSION,
				'Stage ' . $failure['stage'] . ' / Code ' . $failure['code'],
				'UTC ' . gmdate( 'Y-m-d H:i:s' ),
			)
		);
	}
}
