<?php
/**
 * Class Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalyticsBatch
 *
 * @package   Google\Site_Kit\Modules\Search_Console\Datapoints
 * @copyright 2025 Google LLC
 * @license   https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 * @link      https://sitekit.withgoogle.com
 */

namespace Google\Site_Kit\Modules\Search_Console\Datapoints;

use Exception;
use Google\Site_Kit\Core\Modules\Datapoint;
use Google\Site_Kit\Core\Modules\Executable_Datapoint;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception;
use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole as Google_Service_SearchConsole;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryResponse;
use WP_Error;

/**
 * Datapoint class for Search Console search analytics batch requests.
 *
 * @since 1.170.0
 * @access private
 * @ignore
 */
class SearchAnalyticsBatch extends Datapoint implements Executable_Datapoint {

	const REQUEST_METHODS = array( 'POST' );
	const REST_METHODS    = array( 'POST' );
	const DATAPOINT       = 'searchanalytics-batch';

	/**
	 * Callback to obtain the Search Console service.
	 *
	 * @since 1.170.0
	 * @var callable|\Closure
	 */
	private $get_service;

	/**
	 * Callback to prepare single search analytics request arguments.
	 *
	 * @since 1.170.0
	 * @var callable|\Closure
	 */
	private $prepare_args;

	/**
	 * Callback to build a search analytics request.
	 *
	 * @since 1.170.0
	 * @var callable|\Closure
	 */
	private $create_request;

	/**
	 * Identifiers for the requested payloads.
	 *
	 * @since 1.170.0
	 * @var array
	 */
	private $request_identifiers = array();

	/**
	 * Captured errors for individual requests.
	 *
	 * @since 1.170.0
	 * @var array
	 */
	private $request_errors = array();

	/**
	 * Constructor.
	 *
	 * @since 1.170.0
	 *
	 * @param array $definition Datapoint definition.
	 */
	public function __construct( array $definition ) {
		parent::__construct( $definition );

		$this->get_service    = isset( $definition['get_service'] ) ? $definition['get_service'] : null;
		$this->prepare_args   = isset( $definition['prepare_args'] ) ? $definition['prepare_args'] : null;
		$this->create_request = isset( $definition['create_request'] ) ? $definition['create_request'] : null;
	}

	/**
	 * Creates a request object.
	 *
	 * @since 1.170.0
	 *
	 * @param Data_Request $data_request Data request object.
	 * @return callable|WP_Error Callable to execute the batch request, or WP_Error.
	 * @throws Missing_Required_Param_Exception Thrown when required parameters are missing.
	 */
	public function create_request( Data_Request $data_request ) {
		$requests = isset( $data_request->data['requests'] ) ? $data_request->data['requests'] : null;

		if ( empty( $requests ) || ! is_array( $requests ) ) {
			throw new Missing_Required_Param_Exception( 'requests' );
		}

		$this->request_identifiers = array();
		$this->request_errors      = array();

		$service            = $this->get_searchconsole_service();
		$batch              = $service->createBatch();
		$has_valid_requests = false;

		foreach ( $requests as $request_data ) {
			$identifier                  = $this->normalize_identifier( $request_data );
			$this->request_identifiers[] = $identifier;

			try {
				$args = $this->prepare_request_args( $request_data );
				if ( is_wp_error( $args ) ) {
					$this->request_errors[ $identifier ] = $args;
					continue;
				}

				$single_request = $this->build_single_request( $args );

				if ( is_wp_error( $single_request ) ) {
					$this->request_errors[ $identifier ] = $single_request;
					continue;
				}

				$batch->add( $single_request, $identifier );
				$has_valid_requests = true;
			} catch ( Exception $exception ) {
				$this->request_errors[ $identifier ] = $this->exception_to_error( $exception );
			}
		}

		if ( empty( $this->request_identifiers ) ) {
			return new WP_Error(
				'missing_required_param',
				/* translators: %s: Missing parameter name */
				sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'requests' ),
				array( 'status' => 400 )
			);
		}

		if ( ! $has_valid_requests ) {
			return function () {
				return array();
			};
		}

		return function () use ( $batch ) {
			return $batch->execute();
		};
	}

	/**
	 * Parses a response.
	 *
	 * @since 1.170.0
	 *
	 * @param mixed        $response      Request response.
	 * @param Data_Request $data_request  Data request object.
	 * @return array|WP_Error Associative array of responses keyed by identifier, or WP_Error on batch failure.
	 */
	public function parse_response( $response, Data_Request $data_request ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$results = $this->request_errors;

		if ( is_array( $response ) ) {
			foreach ( $response as $identifier => $single_response ) {
				$normalized_identifier             = $this->normalize_response_identifier( $identifier );
				$results[ $normalized_identifier ] = $this->parse_single_response( $single_response );
			}
		}

		// Preserve the original request ordering and ensure all identifiers are represented.
		$ordered_results = array();
		foreach ( $this->request_identifiers as $identifier ) {
			if ( array_key_exists( $identifier, $results ) ) {
				$ordered_results[ $identifier ] = $results[ $identifier ];
			} else {
				$ordered_results[ $identifier ] = new WP_Error(
					'searchanalytics_batch_missing_response',
					__( 'Missing response from Search Console.', 'google-site-kit' )
				);
			}
		}

		// Append any unexpected identifiers returned by the API.
		foreach ( $results as $identifier => $single_result ) {
			if ( array_key_exists( $identifier, $ordered_results ) ) {
				continue;
			}

			$ordered_results[ $identifier ] = $single_result;
		}

		return $ordered_results;
	}

	/**
	 * Parses a single batch response.
	 *
	 * @since 1.170.0
	 *
	 * @param mixed $response Single response.
	 * @return array|WP_Error Parsed rows or WP_Error.
	 */
	private function parse_single_response( $response ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( $response instanceof Google_Service_Exception ) {
			return $this->exception_to_error( $response );
		}

		if ( $response instanceof SearchAnalyticsQueryResponse ) {
			return $response->getRows();
		}

		if ( is_object( $response ) && method_exists( $response, 'getRows' ) ) {
			return $response->getRows();
		}

		return $response;
	}

	/**
	 * Builds a single request.
	 *
	 * @since 1.170.0
	 *
	 * @param array $args Prepared request arguments.
	 * @return mixed Request instance or WP_Error.
	 */
	private function build_single_request( array $args ) {
		if ( ! is_callable( $this->create_request ) ) {
			return new WP_Error(
				'invalid_request_callback',
				__( 'Invalid Search Console request callback.', 'google-site-kit' )
			);
		}

		return call_user_func( $this->create_request, $args );
	}

	/**
	 * Prepares request arguments for a single search analytics request.
	 *
	 * @since 1.170.0
	 *
	 * @param array $request_data Raw request data.
	 * @return array|WP_Error Prepared arguments or WP_Error.
	 */
	private function prepare_request_args( array $request_data ) {
		if ( ! is_callable( $this->prepare_args ) ) {
			return new WP_Error(
				'invalid_request_args_callback',
				__( 'Invalid Search Console request arguments.', 'google-site-kit' )
			);
		}

		return call_user_func( $this->prepare_args, $request_data );
	}

	/**
	 * Gets the Search Console service instance.
	 *
	 * @since 1.170.0
	 *
	 * @return Google_Service_SearchConsole Search Console service instance.
	 * @throws Missing_Required_Param_Exception When the service callback is missing.
	 */
	private function get_searchconsole_service() {
		if ( is_callable( $this->get_service ) ) {
			return call_user_func( $this->get_service );
		}

		throw new Missing_Required_Param_Exception( 'service' );
	}

	/**
	 * Normalizes a request identifier to a string.
	 *
	 * @since 1.170.0
	 *
	 * @param array $request_data Request data.
	 * @return string Normalized identifier.
	 * @throws Missing_Required_Param_Exception When the identifier is missing or invalid.
	 */
	private function normalize_identifier( array $request_data ) {
		if ( isset( $request_data['identifier'] ) ) {
			$identifier = $request_data['identifier'];
		} elseif ( isset( $request_data['id'] ) ) {
			$identifier = $request_data['id'];
		} else {
			throw new Missing_Required_Param_Exception( 'identifier' );
		}

		if ( ! is_scalar( $identifier ) ) {
			throw new Missing_Required_Param_Exception( 'identifier' );
		}

		$identifier = (string) $identifier;

		if ( '' === $identifier ) {
			throw new Missing_Required_Param_Exception( 'identifier' );
		}

		return $identifier;
	}

	/**
	 * Normalizes a response identifier to align with requested keys.
	 *
	 * @since 1.170.0
	 *
	 * @param string|int $identifier Raw response identifier.
	 * @return string|int Normalized identifier.
	 */
	private function normalize_response_identifier( $identifier ) {
		if ( is_string( $identifier ) && 0 === strpos( $identifier, 'response-' ) ) {
			$identifier = substr( $identifier, strlen( 'response-' ) );
		}

		return $identifier;
	}

	/**
	 * Converts an exception to a WP_Error instance.
	 *
	 * @since 1.170.0
	 *
	 * @param Exception $exception Exception instance.
	 * @return WP_Error WP_Error instance.
	 */
	private function exception_to_error( Exception $exception ) {
		$status = (int) ( $exception->getCode() ?: 500 );

		return new WP_Error(
			'searchanalytics_batch_request_failed',
			$exception->getMessage(),
			array( 'status' => $status )
		);
	}
}
