<?php
/**
 * AJAX handlers for AccessLens.
 *
 * Actions registered (admin only, requires manage_options):
 *   accesslens_start_scan  — start a new scan job
 *   accesslens_poll_scan   — poll job status
 *   accesslens_get_report  — proxy report download
 *
 * @package AccessLens
 */

declare(strict_types=1);

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

/**
 * AccessLens_Ajax
 */
class AccessLens_Ajax {

	/** @var AccessLens_Engine_Client */
	private AccessLens_Engine_Client $client;

	/** Transient TTL: 4 hours. */
	private const JOB_TTL = 4 * HOUR_IN_SECONDS;

	/**
	 * @param AccessLens_Engine_Client $client Engine HTTP client.
	 */
	public function __construct( AccessLens_Engine_Client $client ) {
		$this->client = $client;
	}

	/**
	 * Register all AJAX actions.
	 */
	public function register(): void {
		add_action( 'wp_ajax_accesslens_start_scan', array( $this, 'handle_start_scan' ) );
		add_action( 'wp_ajax_accesslens_poll_scan', array( $this, 'handle_poll_scan' ) );
		add_action( 'wp_ajax_accesslens_get_report', array( $this, 'handle_get_report' ) );
	}

	// ----------------------------------------------------------------
	// Helpers
	// ----------------------------------------------------------------

	/**
	 * Verify nonce and capability, die on failure.
	 */
	private function verify_request(): void {
		check_ajax_referer( 'accesslens_audit_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permisos insuficientes.', 'accesslens' ) ), 403 );
		}
	}

	/**
	 * Get transient key for current user's active job.
	 */
	private function job_transient_key(): string {
		return 'accesslens_job_' . get_current_user_id();
	}

	// ----------------------------------------------------------------
	// Handlers
	// ----------------------------------------------------------------

	/**
	 * Handle accesslens_start_scan AJAX action.
	 */
	public function handle_start_scan(): void {
		$this->verify_request(); // verifica nonce (check_ajax_referer) + capability.

		// El nonce se verifica en verify_request(); PHPCS no rastrea helpers.
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// Sanitize inputs.
		$raw_url  = isset( $_POST['scan_url'] ) ? sanitize_text_field( wp_unslash( $_POST['scan_url'] ) ) : '';
		$raw_mode = isset( $_POST['scan_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['scan_mode'] ) ) : 'single';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$scan_url = esc_url_raw( $raw_url );
		if ( empty( $scan_url ) ) {
			wp_send_json_error( array( 'message' => __( 'URL no valida.', 'accesslens' ) ), 400 );
			return;
		}

		// Validacion basica del modo. El nivel free/Pro lo decide el servicio externo
		// (serviceware): el plugin no bloquea funciones; envia la licencia y el motor aplica el plan.
		$mode = in_array( $raw_mode, array( 'single', 'sitemap', 'crawl' ), true ) ? $raw_mode : 'single';

		$result = $this->client->start_scan( $scan_url, $mode, accesslens_license_key() );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
			return;
		}

		// Persist jobId for this user.
		set_transient( $this->job_transient_key(), sanitize_text_field( $result['jobId'] ), self::JOB_TTL );

		wp_send_json_success(
			array(
				'jobId'     => esc_html( $result['jobId'] ),
				'status'    => esc_html( $result['status'] ?? 'queued' ),
				'pageLimit' => absint( $result['pageLimit'] ?? 0 ),
			)
		);
	}

	/**
	 * Handle accesslens_poll_scan AJAX action.
	 */
	public function handle_poll_scan(): void {
		$this->verify_request();

		$job_id = get_transient( $this->job_transient_key() );
		if ( empty( $job_id ) ) {
			wp_send_json_error( array( 'message' => __( 'No hay un escaneo activo para este usuario.', 'accesslens' ) ), 404 );
			return;
		}

		$result = $this->client->get_status( $job_id );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
			return;
		}

		// Sanitize output before sending to browser.
		wp_send_json_success( $this->sanitize_status_response( $result ) );
	}

	/**
	 * Handle accesslens_get_report AJAX action.
	 * Proxies the PDF/HTML from the engine and forces browser download.
	 */
	public function handle_get_report(): void {
		$this->verify_request();

		$job_id = get_transient( $this->job_transient_key() );
		if ( empty( $job_id ) ) {
			wp_send_json_error( array( 'message' => __( 'No hay un escaneo completado para descargar.', 'accesslens' ) ), 404 );
			return;
		}

		// El nonce se verifica en verify_request(); PHPCS no rastrea helpers.
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// Collect and sanitize report metadata from POST.
		$meta = array(
			'siteName'    => sanitize_text_field( wp_unslash( $_POST['site_name'] ?? get_bloginfo( 'name' ) ) ),
			'siteUrl'     => esc_url_raw( wp_unslash( $_POST['site_url'] ?? home_url() ) ),
			'entityType'  => in_array( wp_unslash( $_POST['entity_type'] ?? '' ), array( 'private', 'public' ), true )
				? sanitize_text_field( wp_unslash( $_POST['entity_type'] ) )
				: 'private',
			'contactEmail' => sanitize_email( wp_unslash( $_POST['contact_email'] ?? '' ) ),
			'contactUrl'   => esc_url_raw( wp_unslash( $_POST['contact_url'] ?? '' ) ),
			'preparedBy'   => sanitize_text_field( wp_unslash( $_POST['prepared_by'] ?? '' ) ),
		);
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$format = 'pdf'; // Force PDF for now.

		// Envia la licencia; el motor decide si incluye las funciones Pro (IA) en el informe.
		$result = $this->client->get_report( $job_id, $meta, $format, accesslens_license_key() );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
			return;
		}

		// Stream binary to browser.
		$content_type = $result['content_type'] ?: 'application/pdf';
		$filename      = 'accesslens-report-' . sanitize_title( $meta['siteName'] ) . '.pdf';

		header( 'Content-Type: ' . sanitize_mime_type( $content_type ) );
		header( 'Content-Disposition: attachment; filename="' . rawurlencode( $filename ) . '"' );
		header( 'Content-Length: ' . strlen( $result['body'] ) );
		header( 'Cache-Control: no-store' );

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $result['body'];
		wp_die();
	}

	// ----------------------------------------------------------------
	// Sanitizers
	// ----------------------------------------------------------------

	/**
	 * Sanitize the status response array before sending to JS.
	 *
	 * @param array $data Raw data from engine.
	 * @return array Sanitized data.
	 */
	private function sanitize_status_response( array $data ): array {
		$allowed_statuses = array( 'queued', 'discovering', 'running', 'done', 'error' );
		$status           = sanitize_text_field( $data['status'] ?? 'queued' );
		if ( ! in_array( $status, $allowed_statuses, true ) ) {
			$status = 'queued';
		}

		$progress = array(
			'total' => absint( $data['progress']['total'] ?? 0 ),
			'done'  => absint( $data['progress']['done'] ?? 0 ),
		);

		$results = array();
		if ( is_array( $data['results'] ?? null ) ) {
			foreach ( $data['results'] as $page ) {
				if ( ! is_array( $page ) ) {
					continue;
				}
				$results[] = $this->sanitize_page_result( $page );
			}
		}

		$errors = array();
		if ( is_array( $data['errors'] ?? null ) ) {
			foreach ( $data['errors'] as $err ) {
				$errors[] = sanitize_text_field( (string) $err );
			}
		}

		return array(
			'status'   => $status,
			'progress' => $progress,
			'results'  => $results,
			'errors'   => $errors,
			'error'    => sanitize_text_field( $data['error'] ?? '' ),
		);
	}

	/**
	 * Sanitize a single page result object.
	 *
	 * @param array $page Raw page result.
	 * @return array Sanitized.
	 */
	private function sanitize_page_result( array $page ): array {
		$summary = array(
			'total'    => absint( $page['summary']['total'] ?? 0 ),
			'critical' => absint( $page['summary']['critical'] ?? 0 ),
			'serious'  => absint( $page['summary']['serious'] ?? 0 ),
			'moderate' => absint( $page['summary']['moderate'] ?? 0 ),
			'minor'    => absint( $page['summary']['minor'] ?? 0 ),
		);

		$violations = array();
		if ( is_array( $page['violations'] ?? null ) ) {
			foreach ( $page['violations'] as $v ) {
				if ( ! is_array( $v ) ) {
					continue;
				}
				$wcag = array();
				if ( is_array( $v['wcagCriteria'] ?? null ) ) {
					foreach ( $v['wcagCriteria'] as $w ) {
						$wcag[] = sanitize_text_field( (string) $w );
					}
				}
				$violations[] = array(
					'id'           => sanitize_text_field( $v['id'] ?? '' ),
					'impact'       => sanitize_text_field( $v['impact'] ?? '' ),
					'wcagCriteria' => $wcag,
					'help'         => sanitize_text_field( $v['help'] ?? '' ),
					'nodes'        => absint( count( $v['nodes'] ?? array() ) ),
				);
			}
		}

		return array(
			'url'        => esc_url_raw( $page['url'] ?? '' ),
			'pageTitle'  => sanitize_text_field( $page['pageTitle'] ?? '' ),
			'summary'    => $summary,
			'violations' => $violations,
		);
	}
}
