<?php
/**
 * Utility class for web service calls.
 *
 * @package Ealab\Deployer
 */

namespace EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility;

use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\GuzzleHttp\Client;
use EaLab\WP_Deployer\vendor\GuzzleHttp\Exception\GuzzleException;
use EaLab\WP_Deployer\vendor\GuzzleHttp\RequestOptions;
use EaLab\WP_Deployer\vendor\Psr\Http\Message\ResponseInterface;

/**
 * Utility class for web service calls.
 */
class Api {
	/**
	 * Call an external web service using cURL or Guzzle.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array      $header_data Contains header data to pass to the API.
	 * @param string     $licence_key The licence key to pass to the API. By default empty string.
	 * @param mixed      $data Data to pass to the API. By default an empty array.
	 * @param bool       $is_file Is the data a file. By default false.
	 * @param array|null $options Options:
	 *     - "use_guzzle", bool: Use Guzzle instead of curl. The caller should adapt to use a Response return value.
	 *
	 * @return bool|string|ResponseInterface The cURL or Guzzle result.
	 *
	 * @throws WpDeployException Could not call the web service.
	 *
	 * @deprecated Use V2 instead.
	 */
	public static function callApi( $method, $url, $header_data = array(), $licence_key = '', $data = array(), bool $is_file = false, ?array $options = null ) {
		// Dev note: we send the same args to the handlers with ...func_get_args().
		// This is sufficient because we don't have arguments passed by reference,
		// and the argument list is strictly the same between the handlers and this method.
		if ( ! empty( $options['use_guzzle'] ) ) {
			return static::callApiWithGuzzle( ...func_get_args() );
		}

		return static::callApiWithCurl( ...func_get_args() );
	}

	/**
	 * Call an external web service using cURL or Guzzle.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array|null $options Options:
	 *     - "data', string: Data to pass to the API for methods supporting a request body.
	 *     - "header_data', array: Contains header data to pass to the API.
	 *     - "is_file', bool: Is the data a file. By default false.
	 *     - "licence_key', string: The licence key to pass to the API. By default empty string.
	 *     - "query", array<string,string>: Query params to add to the URL.
	 *       They can also be directly added to the $url argument.
	 *     - "use_guzzle", bool: Set to "true" to use Guzzle.
	 *
	 * @return bool|string|ResponseInterface The cURL or Guzzle result.
	 *
	 * @throws WpDeployException Could not call the web service.
	 */
	public static function callApiV2( string $method, string $url, ?array $options = null ) {
		// Dev note: we send the same args to the handlers with ...func_get_args().
		// This is sufficient because we don't have arguments passed by reference,
		// and the argument list is strictly the same between the handlers and this method.
		if ( ! empty( $options['use_guzzle'] ) ) {
			return static::callApiWithGuzzleV2( ...func_get_args() );
		}

		return static::callApiWithCurlV2( ...func_get_args() );
	}

	/**
	 * Performs a call to the production environment as declared in the settings.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $route_api The route of the API. It may be preceded by a slash.
	 * @param mixed      $content The content to pass to the API. It will be JSON encoded.
	 * @param bool       $is_file Is the content a file. By default false.
	 * @param array|null $options Options:
	 *     - "use_guzzle", bool: Use Guzzle instead of curl.
	 *
	 * @return bool|string|ResponseInterface The result. If used Guzzle, a Response object will be returned.
	 *
	 * @throws WpDeployException Error while calling API.
	 *
	 * @deprecated Use V2 instead.
	 */
	public static function callApiProd( string $method, string $route_api, $content, bool $is_file = false, ?array $options = null ) {
		$settings = Settings::getInstance();
		$url_prod = $settings->get( 'url_prod' );

		if ( empty( $url_prod ) ) {
			throw new WpDeployException( _dgettext( 'deployer-core', 'The production URL is not set.' ) );
		}

		$api_url = rtrim( $url_prod, '/' ) . '/' . ltrim( $route_api, '/' );

		$header_data = array();

		// The access token is a simple token which must match between the instances.
		$access_token = $settings->get( 'access_token' );

		if ( ! empty( $access_token ) ) {
			$header_data[] = 'X-Deployer-Access-Token: ' . $access_token;
		}

		if ( $settings->get( 'basic_auth_enable' ) ) {
			// This is for instances protected by basic auth (another site code or at server level).
			$username = $settings->get( 'basic_auth_username' );
			$password = $settings->get( 'basic_auth_password' );

			// Build the Basic auth header.
			$header_data[] = 'Authorization: Basic ' . base64_encode( $username . ':' . $password );
		}

		return static::callApi( $method, $api_url, $header_data, '', json_encode( $content ), $is_file, $options );
	}

	/**
	 * Performs a call to the production environment as declared in the settings.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $route_api The route of the API. It may be preceded by a slash.
	 * @param array|null $options Options:
	 *     - "data', string: Data to pass to the API for methods supporting a request body.
	 *     - "header_data', array: Contains header data to pass to the API.
	 *     - "is_file', bool: Is the data a file. By default false.
	 *     - "licence_key', string: The licence key to pass to the API. By default empty string.
	 *     - "query", array<string,string>: Query params to add to the URL.
	 *       They can also be directly added to the $url argument.
	 *     - "prod_url_override", string: Overrides the production URL from settings.
	 *     - "use_guzzle", bool: Set to "true" to use Guzzle.
	 *
	 * @return bool|string|ResponseInterface The result. If used Guzzle, a Response object will be returned.
	 *
	 * @throws WpDeployException Error while calling API.
	 */
	public static function callApiProdV2( string $method, string $route_api, ?array $options = null ) {
		$settings = Settings::getInstance();

		if ( ! empty( $options['prod_url_override'] ) ) {
			$url_prod = $options['prod_url_override'];
		} else {
			$url_prod = $settings->get( 'url_prod' );
		}

		if ( empty( $url_prod ) ) {
			throw new WpDeployException( _dgettext( 'deployer-core', 'The production URL is not set.' ) );
		}

		$api_url = rtrim( $url_prod, '/' ) . '/' . ltrim( $route_api, '/' );

		// The access token is a simple token which must match between the instances.
		$access_token = $settings->get( 'access_token' );

		if ( ! empty( $access_token ) ) {
			$options['header_data'][] = 'X-Deployer-Access-Token: ' . $access_token;
		}

		if ( $settings->get( 'basic_auth_enable' ) ) {
			// This is for instances protected by basic auth (another site code or at server level).
			$username = $settings->get( 'basic_auth_username' );
			$password = $settings->get( 'basic_auth_password' );

			// Build the Basic auth header.
			$options['header_data'][] = 'Authorization: Basic ' . base64_encode( $username . ':' . $password );
		}

		return static::callApiV2( $method, $api_url, $options );
	}

	/**
	 * Call an external web service using cURL.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array      $header_data Contains header data to pass to the API.
	 * @param string     $licence_key The licence key to pass to the API. By default empty string.
	 * @param mixed      $data Data to pass to the API. By default an empty array.
	 * @param bool       $is_file Is the data a file. By default false.
	 * @param array|null $options Options.
	 *
	 * @return bool|string The cURL result.
	 *
	 * @throws WpDeployException Could not call API.
	 *
	 * @deprecated Use V2 instead.
	 */
	private static function callApiWithCurl( $method, $url, $header_data = array(), $licence_key = '', $data = array(), bool $is_file = false, ?array $options = null ) {
		$curl          = curl_init();
		$headr         = array();
		$content_type  = 'application/json';
		$random_number = rand( 0, 999999 ) / 1000000; // Generate a random number between 0 and 1.
		$random_string = substr( strval( $random_number ), 2 ); // Convert to string and extract substring.

		if ( $is_file ) {
			$content_type = 'multipart/form-data; boundary=' . $random_string;
		}

		$headr[] = 'Content-type: ' . $content_type;

		foreach ( $header_data as $header_datum ) {
			$headr[] = $header_datum;
		}

		if ( ! empty( $licence_key ) ) {
			$headr[] = 'X-EaLab-Api-Key: ' . $licence_key;
		}

		switch ( $method ) {
			case 'POST':
				curl_setopt( $curl, CURLOPT_POST, 1 );
				if ( ! empty( $data ) ) {
					curl_setopt( $curl, CURLOPT_POSTFIELDS, $data );
				}
				break;

			case 'PUT':
				curl_setopt( $curl, CURLOPT_PUT, 1 );
				break;

			default:
				if ( ! empty( $data ) ) {
					$url = sprintf( '%s?%s', $url, http_build_query( $data ) );
				}
		}

		// Authentication.
		curl_setopt( $curl, CURLOPT_HTTPHEADER, $headr );
		curl_setopt( $curl, CURLOPT_URL, $url );
		curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
		curl_setopt( $curl, CURLOPT_FAILONERROR, true );
		$result = curl_exec( $curl );

		if ( curl_errno( $curl ) ) {
			$error_msg  = curl_error( $curl );
			$error_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
		}

		curl_close( $curl );

		if ( ! empty( $error_msg ) ) {
			throw new WpDeployException( $error_msg, $error_code );
		}

		return $result;
	}

	/**
	 * Call an external web service using cURL.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array|null $options Options:
	 *     - "data', string: Data to pass to the API for methods supporting a request body.
	 *     - "header_data', array: Contains header data to pass to the API.
	 *     - "is_file', bool: Is the data a file. By default false.
	 *     - "licence_key', string: The licence key to pass to the API. By default empty string.
	 *     - "query", array<string,string>: Query params to add to the URL.
	 *       They can also be directly added to the $url argument.
	 *
	 * @return bool|string The cURL result.
	 *
	 * @throws WpDeployException Could not call API.
	 */
	private static function callApiWithCurlV2( string $method, string $url, ?array $options = null ) {
		$curl          = curl_init();
		$headr         = array();
		$content_type  = 'application/json';
		$random_number = rand( 0, 999999 ) / 1000000; // Generate a random number between 0 and 1.
		$random_string = substr( strval( $random_number ), 2 ); // Convert to string and extract substring.

		if ( ! empty( $options['is_file'] ) ) {
			$content_type = 'multipart/form-data; boundary=' . $random_string;
		}

		$headr[] = 'Content-type: ' . $content_type;

		foreach ( $options['header_data'] ?? array() as $header_datum ) {
			$headr[] = $header_datum;
		}

		if ( ! empty( $options['licence_key'] ) ) {
			$headr[] = 'X-EaLab-Api-Key: ' . $options['licence_key'];
		}

		if ( ! empty( $options['query'] ) ) {
			$url = sprintf( '%s?%s', $url, http_build_query( $options['query'] ) );
		}

		switch ( $method ) {
			case 'POST':
				curl_setopt( $curl, CURLOPT_POST, 1 );

				if ( ! empty( $options['data'] ) ) {
					curl_setopt( $curl, CURLOPT_POSTFIELDS, $options['data'] );
				}

				break;

			case 'PUT':
				curl_setopt( $curl, CURLOPT_PUT, 1 );

				if ( ! empty( $options['data'] ) ) {
					curl_setopt( $curl, CURLOPT_POSTFIELDS, $options['data'] );
				}

				break;
		}

		// Authentication.
		curl_setopt( $curl, CURLOPT_HTTPHEADER, $headr );
		curl_setopt( $curl, CURLOPT_URL, $url );
		curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
		curl_setopt( $curl, CURLOPT_FAILONERROR, true );
		$result = curl_exec( $curl );

		if ( curl_errno( $curl ) ) {
			$error_msg  = curl_error( $curl );
			$error_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
		}

		curl_close( $curl );

		if ( ! empty( $error_msg ) ) {
			throw new WpDeployException( $error_msg, $error_code );
		}

		return $result;
	}

	/**
	 * Call a external web service using Guzzle.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array      $header_data Contains header data to pass to the API.
	 * @param string     $licence_key The licence key to pass to the API. By default empty string.
	 * @param mixed      $data Data to pass to the API. By default an empty array.
	 * @param bool       $is_file Is the data a file. By default false.
	 * @param array|null $options Options.
	 *
	 * @return ResponseInterface The Guzzle result.
	 *
	 * @throws WpDeployException Could not call API.
	 *
	 * @deprecated Use V2 instead.
	 */
	private static function callApiWithGuzzle( $method, $url, $header_data = array(), $licence_key = '', $data = array(), bool $is_file = false, ?array $options = null ): ResponseInterface {
		$guzzle_client = new Client();

		$headr = array();

		foreach ( $header_data as $header_datum ) {
			$split = explode( ':', $header_datum, 2 );

			if ( 2 !== count( $split ) ) {
				throw new WpDeployException( _dgettext( 'deployer-core', 'Invalid header.' ) );
			}

			$headr[ $split[0] ] = $split[1];
		}

		if ( ! empty( $licence_key ) ) {
			$headr['X-EaLab-Api-Key'] = $licence_key;
		}

		$request_options = array(
			RequestOptions::HEADERS => $headr,
		);

		if ( $is_file ) {
			$request_options[ RequestOptions::MULTIPART ] = array(
				array(
					'name'     => 'file',
					'contents' => $data,
				),
			);
		} else {
			// Everything we send is by default a JSON.
			// $data will be encoded by Guzzle.
			$request_options[ RequestOptions::JSON ] = $data;
		}

		try {
			return $guzzle_client->request(
				$method,
				$url,
				$request_options,
			);
		} catch ( GuzzleException $e ) {
			throw new WpDeployException( $e->getMessage(), 0, $e );
		}
	}

	/**
	 * Call an external web service using Guzzle.
	 *
	 * @param string     $method [POST, PUT, GET] The method.
	 * @param string     $url The URL of the RestApi Route.
	 * @param array|null $options Options:
	 *     - "data', string: Data to pass to the API for methods supporting a request body.
	 *     - "header_data', array: Contains header data to pass to the API.
	 *     - "is_file', bool: Is the data a file. By default false.
	 *     - "licence_key', string: The licence key to pass to the API. By default empty string.
	 *     - "query", array<string,string>: Query params to add to the URL.
	 *       They can also be directly added to the $url argument.
	 *
	 * @return ResponseInterface The Guzzle result.
	 *
	 * @throws WpDeployException Could not call API.
	 */
	private static function callApiWithGuzzleV2( $method, $url, ?array $options = null ): ResponseInterface {
		$guzzle_client = new Client();

		$headr = array();

		foreach ( $options['header_data'] as $header_datum ) {
			$split = explode( ':', $header_datum, 2 );

			if ( 2 !== count( $split ) ) {
				throw new WpDeployException( _dgettext( 'deployer-core', 'Invalid header.' ) );
			}

			$headr[ $split[0] ] = $split[1];
		}

		if ( ! empty( $options['licence_key'] ) ) {
			$headr['X-EaLab-Api-Key'] = $options['licence_key'];
		}

		$request_options = array(
			RequestOptions::HEADERS => $headr,
		);

		$request_options[ RequestOptions::QUERY ] = $options['query'] ?? array();

		if ( ! empty( $options['is_file'] ) ) {
			$request_options[ RequestOptions::MULTIPART ] = array(
				array(
					'name'     => 'file',
					'contents' => $options['data'] ?? '',
				),
			);
		} else {
			$request_options[ RequestOptions::BODY ] = $options['data'] ?? '';
		}

		try {
			return $guzzle_client->request(
				$method,
				$url,
				$request_options,
			);
		} catch ( GuzzleException $e ) {
			throw new WpDeployException( $e->getMessage(), 0, $e );
		}
	}
}
