<?php
/**
 * File: Extension_ImageService_Api.php
 *
 * @since 2.2.0
 *
 * @package W3TC
 */

namespace W3TC;

/**
 * Class: Extension_ImageService_Api
 *
 * @since 2.2.0
 */
class Extension_ImageService_Api {
	/**
	 * W3TC Pro license key.
	 *
	 * @since 2.2.0
	 * @access private
	 *
	 * @var string
	 */
	private $license_key = '0';

	/**
	 * W3TC Pro licensed home URL.
	 *
	 * @since 2.2.0
	 * @access private
	 *
	 * @var string
	 */
	private $home_url;

	/**
	 * W3TC Pro licensed product item name.
	 *
	 * @since 2.2.0
	 * @access private
	 *
	 * @var string
	 */
	private $item_name = '0';

	/**
	 * API endpoints.
	 *
	 * @since 2.2.0
	 * @access private
	 *
	 * @var array
	 */
	private $endpoints = array(
		'convert'  => array(
			'method' => 'POST',
			'uri'    => '/image/convert',
		),
		'status'   => array(
			'method' => 'GET',
			'uri'    => '/job/status',
		),
		'download' => array(
			'method' => 'GET',
			'uri'    => '/image/download',
		),
		'usage'    => array(
			'method' => 'GET',
			'uri'    => '/image/usage',
		),
	);

	/**
	 * Constructor.
	 *
	 * @since 2.2.0
	 */
	public function __construct() {
		$config = Dispatcher::config();

		if ( Util_Environment::is_w3tc_pro( $config ) ) {
			$this->license_key = $config->get_string( 'plugin.license_key' );
			$this->home_url    = network_home_url();
			$this->item_name   = W3TC_PURCHASE_PRODUCT_NAME;
		} else {
			$this->home_url = md5( network_home_url() );
		}
	}

	/**
	 * Convert an image; submit a job request.
	 *
	 * @since 2.2.0
	 *
	 * @see self::set_transient_data()
	 *
	 * @param string $filepath Image file path.
	 * @param array  $options  Optional array of options.  Overrides settings.
	 * @return array
	 */
	public function convert( $filepath, array $options = array() ) {
		// Abort if rate-limit exceeded.
		$result = get_transient( 'w3tc_imageservice_limited' );

		if ( ! empty( $result ) ) {
			return $result;
		}

		// Prepare request.
		$config   = Dispatcher::config();
		$settings = $config->get_array( 'imageservice' );
		$options  = array_merge(
			array(
				'optimize' => 'lossy' === $settings['compression'] ? '1' : '0',
			),
			$options
		);
		$boundary = wp_generate_password( 24 );
		$body     = '';

		$post_fields = array(
			'license_key' => $this->license_key,
			'home_url'    => $this->home_url,
			'item_name'   => $this->item_name,
			'optimize'    => $options['optimize'],
		);

		if ( Util_Environment::is_pro_constant( $config ) ) {
			$post_fields['pro_c'] = 1;
		}

		// Add mimeTypeOut parameter(s) based on settings or options.
		$mime_types_out = array();

		// If formats are specified in options, use those instead of settings.
		if ( isset( $options['formats'] ) && is_array( $options['formats'] ) && ! empty( $options['formats'] ) ) {
			$mime_types_out = $options['formats'];
		} else {
			// Check webp setting.
			$webp_enabled = isset( $settings['webp'] ) && ! empty( $settings['webp'] );
			if ( $webp_enabled ) {
				$mime_types_out[] = 'image/webp';
			}

			// Check avif setting - handle both boolean and string values, default to true if not set.
			// Only allow AVIF for Pro license holders.
			$avif_enabled = ! isset( $settings['avif'] ) || ( true === $settings['avif'] || '1' === $settings['avif'] || 1 === $settings['avif'] );
			if ( $avif_enabled && Util_Environment::is_w3tc_pro( $config ) ) {
				$mime_types_out[] = 'image/avif';
			}

			// If no formats are selected, default to WebP for backward compatibility.
			if ( empty( $mime_types_out ) ) {
				$mime_types_out[] = 'image/webp';
			}
		}

		foreach ( $post_fields as $k => $v ) {
			$body .= '--' . $boundary . "\r\n";
			$body .= 'Content-Disposition: form-data; name="' . $k . '"' . "\r\n\r\n";
			$body .= $v . "\r\n";
		}

		/**
		 * Add mimeTypeOut fields.
		 *
		 * `BoldGrid/w3tc-api2` expects `mimeTypeOut` as an array.
		 * For multipart, that means sending `mimeTypeOut[]` fields.
		 */
		foreach ( $mime_types_out as $mime_type ) {
			$body .= '--' . $boundary . "\r\n";
			$body .= 'Content-Disposition: form-data; name="mimeTypeOut[]"' . "\r\n\r\n";
			$body .= $mime_type . "\r\n";
		}

		$body .= '--' . $boundary . "\r\n";
		$body .= 'Content-Disposition: form-data; name="image"; filename="' . basename( $filepath ) . '"' . "\r\n\r\n";
		$body .= file_get_contents( $filepath ) . "\r\n" . '--' . $boundary . '--'; // phpcs:ignore WordPress.WP.AlternativeFunctions

		// Send request.
		$response = wp_remote_request(
			Util_Environment::get_api_base_url() . $this->endpoints['convert']['uri'],
			array(
				'method'    => $this->endpoints['convert']['method'],
				'sslverify' => false,
				'timeout'   => 30,
				'headers'   => array(
					'Accept'       => 'application/json',
					'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
				),
				'body'      => $body,
			)
		);

		// Examine response.
		if ( is_wp_error( $response ) ) {
			return array(
				'error' => __( 'WP Error: ', 'w3-total-cache' ) . $response->get_error_message(),
			);
		}

		// Convert response body to an array.
		$response_body = json_decode( wp_remote_retrieve_body( $response ), true );

		// Update usage.
		if ( isset( $response_body['usage_hourly'] ) ) {
			// Ensure that the monthly limit is represented correctly.
			$response_body['limit_monthly'] = $response_body['limit_monthly'] ?? \__( 'Unlimited', 'w3-total-cache' );

			// Update usage transient data.
			$this->set_transient_data( $response_body );
		}

		// Handle non-200 response codes.
		if ( 200 !== $response['response']['code'] ) {
			$result = array(
				'code'   => $response['response']['code'],
				'error'  => esc_html__( 'Error: Received a non-200 response code: ', 'w3-total-cache' ) . $response['response']['code'],
				'status' => 'error',
				'time'   => time(),
			);

			if ( isset( $response_body['error']['id'] ) && 'exceeded-hourly' === $response_body['error']['id'] ) {
				$result['message'] = sprintf(
					// translators: 1: Hourly request limit.
					esc_html__( 'You reached your hourly limit of %1$d; try again later%2$s.', 'w3-total-cache' ),
					esc_attr( $response_body['limit_hourly'] ),
					isset( $response_body['licensed'] ) && $response_body['licensed'] ? '' :
						sprintf(
							// translators: 1: Hourly request limit, 2: HTML anchor open tag, 3: HTML anchor close tag.
							esc_html__( ' or %1$supgrade to Pro%2$s for higher limits', 'w3-total-cache' ),
							'<a href="#" class="button-buy-plugin" data-src="imageservice_api_limit">',
							'</a>'
						)
				);
				set_transient( 'w3tc_imageservice_limited', $result, 30 * MINUTE_IN_SECONDS );
			} elseif ( isset( $response_body['error']['id'] ) && 'exceeded-monthly' === $response_body['error']['id'] ) {
				$result['message'] = sprintf(
					// translators: 1: Monthly request limit, 2: HTML anchor open tag, 3: HTML anchor close tag.
					esc_html__( 'You reached your monthly limit of %1$d; try again later or %2$supgrade to Pro%3$s for unlimited.', 'w3-total-cache' ),
					esc_attr( $response_body['limit_monthly'] ),
					'<a href="#" class="button-buy-plugin" data-src="imageservice_api_limit">',
					'</a>'
				);
				set_transient( 'w3tc_imageservice_limited', $result, DAY_IN_SECONDS );
			} elseif ( isset( $response_body['error']['id'] ) && 'invalid-output-mime' === $response_body['error']['id'] ) {
				$result['message'] = esc_html__( 'Invalid output image MIME type.', 'w3-total-cache' );
			} elseif ( isset( $response_body['error']['id'] ) && 'missing-image' === $response_body['error']['id'] ) {
				$result['message'] = esc_html__( 'An image file is required.', 'w3-total-cache' );
			} elseif ( isset( $response_body['error']['id'] ) && 'invalid-image' === $response_body['error']['id'] ) {
				$result['message'] = esc_html__( 'Valid image data is required.', 'w3-total-cache' );
			} elseif ( isset( $response_body['error']['id'] ) && 'invalid-input-mime' === $response_body['error']['id'] ) {
				$result['message'] = esc_html__( 'Invalid input image MIME type.', 'w3-total-cache' );
			} elseif ( 403 === $response['response']['code'] ) {
				$result['message'] = sprintf(
					// translators: 1: HTML anchor open tag, 2: HTML anchor close tag.
					esc_html__( 'Please verify your license key in %1$sGeneral Settings%2$s.', 'w3-total-cache' ),
					'<a href="' . esc_url( Util_Ui::admin_url( 'admin.php?page=w3tc_general#licensing' ) ) . '">',
					'</a>'
				);
			} elseif ( isset( $response_body['error']['message'] ) ) {
				// Unknown error message id; forward the error message.
				$result['message'] = esc_html( $response_body['error']['message'] );
			}

			return $result;
		}

		return $response_body;
	}

	/**
	 * Get job status.
	 *
	 * @since 2.2.0
	 *
	 * @param int    $job_id    Job id.
	 * @param string $signature Signature.
	 * @return array
	 */
	public function get_status( $job_id, $signature ) {
		$response = wp_remote_request(
			Util_Environment::get_api_base_url() . $this->endpoints['status']['uri'] . '/' . $job_id . '/' . $signature,
			array(
				'method'    => $this->endpoints['status']['method'],
				'sslverify' => false,
				'timeout'   => 10,
				'headers'   => array(
					'Accept' => 'application/json',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			return array(
				'error' => __( 'WP Error: ', 'w3-total-cache' ) . $response->get_error_message(),
			);
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$response_body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( 404 === $response_code || ( isset( $response_body['error']['code'] ) && 404 === (int) $response_body['error']['code'] ) ) {
			return array(
				'code'   => 404,
				'status' => 'notfound',
				'error'  => isset( $response_body['error'] ) ? $response_body['error'] : __( 'Error: Job not found (404).', 'w3-total-cache' ),
			);
		}

		if ( 200 !== $response_code ) {
			return array(
				'code'   => $response_code,
				'status' => 'error',
				'error'  => sprintf(
					// translators: 1: HTTP status code.
					__( 'Error: Received a non-200 response code: %1$d', 'w3-total-cache' ),
					$response_code
				),
			);
		}

		// Pass error message.
		if ( isset( $response_body['error'] ) ) {
			return array(
				'error' => $response_body['error'],
			);
		}

		return $response_body;
	}

	/**
	 * Download a processed image.
	 *
	 * @since 2.2.0
	 *
	 * @param int    $job_id      Job id.
	 * @param string $signature   Signature.
	 * @param string $mime_type_out Optional. MIME type of the output format (e.g., 'image/webp', 'image/avif').
	 * @return array WP response array.
	 */
	public function download( $job_id, $signature, $mime_type_out = null ) {
		$url = Util_Environment::get_api_base_url() . $this->endpoints['download']['uri'] . '/' . $job_id . '/' . $signature;

		// Add mimeTypeOut as query parameter if provided.
		if ( ! empty( $mime_type_out ) ) {
			$url .= '?mimeTypeOut=' . rawurlencode( $mime_type_out );
		}

		$response = wp_remote_request(
			$url,
			array(
				'method'    => $this->endpoints['download']['method'],
				'timeout'   => 10,
			)
		);

		if ( is_wp_error( $response ) ) {
			return array(
				'error' => __( 'WP Error: ', 'w3-total-cache' ) . $response->get_error_message(),
			);
		}

		// Check for HTTP error status codes.
		$response_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $response_code ) {
			$body = wp_remote_retrieve_body( $response );
			$json = json_decode( $body, true );

			$error_message = isset( $json['error'] ) ? $json['error'] :
				sprintf(
					// translators: 1: HTTP status code.
					__( 'HTTP Error: Received status code %1$d', 'w3-total-cache' ),
					$response_code
				);

			return array(
				'error' => $error_message,
			);
		}

		// Get the response body.
		$body = wp_remote_retrieve_body( $response );

		// Convert response body to an array.  A successful image results in a JSON decode false return.
		$json = json_decode( $body, true );

		// Pass error message if JSON decode succeeded and contains an error.
		if ( is_array( $json ) && isset( $json['error'] ) ) {
			return array(
				'error' => $json['error'],
			);
		}

		return $response;
	}

	/**
	 * Get usage statistics.
	 *
	 * @since 2.2.0
	 *
	 * @see self::set_transient_data()
	 *
	 * @param bool $refresh Refresh the transient data.
	 * @return array
	 */
	public function get_usage( bool $refresh = false ): array {
		$transient_data = get_transient( 'w3tc_imageservice_usage' );

		if ( ! empty( $transient_data ) && isset( $transient_data['usage_hourly'] ) && ! $refresh ) {
			return $transient_data;
		}

		$error_message  = __( 'Unknown', 'w3-total-cache' );
		$error_response = array(
			'usage_hourly'             => $error_message,
			'usage_monthly'            => $error_message,
			'limit_hourly'             => $error_message,
			'limit_monthly'            => $error_message,
			'limit_hourly_unlicensed'  => $error_message,
			'limit_monthly_unlicensed' => $error_message,
			'limit_hourly_licensed'    => $error_message,
			'limit_monthly_licensed'   => $error_message,
		);

		$response = wp_remote_request(
			esc_url(
				Util_Environment::get_api_base_url() . $this->endpoints['usage']['uri'] .
					'/' . rawurlencode( $this->license_key ) .
					'/' . urlencode( $this->item_name ) . // phpcs:ignore
					'/' . rawurlencode( $this->home_url )
			),
			array(
				'method'    => $this->endpoints['usage']['method'],
				'sslverify' => false,
				'timeout'   => 10,
				'headers'   => array(
					'Accept' => 'application/json',
				),
			)
		);

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

		// Convert response body to an array.
		$response_body = json_decode( wp_remote_retrieve_body( $response ), true );

		// If usage is not obtained, then return error response.
		if ( ! isset( $response_body['usage_hourly'] ) ) {
			return $error_response;
		} else {
			// Ensure that the monthly limit is represented correctly.
			$response_body['limit_monthly'] = $response_body['limit_monthly'] ?? \__( 'Unlimited', 'w3-total-cache' );

			// Update usage transient data.
			$this->set_transient_data( $response_body );

			return $response_body;
		}
	}

	/**
	 * Update usage transient data.
	 *
	 * @since 2.7.4
	 *
	 * @param array $data Usage data.
	 */
	private function set_transient_data( array $data ): void {
			// Ensure that the monthly limit is represented correctly.
			$data['limit_monthly'] = $data['limit_monthly'] ?? \__( 'Unlimited', 'w3-total-cache' );

			// Update usage transient data.
			set_transient(
				'w3tc_imageservice_usage',
				array(
					'updated_at'               => time(),
					'usage_hourly'             => $data['usage_hourly'] ?? null,
					'usage_monthly'            => $data['usage_monthly'] ?? null,
					'limit_hourly'             => $data['limit_hourly'] ?? null,
					'limit_monthly'            => $data['limit_monthly'], // Validated above.
					'limit_hourly_unlicensed'  => $data['limit_hourly_unlicensed'] ?? null,
					'limit_monthly_unlicensed' => $data['limit_monthly_unlicensed'] ?? null,
					'limit_hourly_licensed'    => $data['limit_hourly_licensed'] ?? null,
					'limit_monthly_licensed'   => $data['limit_monthly_licensed'] ?? null,
				),
				DAY_IN_SECONDS
			);
	}
}
