<?php
/**
 * Licence manager.
 *
 * @package Ealab\Deployer
 */

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

use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBasedObject;
use EaLab\WP_Deployer\vendor\Dakwamine\Component\Exception\UnmetDependencyException;
use DateTimeInterface;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\EaLabWebServiceException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\InvalidConfigException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\LicenceException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\LicenceKeyUnsetException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\DeployerConfigRepository\DeployerConfig;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Environment;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\InvalidValue;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Settings;
use Throwable;

/**
 * Licence manager.
 */
class LicenceManager extends ComponentBasedObject {
	public const FREEMIUM_LICENCE_KEY = 'e7fb57d0-964e-46e6-9471-c2c78b9a1180';

	private const OPTION__EXPIRATION_DATE = 'deploy_sync_content__expiration_date';

	private const LICENCE_ERROR__EXPIRED = 10;

	/**
	 * Root instance.
	 *
	 * @var ComponentBasedObject|null
	 */
	private static ?ComponentBasedObject $instance = null;

	/**
	 * Gets the main instance.
	 *
	 * @return static|ComponentBasedObject The instance.
	 *
	 * @throws UnmetDependencyException Instantiation error.
	 */
	public static function getInstance(): ComponentBasedObject {
		if ( empty( static::$instance ) ) {
			static::$instance = static::getRootComponentByClassName( static::class, true );
		}

		return static::$instance;
	}

	/**
	 * Gets the licence key.
	 *
	 * @return string
	 *   The licence key. When there is no registered key, the freemium key
	 *   will be returned.
	 *
	 * @throws \Throwable
	 *   Licence key retrieval error.
	 */
	public function getLicenceKey(): string {
		$licence_key = $this->getRegisteredLicenceKey();

		return empty( $licence_key ) ? static::FREEMIUM_LICENCE_KEY : $licence_key;
	}

	/**
	 * The expiration date.
	 *
	 * @return \DateTimeInterface|InvalidValue|null The datetime object, null if no expiration date,
	 *     or InvalidValue if not set or invalid.
	 */
	private function getRegisteredExpirationDate() {
		$expiration_date = get_option( static::OPTION__EXPIRATION_DATE, new InvalidValue() );

		if ( $expiration_date instanceof InvalidValue ) {
			return $expiration_date;
		}

		if ( empty( $expiration_date ) ) {
			return null;
		}

		$parsed = \DateTime::createFromFormat( DateTimeInterface::ATOM, $expiration_date, new \DateTimeZone( 'UTC' ) );

		return empty( $parsed ) ? new InvalidValue() : $parsed;
	}

	/**
	 * Gets the licence key saved in the DB.
	 *
	 * @return string|null The licence key, or null if unset. Please note that an empty string is possible.
	 *
	 * @throws Throwable Code error.
	 */
	public function getRegisteredLicenceKey(): ?string {
		$key = Settings::getInstance()->get( Settings::SETTINGS__LICENCE_KEY, new InvalidValue() );

		return $key instanceof InvalidValue ? null : $key;
	}

	/**
	 * Revalidates the licence key against the EaLab web service.
	 *
	 * @param array|null $options Options:
	 *     - "force_revalidate" (bool): Forces the revalidation if true.
	 *
	 * @throws EaLabWebServiceException Web service error.
	 * @throws LicenceException Licence error.
	 * @throws LicenceKeyUnsetException Licence key is unset.
	 * @throws WpDeployException Deploy & Sync Content error.
	 * @throws Throwable Any other unmanaged errors.
	 */
	private function revalidateLicenceKey( ?array $options = null ): void {
		$force_revalidate = $options['force_revalidate'] ?? false;

		if ( ! $force_revalidate && Environment::isProductionEnvironment() ) {
			// We didn't ask to revalidate, we are not on the settings UI, and we are on the production environment.
			// Do not ping the licencing web service to prevent potential hangs.
			throw new LicenceKeyUnsetException( _dgettext( 'deployer-core', 'The revalidation of the <em>Deploy & Sync Content</em> licence is required, but we could not revalidate the licence because this is the production environment. Please go to the settings UI of <em>Deploy & Sync Content</em> and save again the licence key.' ) );
		}

		$licence_key = $this->getLicenceKey();

		if ( empty( $licence_key ) ) {
			// No key, no gain.
			throw new LicenceKeyUnsetException( _dgettext( 'deployer-core', 'The licence key is not set.' ) );
		}

		// Validate the key on the licencing web service.
		// Simply use the config retriever, which already takes care of the licencing.
		$licence_status = DeployerConfig::getLicenceStatus();

		if ( array_key_exists( 'expiresOn', $licence_status['licenceStatus'] ?? array() ) ) {
			$new_expiration_date = $licence_status['licenceStatus']['expiresOn'];

			$this->saveExpirationDate( $new_expiration_date );
		}

		if ( ! ( $licence_status['isValid'] ?? false ) ) {
			// Dev note: in case of invalid licence, errors should have been triggered.
			// But this is in case the response gave a 2xx code.
			$additional_messages = array_map(
				static function ( $message ) {
					if ( ! is_array( $message ) ) {
						return '';
					}

					$type    = $message['type'] ?? '';
					$message = $message['message'] ?? '';

					if ( 'error' !== $type ) {
						return '';
					}

					return $message;
				},
				$licence_status['messages'] ?? array()
			);

			$additional_messages = implode( ', ', array_filter( $additional_messages ) );
			$additional_messages = empty( $additional_messages ) ? '' : ' Reason(s): ' . $additional_messages;

			throw new LicenceException( sprintf( _dgettext( 'deployer-core', 'The licence is invalid.%s' ), esc_html( $additional_messages ) ) );
		}
	}

	/**
	 * Saves the expiration date.
	 *
	 * @param DateTimeInterface|string|null $date_time The date as a DateTimeInterface, a formatted string
	 *     (ATOM expected), or null to tell "no expiration".
	 */
	private function saveExpirationDate( $date_time ): void {
		if ( $date_time instanceof DateTimeInterface ) {
			$date_time = $date_time->format( DateTimeInterface::ATOM );
		}

		update_option( static::OPTION__EXPIRATION_DATE, $date_time );
	}

	/**
	 * Checks if the licence key in DB is valid.
	 *
	 * @param array<string,mixed> $options Options:
	 *     - "is_validating_from_ui" (bool): Set to true if this method is called from the UI. False by default.
	 *       This should be set to true when using the UI to edit the settings, or when expired;
	 *       it changes the output messages.
	 *     - "allow_revalidation" (bool): Set to true to allow revalidation. True by default.
	 *
	 * @throws EaLabWebServiceException Web service error.
	 * @throws InvalidConfigException Configuration is invalid.
	 * @throws LicenceException Licence error.
	 * @throws LicenceKeyUnsetException Missing licence key.
	 * @throws Throwable Other unmanaged errors.
	 * @throws WpDeployException Deploy & Sync Content errors.
	 */
	public function validateLicenceKey( ?array $options = null ): void {
		try {
			$has_revalidated_once = false;

			$is_validating_from_ui = $options['is_validating_from_ui'] ?? false;
			$allow_revalidation    = $options['allow_revalidation'] ?? true;

			if ( $is_validating_from_ui ) {
				// Force revalidation, even on production environments.
				// Some errors may already happen here.
				$allow_revalidation && $this->revalidateLicenceKey(
					array(
						'force_revalidate' => true,
					)
				);
				$has_revalidated_once = true;
			}

			// We check the current expiration date saved in DB.
			$current_registered_date = $this->getRegisteredExpirationDate();

			if ( $current_registered_date instanceof InvalidValue ) {
				// The date is unset or invalid.
				if ( ! $has_revalidated_once && $is_validating_from_ui && $allow_revalidation && ! Environment::isProductionEnvironment() ) {
					// Not revalidated yet, and we are validating from UI, and we are not on a production environment.
					// Let's revalidate.
					$this->revalidateLicenceKey(
						array(
							'force_revalidate' => true,
						)
					);
					$has_revalidated_once = true;

					$current_registered_date = $this->getRegisteredExpirationDate();

					$date_is_invalid = $current_registered_date instanceof InvalidValue;
				} else {
					// Already revalidated, or we are not allowed to revalidate the date.
					$date_is_invalid = true;
				}
			}

			$standard_error_message = _dgettext( 'deployer-core', 'The <em>Deploy & Sync Content</em> licence did not validate successfully. Please check your licence key, the connection status, or try validating later.' );

			if ( ! empty( $date_is_invalid ) ) {
				// Nothing more can be done.
				throw new LicenceException( $standard_error_message );
			}

			if ( ! empty( $current_registered_date ) ) {
				// There is a date.
				// Make sure that it is after the now date.
				$now_date = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );

				if ( $current_registered_date < $now_date ) {
					// The currently registered date is from the past.
					$expired_message = _dgettext( 'deployer-core', 'The licence for <em>Deploy & Sync Content</em> is expired.' );

					if ( $has_revalidated_once ) {
						// No need to go further; the licence is expired.
						throw new LicenceException( $expired_message );
					}

					// Try to revalidate automatically to check if the expiration date
					// has been updated on the web service side.
					// Allow forced revalidation on production instances only if validating from UI.
					$has_revalidated_once = true;
					$allow_revalidation && $this->revalidateLicenceKey(
						array(
							'force_revalidate' => $is_validating_from_ui,
						)
					);

					// We check the new expiration date saved in DB.
					$new_registered_date = $this->getRegisteredExpirationDate();

					if ( $new_registered_date instanceof InvalidValue ) {
						// Nothing more can be done.
						throw new LicenceException( $standard_error_message );
					}

					if ( ! empty( $new_registered_date ) && $new_registered_date < $now_date ) {
						// The new date is a real date (meaning it's not a perpetual licence)
						// and it is still a date from the past.
						throw new LicenceException( $expired_message, static::LICENCE_ERROR__EXPIRED );
					}
				}
			}
		} catch ( EaLabWebServiceException $e ) {
			// The licencing web service is not reachable.
			// Let's throw the error again, but do not remove the locally saved expiration date.
			throw $e;
		} catch ( \Throwable $t ) {
			if ( $t instanceof LicenceException && static::LICENCE_ERROR__EXPIRED !== $t->getCode() ) {
				// There is a licencing problem, but not an expiration one.
				// Remove the saved date, if any.
				delete_option( static::OPTION__EXPIRATION_DATE );
			}

			// Rethrow the error.
			throw $t;
		}

		// The licence revalidated successfully.
		$has_revalidated_once && do_action( 'deploy_sync_content__licence_manager__on_licence_key_successful_revalidation' );
	}
}
