<?php
/**
 * HTTP client for the AccessLens Engine.
 *
 * Wraps all communication with the external engine API.
 * Never throws exceptions — returns WP_Error on failure.
 *
 * @package AccessLens
 */

declare(strict_types=1);

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

/**
 * AccessLens_Engine_Client
 */
class AccessLens_Engine_Client {

	/** Error slugs the engine is documented to return; anything else maps to 'engine_error'. */
	private const KNOWN_ENGINE_ERRORS = array( 'pro_required', 'quota_exceeded', 'rate_limited', 'domain_not_allowed', 'service_busy' );

	/** Timeout in seconds for scan start. */
	private const TIMEOUT_START = 20;

	/** Timeout in seconds for status poll. */
	private const TIMEOUT_POLL = 20;

	/** Timeout in seconds for report generation (PDF can be slow). */
	private const TIMEOUT_REPORT = 120;

	/**
	 * Get engine base URL, stripped of trailing slash.
	 *
	 * Fija por diseno via ACCESSLENS_ENGINE_URL (constante). Solo se puede sobreescribir
	 * con el filtro 'accesslens_engine_url' (uso previsto: desarrollo local).
	 */
	private function base_url(): string {
		$url = apply_filters( 'accesslens_engine_url', ACCESSLENS_ENGINE_URL );
		return rtrim( esc_url_raw( (string) $url ), '/' );
	}

	/**
	 * Build a WP_Error from a non-2xx engine response, preserving the engine's
	 * stable error slug (pro_required, quota_exceeded, rate_limited,
	 * domain_not_allowed, service_busy) so callers can map it to a user message.
	 *
	 * @param array|WP_Error $response         Raw response from wp_remote_*.
	 * @param string         $fallback_message Message used when the engine did not send a known slug.
	 * @return WP_Error
	 */
	private function build_error_from_response( $response, string $fallback_message ): WP_Error {
		$code       = (int) wp_remote_retrieve_response_code( $response );
		$data       = json_decode( wp_remote_retrieve_body( $response ), true );
		$error_slug = ( is_array( $data ) && ! empty( $data['error'] ) ) ? sanitize_key( (string) $data['error'] ) : '';

		if ( in_array( $error_slug, self::KNOWN_ENGINE_ERRORS, true ) ) {
			return new WP_Error(
				$error_slug,
				$fallback_message,
				array(
					'http_status' => $code,
					'resetAt'     => sanitize_text_field( (string) ( $data['resetAt'] ?? '' ) ),
					'retryAfter'  => absint( $data['retryAfter'] ?? 0 ),
				)
			);
		}

		return new WP_Error( 'engine_error', $fallback_message, array( 'http_status' => $code ) );
	}

	/**
	 * Start a new scan. El nivel free/Pro lo decide el motor a partir de la licencia.
	 *
	 * @param string $url         URL to scan.
	 * @param string $mode        "single", "sitemap" or "crawl".
	 * @param string $license_key Clave de licencia Freemius (o '' si free).
	 * @return array{jobId:string,status:string,pageLimit:int}|WP_Error
	 */
	public function start_scan( string $url, string $mode, string $license_key = '' ): array|WP_Error {
		$endpoint = $this->base_url() . '/scan';

		$body = wp_json_encode(
			array(
				'url'        => $url,
				'siteUrl'    => home_url( '/' ),
				'mode'       => $mode,
				'licenseKey' => $license_key,
			)
		);

		if ( false === $body ) {
			return new WP_Error( 'json_encode', __( 'Error encoding the JSON request.', 'accesslens' ) );
		}

		$response = wp_remote_post(
			$endpoint,
			array(
				'timeout'     => self::TIMEOUT_START,
				'headers'     => array( 'Content-Type' => 'application/json' ),
				'body'        => $body,
				'data_format' => 'body',
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 202 !== (int) $code ) {
			return $this->build_error_from_response(
				$response,
				sprintf(
					/* translators: %d: HTTP status code */
					__( 'The engine returned HTTP %d when starting the scan.', 'accesslens' ),
					$code
				)
			);
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) || empty( $data['jobId'] ) ) {
			return new WP_Error( 'invalid_response', __( 'Invalid response from the engine (start).', 'accesslens' ) );
		}

		return $data;
	}

	/**
	 * Poll the status of an existing scan job.
	 *
	 * @param string $job_id Job identifier.
	 * @return array|WP_Error Decoded JSON body or WP_Error.
	 */
	public function get_status( string $job_id ): array|WP_Error {
		$endpoint = $this->base_url() . '/scan/' . rawurlencode( $job_id );

		$response = wp_remote_get(
			$endpoint,
			array( 'timeout' => self::TIMEOUT_POLL )
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== (int) $code ) {
			return $this->build_error_from_response(
				$response,
				sprintf(
					/* translators: %d: HTTP status code */
					__( 'The engine returned HTTP %d when checking status.', 'accesslens' ),
					$code
				)
			);
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			return new WP_Error( 'invalid_response', __( 'Invalid response from the engine (poll).', 'accesslens' ) );
		}

		return $data;
	}

	/**
	 * Request the compliance report and return raw binary body + content-type.
	 *
	 * @param string $job_id     Job identifier.
	 * @param array  $report_meta Report metadata (siteName, siteUrl, entityType, etc.).
	 * @param string $format      "pdf" or "html".
	 * @param string $license_key Clave de licencia Freemius (o '' si free).
	 * @return array{body:string,content_type:string}|WP_Error
	 */
	public function get_report( string $job_id, array $report_meta, string $format = 'pdf', string $license_key = '' ): array|WP_Error {
		$endpoint = $this->base_url() . '/scan/' . rawurlencode( $job_id ) . '/report';

		// siteUrl siempre es home_url('/'): el motor lo usa para restringir el target al
		// propio dominio del sitio, no es un campo editable por el usuario.
		$payload = array_merge(
			$report_meta,
			array(
				'siteUrl'    => home_url( '/' ),
				'format'     => in_array( $format, array( 'pdf', 'html' ), true ) ? $format : 'pdf',
				'licenseKey' => $license_key,
			)
		);

		$body = wp_json_encode( $payload );
		if ( false === $body ) {
			return new WP_Error( 'json_encode', __( 'Error encoding the report metadata.', 'accesslens' ) );
		}

		$response = wp_remote_post(
			$endpoint,
			array(
				'timeout'     => self::TIMEOUT_REPORT,
				'headers'     => array( 'Content-Type' => 'application/json' ),
				'body'        => $body,
				'data_format' => 'body',
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== (int) $code ) {
			return $this->build_error_from_response(
				$response,
				sprintf(
					/* translators: %d: HTTP status code */
					__( 'The engine returned HTTP %d when generating the report.', 'accesslens' ),
					$code
				)
			);
		}

		return array(
			'body'         => wp_remote_retrieve_body( $response ),
			'content_type' => wp_remote_retrieve_header( $response, 'content-type' ),
		);
	}
}
