<?php
/**
 * @package Polylang-Pro
 */

namespace WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Settings;

use PLL_Language;
use PLL_Model;
use WP_Error;
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Clients\Deepl as Client;
use WP_Syntex\Polylang_Pro\Modules\Machine_Translation\Services\Deepl as Service;

defined( 'ABSPATH' ) || exit;

/**
 * Machine translation settings: DeepL.
 *
 * @since 3.6
 */
class Deepl implements Settings_Interface {
	/**
	 * Name of the action to check the API key.
	 *
	 * @since 3.6
	 *
	 * @var string
	 */
	const API_KEY_ACTION = 'pll_deepl_check_api_key';

	/**
	 * Name of the action to get the DeepL usage.
	 *
	 * @since 3.6
	 *
	 * @var string
	 */
	const USAGE_ACTION = 'pll_deepl_get_usage';

	/**
	 * Name of the action to get the glossaries.
	 *
	 * @since 3.8
	 *
	 * @var string
	 */
	const GLOSSARIES_ACTION = 'pll_deepl_get_glossaries';

	/**
	 * Service.
	 *
	 * @var Service
	 */
	private $service;

	/**
	 * Polylang's model.
	 *
	 * @var PLL_Model
	 */
	private $model;

	/**
	 * Base of the name attribute used by the inputs.
	 *
	 * @var string
	 *
	 * @phpstan-var non-falsy-string
	 */
	private $input_base_name;

	/**
	 * Stores the fields' options.
	 *
	 * @var array
	 */
	private $options;

	/**
	 * Constructor.
	 *
	 * @since 3.6
	 *
	 * @param string    $input_base_name Base of the name attribute used by the inputs.
	 *                                   Can contain a placeholder `{slug}` that will be replaced by the service's slug.
	 *                                   Ex: `machine_translation_services[{slug}]`.
	 * @param array     $options         Service's options.
	 * @param Service   $service         Service.
	 * @param PLL_Model $model           Polylang's model.
	 *
	 * @phpstan-param non-falsy-string $input_base_name
	 */
	public function __construct( string $input_base_name, array $options, Service $service, PLL_Model $model ) {
		$this->service         = $service;
		$this->model           = $model;
		$this->input_base_name = str_replace( '{slug}', $service::get_slug(), $input_base_name );
		$this->options         = $options;

		add_action( 'wp_ajax_' . self::API_KEY_ACTION, array( $this, 'check_api_key' ) );
		add_action( 'wp_ajax_' . self::USAGE_ACTION, array( $this, 'update_characters_consumption_view' ) );
		add_action( 'wp_ajax_' . self::GLOSSARIES_ACTION, array( $this, 'update_glossaries_view' ) );
	}

	/**
	 * Ajax callback that checks for the API key validity.
	 *
	 * @since 3.6
	 *
	 * @return void
	 *
	 * @phpstan-return never
	 */
	public function check_api_key() {
		check_ajax_referer( self::API_KEY_ACTION, '_pll_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( -1 );
		}

		if ( empty( $_GET['api_key'] ) || ! is_string( $_GET['api_key'] ) ) {
			wp_send_json_error(
				array(
					'message'       => esc_html__( 'Please fill in the API key field.', 'polylang-pro' ),
					'message_class' => 'pll-message-error-auth', // See `get_error_message_class()`.
				)
			);
		}

		$api_key = (string) sanitize_text_field( wp_unslash( $_GET['api_key'] ) );
		$valid   = $this->is_api_key_valid( array( 'api_key' => $api_key ) );

		if ( $valid->has_errors() ) {
			// The key is invalid or we had a failure while checking it.
			wp_send_json_error(
				array(
					'message'       => esc_html( $valid->get_error_message() ),
					'message_class' => $this->get_error_message_class( $valid ),
				)
			);
		}

		wp_send_json_success();
	}

	/**
	 * Displays the characters consumption view.
	 *
	 * @since 3.6
	 *
	 * @return void
	 *
	 * @phpstan-return never
	 */
	public function update_characters_consumption_view() {
		check_ajax_referer( self::USAGE_ACTION, '_pll_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( -1 );
		}

		$usage = $this->service->get_client()->get_usage();

		if ( is_wp_error( $usage ) ) {
			// Error while retrieving the data: display the error message.
			wp_send_json_error(
				array(
					'message' => esc_html(
						sprintf(
							/* translators: %s is an error message. */
							__( 'Error while retrieving the data: %s.', 'polylang-pro' ),
							$usage->get_error_message()
						)
					),
				)
			);
		}

		if ( ! $usage['character_limit'] ) {
			// The character limit is 0: display only the character count.
			wp_send_json_success(
				array(
					'message' => esc_html(
						sprintf(
							/* translators: %s is a formatted count number. */
							_n( '%s translated character.', '%s translated characters.', $usage['character_count'], 'polylang-pro' ),
							number_format_i18n( $usage['character_count'] )
						)
					),
				)
			);
		}

		// Display a graphic.
		$percent  = round( $usage['character_count'] * 100 / $usage['character_limit'], 1 );
		$percent  = (float) min( $percent, 100 );
		$decimals = 1;

		if ( floor( $percent ) === $percent ) {
			$decimals = 0;
		}

		wp_send_json_success(
			array(
				'percent_formatted' => number_format_i18n( $percent, $decimals ) . '%',
				'percent'           => (string) $percent . '%',
				'message'           => esc_html(
					sprintf(
						/* translators: %1$s is a formatted count number, %2$s is a formatted limit number. */
						_n( '%1$s / %2$s translated character.', '%1$s / %2$s translated characters.', $usage['character_count'], 'polylang-pro' ),
						number_format_i18n( $usage['character_count'] ),
						number_format_i18n( $usage['character_limit'] )
					)
				),
			)
		);
	}

	/**
	 * Displays the glossaries view.
	 *
	 * @since 3.8
	 *
	 * @return void
	 *
	 * @phpstan-return never
	 */
	public function update_glossaries_view() {
		check_ajax_referer( self::GLOSSARIES_ACTION, '_pll_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( -1 );
		}

		if ( isset( $_GET['api_key'] ) ) {
			$api_key    = is_string( $_GET['api_key'] ) ? (string) sanitize_text_field( wp_unslash( $_GET['api_key'] ) ) : '';
			$glossaries = $this->get_new_client( $api_key )->get_glossaries();
		} else {
			$glossaries = $this->service->get_client()->get_glossaries();
		}

		if ( is_wp_error( $glossaries ) ) {
			// Error while retrieving the data: display the error message.
			wp_send_json_error(
				array(
					'message' => esc_html(
						sprintf(
							/* translators: %s is an error message. */
							__( 'Error while retrieving the data: %s.', 'polylang-pro' ),
							$glossaries->get_error_message()
						)
					),
				)
			);
		}

		wp_send_json_success(
			array(
				'glossaries' => $glossaries,
				'selected'   => $this->options['glossary_id'],
			)
		);
	}

	/**
	 * Tells if the given service options contain a non-empty authentication key.
	 *
	 * @since 3.6
	 *
	 * @param array $options Options for this service.
	 * @return bool
	 */
	public function has_api_key( array $options ): bool {
		return ! empty( $options['api_key'] ) && is_string( $options['api_key'] ) && '' !== trim( $options['api_key'] );
	}

	/**
	 * Tells if the authentication key from the given service options is valid by contacting the service.
	 *
	 * @since 3.6
	 *
	 * @param array $options Options for this service (must be sanitized beforehand).
	 * @return WP_Error {
	 *    An empty `WP_Error` if the authentication succeeded.
	 *    In the other cases, the `WP_Error` data will contain an array as follow:
	 *
	 *    @type string $type     `'error'` if the API key is invalid, or `'warning'` if there was an error while
	 *                           contacting the service.
	 *    @type string $field_id CSS ID of the field in fault.
	 * }
	 */
	public function is_api_key_valid( array $options ): WP_Error {
		if ( ! $this->has_api_key( $options ) ) {
			$options['api_key'] = '';
		}

		$error = $this->get_new_client( $options['api_key'] )->is_api_key_valid();

		if ( ! $error->has_errors() ) {
			// The key is valid.
			return $error;
		}

		$error->add_data(
			array(
				'message_class' => $this->get_error_message_class( $error ),
				'field_id'      => 'pll-deepl-api-key',
			)
		);

		return $error;
	}

	/**
	 * Prints error notices.
	 *
	 * @since 3.6
	 *
	 * @return void
	 */
	public function print_notices() {
		if ( $this->service->is_active() ) {
			$this->print_view(
				'inner-notices-row',
				array(
					'name'      => $this->service->get_name(),
					'languages' => $this->get_unsupported_languages(),
				)
			);
		}
	}

	/**
	 * Prints settings fields.
	 *
	 * @since 3.6
	 *
	 * @return void
	 */
	public function print_settings_fields() {
		$this->print_view(
			'characters-consumption-row',
			array(
				'ajax_action' => self::USAGE_ACTION,
			)
		);

		$this->print_view(
			'service-authentication-row',
			array(
				'ajax_action'     => self::API_KEY_ACTION,
				/* translators: %s is a service name. */
				'button_label'    => sprintf( __( 'Check connection to %s', 'polylang-pro' ), $this->service->get_name() ),
				'id'              => 'deepl-api-key',
				'message_default' => sprintf(
					/* translators: %1$s is an opening link tag leading to account creation, %2$s is an opening link tag leading to the "API keys" page, %3$s is a closing link tag. */
					__( '%1$sCreate your account on DeepL%3$s, then %2$sfind your API key in the "API Keys & Limits" tab%3$s.', 'polylang-pro' ),
					'<a href="https://www.deepl.com/pro-api">',
					'<a href="https://www.deepl.com/your-account/keys">',
					'</a>'
				),
				'messages_error'  => array(
					'pll-message-error-auth'        => sprintf(
						/* translators: %1$s is an opening link tag leading to the service's "API keys" page, %2$s is a closing link tag. */
						__( '%1$sVerify your API key in the "API Keys & Limits" tab%2$s.', 'polylang-pro' ),
						'<a href="https://www.deepl.com/your-account/keys">',
						'</a>'
					),
					'pll-message-error-unavailable' => sprintf(
						/* translators: %1$s is an opening link tag leading to the service's status page, %2$s is a closing link tag. */
						__( 'You can look at %1$sthe DeepL Pro/Free API\'s status%2$s.', 'polylang-pro' ),
						'<a href="https://www.deeplstatus.com/">',
						'</a>'
					),
				),
				'message_success' => __( 'Your API key is valid.', 'polylang-pro' ),
				'option'          => 'api_key',
				'title'           => __( 'API key', 'polylang-pro' ),
			)
		);

		$this->print_view(
			'deepl-formality-row',
			array(
				'option'   => 'formality',
				'formal'   => $this->get_active_languages_by_formality( 'formal' ),
				'informal' => $this->get_active_languages_by_formality( 'informal' ),
			)
		);

		$this->print_view(
			'deepl-glossary-row',
			array(
				'option'      => 'glossary_id',
				'ajax_action' => self::GLOSSARIES_ACTION,
				'description' => sprintf(
					/* translators: %1$s is an opening link tag, %2$s is a closing link tag. */
					__( 'Select which glossary to use for the translation process. %1$sEdit your glossaries on DeepL%2$s.', 'polylang-pro' ),
					'<a href="https://www.deepl.com/glossary">',
					'</a>'
				),
			)
		);
	}

	/**
	 * Prints a view.
	 *
	 * @since 3.6
	 *
	 * @param string $view Name of the view.
	 * @param array  $atts Optional. Data to print. See views headers.
	 * @return void
	 */
	private function print_view( string $view, array $atts = array() ) {
		$atts['slug']            = $this->service::get_slug();
		$atts['input_base_name'] = $this->input_base_name;

		if ( isset( $atts['option'] ) && ! isset( $atts['value'] ) ) {
			$atts['value'] = $this->options[ $atts['option'] ] ?? '';
		}

		include POLYLANG_PRO_DIR . "/src/modules/Machine_Translation/Views/{$view}.php";
	}

	/**
	 * Returns the lists of languages that are not supported by the service.
	 *
	 * @since 3.6
	 * @return string[] Array of language names (and their locale).
	 *
	 * @phpstan-return list<string>
	 */
	private function get_unsupported_languages(): array {
		$languages = array();

		foreach ( $this->model->get_languages_list() as $language ) {
			if ( empty( $this->service::get_target_code( $language ) ) ) {
				$languages[] = $this->get_language_label( $language );
			}
		}

		sort( $languages );

		return $languages;
	}

	/**
	 * Returns the lists of active formal or informal languages.
	 * Formal languages have a locale with a `_formal` suffix (`de_DE_formal`, `nl_NL_formal`),
	 * Informal languages have a `_informal` suffix (`de_CH_informal`).
	 *
	 * @since 3.6
	 *
	 * @param string $formality Formality.
	 * @return string[] Array of arrays of language names (and their locale).
	 *
	 * @phpstan-param 'formal'|'informal' $formality
	 * @phpstan-return list<string>
	 */
	private function get_active_languages_by_formality( string $formality ): array {
		$languages = array();

		foreach ( $this->model->get_languages_list() as $language ) {
			if ( empty( $this->service::get_target_code( $language ) ) ) {
				continue;
			}

			if ( ! preg_match( "@_{$formality}$@", $language->locale ) ) {
				continue;
			}

			$languages[] = $this->get_language_label( $language );
		}

		sort( $languages );

		return $languages;
	}

	/**
	 * Returns a language name and its locale.
	 *
	 * @since 3.6
	 *
	 * @param PLL_Language $language A language object.
	 * @return string
	 */
	private function get_language_label( PLL_Language $language ): string {
		return sprintf(
			/* translators: %1$s is a language name, %2$s is a language locale. */
			_x( '%1$s (%2$s)', 'Language label', 'polylang-pro' ),
			$language->name,
			sprintf( '<code>%s</code>', $language->locale )
		);
	}

	/**
	 * Returns the HTML class corresponding to the given error.
	 * See the array keys for the 'messages_error' in `print_settings_fields()`.
	 *
	 * @since 3.6
	 *
	 * @param WP_Error $error An error object.
	 * @return string `'pll-message-error-auth'` in case of authentication failure, `'pll-message-error-unavailable'` in other cases.
	 */
	private function get_error_message_class( WP_Error $error ): string {
		return 'pll_deepl_authentication_failure' === $error->get_error_code() ? 'pll-message-error-auth' : 'pll-message-error-unavailable';
	}

	/**
	 * Returns a new client instance.
	 *
	 * @since 3.8
	 *
	 * @param string $api_key The API key (must be sanitized beforehand).
	 * @return Client
	 */
	private function get_new_client( string $api_key = '' ): Client {
		$service = new Service( array( 'api_key' => $api_key ), $this->model );
		return $service->get_client();
	}
}
