<?php

/**
 * Service class for communicating with the eTranslation service API.
 */
class eTranslation_Service {
	/**
	 * The eTranslation service username.
	 *
	 * @var string
	 */
	private $username;

	/**
	 * The eTranslation service password.
	 *
	 * @var string
	 */
	private $password;

	/**
	 * Whether to use CURL client instead of WordPress HTTP API.
	 *
	 * @var bool
	 */
	private $use_curl_client;

	/**
	 * The base URL for the eTranslation service API.
	 *
	 * @var string
	 */
	static $api_url = 'https://webgate.ec.europa.eu/etranslation/si/';

	/**
	 * A map of error codes to their corresponding error messages.
	 *
	 * @var array
	 */
	static $error_map = array(
		-20000 => 'Source language not specified',
		-20001 => 'Invalid source language',
		-20002 => 'Target language(s) not specified',
		-20003 => 'Invalid target language(s)',
		-20004 => 'DEPRECATED',
		-20005 => 'Caller information not specified',
		-20006 => 'Missing application name',
		-20007 => 'Application not authorized to access the service',
		-20008 => 'Bad format for ftp address',
		-20009 => 'Bad format for sftp address',
		-20010 => 'Bad format for http address',
		-20011 => 'Bad format for email address',
		-20012 => 'Translation request must be text type, document path type or document base64 type and not several at a time',
		-20013 => 'Language pair not supported by the domain',
		-20014 => 'Username parameter not specified',
		-20015 => 'Extension invalid compared to the MIME type',
		-20016 => 'DEPRECATED',
		-20017 => 'Username parameter too long',
		-20018 => 'Invalid output format',
		-20019 => 'Institution parameter too long',
		-20020 => 'Department number too long',
		-20021 => 'Text to translate too long',
		-20022 => 'Too many FTP destinations',
		-20023 => 'Too many SFTP destinations',
		-20024 => 'Too many HTTP destinations',
		-20025 => 'Missing destination',
		-20026 => 'Bad requester callback protocol',
		-20027 => 'Bad error callback protocol',
		-20028 => 'Concurrency quota exceeded',
		-20029 => 'Document format not supported',
		-20030 => 'Text to translate is empty',
		-20031 => 'Missing text or document to translate',
		-20032 => 'Email address too long',
		-20033 => 'Cannot read stream',
		-20034 => 'Output format not supported',
		-20035 => 'Email destination tag is missing or empty',
		-20036 => 'HTTP destination tag is missing or empty',
		-20037 => 'FTP destination tag is missing or empty',
		-20038 => 'SFTP destination tag is missing or empty',
		-20039 => 'Document to translate tag is missing or empty',
		-20040 => 'Format tag is missing or empty',
		-20041 => 'The content is missing or empty',
		-20042 => 'Source language defined in TMX file differs from request',
		-20043 => 'Source language defined in XLIFF file differs from request',
		-20044 => 'Output format is not available when quality estimate is requested. It should be blank or \'xslx\'',
		-20045 => 'Quality estimate is not available for text snippet',
		-20046 => 'Document too big (>20Mb)',
		-20047 => 'Quality estimation not available',
		-40010 => 'Too many segments to translate',
		-80004 => 'Cannot store notification file at specified FTP address',
		-80005 => 'Cannot store notification file at specified SFTP address',
		-80006 => 'Cannot store translated file at specified FTP address',
		-80007 => 'Cannot store translated file at specified SFTP address',
		-90000 => 'Cannot connect to FTP',
		-90001 => 'Cannot retrieve file at specified FTP address',
		-90002 => 'File not found at specified address on FTP',
		-90007 => 'Malformed FTP address',
		-90012 => 'Cannot retrieve file content on SFTP',
		-90013 => 'Cannot connect to SFTP',
		-90014 => 'Cannot store file at specified FTP address',
		-90015 => 'Cannot retrieve file content on SFTP',
		-90016 => 'Cannot retrieve file at specified SFTP address',
	);

	/**
	 * Constructor for the eTranslation_Service class.
	 *
	 * @param string $username The eTranslation service username.
	 * @param string $password The eTranslation service password.
	 * @param bool   $settings Plugin settings.
	 */
	public function __construct( $username, $password, $settings ) {
		$this->username        = $username;
		$this->password        = $password;
		$this->use_curl_client = 'no' !== $settings['etm_advanced_settings']['etranslation_use_curl_client'];
	}

	/**
	 * Sends a translation document request to the eTranslation service.
	 *
	 * @param string $sourceLanguage The source language code.
	 * @param string $targetLanguage The target language code.
	 * @param string $content The content to be translated.
	 * @param string $domain The translation domain (default is 'GEN').
	 * @param string $id The ID of the translation request (optional).
	 * @return array An array with the response status and body.
	 */
	public function send_translate_document_request( $sourceLanguage, $targetLanguage, $content, $domain = 'GEN', $id = '' ) {
		$translationRequest = array(
			'documentToTranslateBase64' => $this->string_to_base64_data( $content ),
			'sourceLanguage'            => strtoupper( $sourceLanguage ),
			'targetLanguages'           => array(
				strtoupper( $targetLanguage ),
			),
			'errorCallback'             => get_rest_url() . 'etranslation/v1/error_callback/' . $id,
			'callerInformation'         => array(
				'application' => $this->username,
			),
			'destinations'              => array(
				'httpDestinations' => array(
					get_rest_url() . 'etranslation/v1/document/destination/' . $id,
				),
			),
			'domain'                    => $domain,
		);

		$post     = json_encode( $translationRequest );
		$response = $this->send_etranslation_request(
			self::$api_url . 'translate',
			'POST',
			$post,
			array(
				'Content-Type'   => 'application/json',
				'Content-Length' => strlen( $post ),
			)
		);

		$http_status = $response['response'];
		$body        = $response['body'];
		$request_id  = is_numeric( $body ) ? (int) $body : null;

		if ( $http_status !== 200 || $request_id < 0 ) {
			$message = self::$error_map[ $request_id ] ?? $body;
			$err     = $response['error'] ?? $body;
			error_log( "Invalid response from eTranslation: $err [status: $http_status, message: $message]. Please recheck your credentials or try again later!" );
		}

		return array(
			'response' => $http_status,
			'body'     => $body,
		);
	}

	/**
	 * Sends a domain request to the eTranslation service.
	 *
	 * @return array An array with the response status and body.
	 */
	public function send_domain_request() {
		return $this->send_etranslation_request( self::$api_url . 'get-domains', 'GET' );
	}

	/**
	 * Retrieves the available domain language pairs from the eTranslation service.
	 *
	 * @return array An array with the response status and body.
	 */
	public function get_available_domain_language_pairs() {
		$response    = $this->send_domain_request();
		$http_status = $response['response'];
		$body        = $response['body'];

		if ( $http_status !== 200 ) {
			error_log( "Error retrieving domains from eTranslation: $body [status: $http_status]. Please recheck your credentials or try again later!" );
		}

		return array(
			'response' => $http_status,
			'body'     => json_decode( $body ),
		);
	}

	/**
	 * Converts a string to base64 data format.
	 *
	 * @param string $string The input string.
	 * @return array An array containing base64 content, format, and filename.
	 */
	private function string_to_base64_data( $string ) {
		$base64_string = base64_encode( $string );
		return array(
			'content'  => $base64_string,
			'format'   => 'xml',
			'filename' => 'translateMe',
		);
	}

	/**
	 * Sends an eTranslation request with Digest authentication.
	 *
	 * @param string      $url The URL of the request.
	 * @param string      $method The HTTP method (GET or POST).
	 * @param string|null $body The request body (optional).
	 * @param array       $headers The request headers (optional).
	 * @return mixed The WP_Error object on failure, or the response object on success.
	 */
	private function send_etranslation_request( $url, $method, $body = null, $headers = array() ) {
		if ( $this->use_curl_client ) {
			return $this->send_etranslation_curl_request( $url, $method, $body, $headers );
		}

		$auth_value = $this->get_digest_auth_header_value( $url, $method );
		if ( $auth_value ) {
			$headers['Authorization'] = $auth_value;
			$args                     = array(
				'method'    => $method,
				'sslverify' => false,
				'redirects' => 5,
				'headers'   => $headers,
			);
			if ( $body ) {
				$args['body'] = $body;
			}
			$response = wp_remote_request( $url, $args );
			return array(
				'body'     => wp_remote_retrieve_body( $response ),
				'response' => wp_remote_retrieve_response_code( $response ),
				'error'    => is_wp_error( $response ) ? $response->get_error_message() : null,
				'raw'      => $response,
				'client'   => 'WordPress HTTP API',
			);
		} else {
			$status = 401;
			return array(
				'body'     => '',
				'response' => $status,
				'error'    => new WP_Error( $status, 'Failed to retrieve digest auth header' ),
				'raw'      => '',
				'client'   => 'WordPress HTTP API',
			);
		}
	}

	/**
	 * Sends eTranslation request using cURL. Avoids cURL error:0A000126 that occurs using WordPress HTTP API, cURL < 7.88.1 & OpenSSL 3
	 *
	 * @param string $url URL.
	 * @param string $method Method.
	 * @param string $body Body.
	 * @param array  $headers Headers.
	 * @return array
	 */
	private function send_etranslation_curl_request( $url, $method, $body = null, $headers = array() ) {
		$client = curl_init( $url );

		curl_setopt( $client, CURLOPT_RETURNTRANSFER, 1 );
		curl_setopt( $client, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST );
		curl_setopt( $client, CURLOPT_USERPWD, $this->username . ':' . $this->password );
		curl_setopt( $client, CURLOPT_SSL_VERIFYPEER, false );
		curl_setopt( $client, CURLOPT_FOLLOWLOCATION, true );

		$header_arr = array();
		foreach ( $headers as $key => $value ) {
			$header_arr[] = $key . ': ' . $value;
		}
		curl_setopt( $client, CURLOPT_HTTPHEADER, $header_arr );

		if ( 'POST' === $method ) {
			curl_setopt( $client, CURLOPT_POST, 1 );
			curl_setopt( $client, CURLOPT_POSTFIELDS, $body );
		}

		$response    = curl_exec( $client );
		$http_status = curl_getinfo( $client, CURLINFO_RESPONSE_CODE );
		$error       = curl_error( $client );
		curl_close( $client );

		return array(
			'body'     => $response,
			'response' => $http_status,
			'error'    => $error,
			'raw'      => $response,
			'client'   => 'cURL',
		);
	}

	/**
	 * Generates the Digest authentication header value for the eTranslation request.
	 *
	 * @param string $request_url The URL of the request.
	 * @param string $method The HTTP method (GET or POST).
	 * @return string|bool The Digest authentication header value or false on failure.
	 */
	private function get_digest_auth_header_value( $request_url, $method ) {
		$response = wp_remote_post( $request_url );
		$header   = wp_remote_retrieve_header( $response, 'WWW-Authenticate' );

		$retries = 2;
		while ( empty( $header ) && $retries > 0 ) {
			// request failed, retry.
			$response = wp_remote_post( $request_url );
			$header   = wp_remote_retrieve_header( $response, 'WWW-Authenticate' );
			$retries--;
		}
		if ( empty( $header ) ) {
			return false;
		}

		/*
		* Parses the 'www-authenticate' header for nonce, realm and other values.
		*/
		preg_match_all( '#(([\w]+)=["]?([^\s"]+))#', $header, $matches );
		$server_bits = array();
		foreach ( $matches[2] as $i => $key ) {
			$server_bits[ $key ] = $matches[3][ $i ];
		}

		$server_bits['realm'] = $server_bits['realm'] . ' Realm via Digest Authentication';
		$nc                   = '00000001';
		$path                 = parse_url( $request_url, PHP_URL_PATH );
		$client_nonce         = uniqid();
		$ha1                  = md5( $this->username . ':' . $server_bits['realm'] . ':' . $this->password );
		$ha2                  = md5( $method . ':' . $path );
		// The order of this array matters, because it affects resulting hashed val
		$response_bits = array(
			$ha1,
			$server_bits['nonce'],
			$nc,
			$client_nonce,
			$server_bits['qop'],
			$ha2,
		);

		$digest_header_values = array(
			'username'  => '"' . $this->username . '"',
			'realm'     => '"' . $server_bits['realm'] . '"',
			'nonce'     => '"' . $server_bits['nonce'] . '"',
			'uri'       => '"' . $path . '"',
			'algorithm' => '"MD5"',
			'qop'       => $server_bits['qop'],
			'nc'        => $nc,
			'cnonce'    => '"' . $client_nonce . '"',
			'response'  => '"' . md5( implode( ':', $response_bits ) ) . '"',
		);
		$digest_header        = 'Digest ';
		foreach ( $digest_header_values as $key => $value ) {
			$digest_header .= $key . '=' . $value . ', ';
		}
		$digest_header = rtrim( $digest_header, ', ' );
		return $digest_header;
	}

}
