<?php

namespace SugarCalendarBookings\Support\License;

use SugarCalendarBookings\Admin\Settings\LicenseSettingsTab;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Admin\Notices;
use SugarCalendarBookings\Support\Bootable;
use SugarCalendarBookings\Support\Helpers\Helpers;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * License key functionality.
 *
 * @since 1.0.0
 */
class License implements Bootable {

	/**
	 * Interval time, in days, to remote fetch the latest version.
	 *
	 * @since 1.0.0
	 *
	 * @var int
	 */
	const REMOTE_FETCH_LATEST_VERSION_INTERVAL_IN_DAYS = 7;

	/**
	 * Cache key for remote latest version.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const CACHE_REMOTE_LATEST_VERSION_KEY = 'sugar_calendar_bookings_latest_remote_version';

	/**
	 * Holds any license error messages.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $errors = [];

	/**
	 * Holds any license success messages.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $success = [];

	/**
	 * Plugin options.
	 *
	 * @since 1.0.0
	 *
	 * @var Options
	 */
	protected $options;

	/**
	 * License API service.
	 *
	 * @since 1.2.0
	 *
	 * @var LicenseApiService
	 */
	private $api_service;

	/**
	 * Primary class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		$this->options     = Plugin::get( Options::class );
		$this->api_service = Plugin::get( LicenseApiService::class );
	}

	/**
	 * Class initialization.
	 *
	 * @since 1.0.0
	 */
	public function init() {

		// Base initialization logic.
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.0.0
	 */
	public function hooks() {

		// Register licensing ajax action.
		add_action( 'wp_ajax_sugar_calendar_bookings_pro_license_ajax', [ $this, 'process_ajax' ] );
	}

	/**
	 * Get the license key.
	 *
	 * @since 1.2.0
	 *
	 * @return string
	 */
	protected function get_license_key() {

		return $this->options->get( 'license.key' );
	}

	/**
	 * Process AJAX requests fired by a pro version of a plugin and related to a license management.
	 *
	 * @since 1.0.0
	 */
	public function process_ajax() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$generic_error = esc_html__( 'Something went wrong. Please try again later.', 'sugar-calendar-bookings-scheduling-appointments-lite' );

		// Verify nonce.
		if (
			! isset( $_POST['nonce'] ) ||
			! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'sugar_calendar_bookings_pro_license_nonce' )
		) {
			wp_send_json_error( $generic_error );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( $generic_error );
		}

		$task = isset( $_POST['task'] ) ? sanitize_key( $_POST['task'] ) : '';

		switch ( $task ) {
			case 'license_verify':
				$license = isset( $_POST['license'] ) ? sanitize_key( $_POST['license'] ) : '';

				if ( empty( $license ) ) {
					wp_send_json_error( $generic_error );
				}

				$this->verify_key( $license, true );
				break;

			case 'license_deactivate':
				$this->deactivate_key( true );
				break;

			case 'license_refresh':
				$key = $this->options->get( 'license.key' );

				$this->validate_key( $key, true, true );
				break;
		}

		// Process unknown tasks or other edge cases.
		wp_send_json_error( $generic_error );
	}

	/**
	 * Verify a license key entered by the user.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key  License key.
	 * @param bool   $ajax Whether to send a JSON response.
	 *
	 * @return bool
	 */
	public function verify_key( $key = '', $ajax = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		if ( empty( $key ) ) {
			return false;
		}

		$verify = $this->api_service->verify_key( $key );

		// If it returns WP_Error, handle the error.
		if ( is_wp_error( $verify ) ) {
			$msg = $this->sanitize_api_message( $verify->get_error_message() );

			if ( $ajax ) {
				wp_send_json_error( $msg );
			} else {
				$this->errors[] = $msg;

				return false;
			}
		}

		$success         = isset( $verify['success'] ) ? $verify['success'] : esc_html__( 'Congratulations! This site is now receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$this->success[] = $success;

		$license_type = isset( $verify['type'] ) ? $verify['type'] : 'pro';

		// Update the option with verified license data.
		$data = [
			'license' => [
				'key'              => $key,
				'type'             => $license_type,
				'is_expired'       => false,
				'is_disabled'      => false,
				'is_invalid'       => false,
				'is_limit_reached' => false,
			],
		];

		$this->options->update( $data );

		wp_clean_plugins_cache( true );

		if ( $ajax ) {
			wp_send_json_success(
				[
					'type'    => $license_type,
					'message' => $success,
				]
			);
		}

		return true;
	}

	/**
	 * Validate a license key entered by the user.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key           License key.
	 * @param bool   $forced        Force to set contextual messages (false by default).
	 * @param bool   $ajax          AJAX.
	 * @param bool   $return_status Option to return the license status.
	 *
	 * @return string|bool
	 */
	public function validate_key( $key = '', $forced = false, $ajax = false, $return_status = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded

		$validate = $this->api_service->validate_key( $key );

		// If there was a basic API error in validation - do nothing.
		if ( is_wp_error( $validate ) ) {
			// If forced, set contextual error message.
			if ( $forced ) {
				$msg = $this->sanitize_api_message( $validate->get_error_message() );

				if ( $ajax ) {
					wp_send_json_error( $msg );
				} else {
					$this->errors[] = $msg;
				}
			}

			return false;
		}

		$data = [
			'license' => [
				'is_expired'       => false,
				'is_disabled'      => false,
				'is_invalid'       => false,
				'is_limit_reached' => false,
			],
		];

		// Handle different validation statuses.
		if ( $validate === 'invalid' ) {
			$data['license']['is_invalid'] = true;

			$this->options->update( $data );

			if ( $ajax ) {
				wp_send_json_error( esc_html__( 'Your license key for Sugar Calendar Bookings is invalid. The key no longer exists or the user associated with the key has been deleted. Please use a different key to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
			}

			return $return_status ? 'invalid' : false;
		}

		if ( $validate === 'expired' ) {
			$data['license']['is_expired'] = true;

			$this->options->update( $data );

			if ( $ajax ) {
				wp_send_json_error( esc_html__( 'Your license key for Sugar Calendar Bookings has expired. Please renew your license key on SugarCalendar.com to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
			}

			return $return_status ? 'expired' : false;
		}

		if ( $validate === 'disabled' ) {
			$data['license']['is_disabled'] = true;

			$this->options->update( $data );

			if ( $ajax ) {
				wp_send_json_error( esc_html__( 'Your license key for Sugar Calendar Bookings has been disabled. Please use a different key to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
			}

			return $return_status ? 'disabled' : false;
		}

		if ( $validate === 'limit_reached' ) {
			$data['license']['is_limit_reached'] = true;

			$this->options->update( $data );

			if ( $ajax ) {
				wp_send_json_error(
					sprintf(
						wp_kses( /* translators: %1$s - SugarCalendar.com account area URL; %2$s - SugarCalendar.com pricing page URL. */
							__( 'Sorry, but this license has no site activations left. You can update the list of your sites or upgrade the license in the <a href="%1$s" target="_blank" rel="noopener noreferrer">Account area</a>. Or you can <a href="%2$s" target="_blank" rel="noopener noreferrer">purchase a new license key</a>.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
							[
								'a' => [
									'href'   => [],
									'target' => [],
									'rel'    => [],
								],
							]
						),
						esc_url(
							'https://sugarcalendar.com/account/licenses/'
						),
						esc_url(
							Helpers::get_utm_url(
								'https://sugarcalendar.com/pricing/bookings/',
								[
									'medium'  => 'license-notice',
									'content' => 'limit-reached',
								]
							)
						)
					)
				);
			}

			return $return_status ? 'limit_reached' : false;
		}

		$license_type = $this->options->get( 'license.type' );

		// Otherwise, our check has returned successfully. Set the transient and update our license type and flags.
		$data['license']['type'] = $license_type;

		$this->options->update( $data );

		// If forced, set contextual success message.
		if ( $forced ) {
			$msg             = esc_html__( 'Your key has been refreshed successfully.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
			$this->success[] = $msg;

			if ( $ajax ) {
				wp_send_json_success(
					[
						'type'    => $license_type,
						'message' => $msg,
					]
				);
			}
		}

		return $return_status ? 'valid' : true;
	}

	/**
	 * Deactivate a license key entered by the user.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $ajax Whether to send a JSON response.
	 */
	public function deactivate_key( $ajax = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$license_key = $this->options->get( 'license.key' );

		if ( empty( $license_key ) ) {
			if ( $ajax ) {
				wp_send_json_success(
					[
						'message' => esc_html__( 'No license key found to deactivate.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					]
				);
			}

			return;
		}

		// Perform a request to deactivate the key.
		$deactivate = $this->api_service->deactivate_key( $license_key );

		// Clear the license key from the database regardless of API response.
		// This ensures users can always remove an invalid or problematic key.
		$data = [
			'license' => [
				'key'  => '',
				'type' => 'lite',
			],
		];

		$this->options->update( $data );

		// If it returns WP_Error, still consider it a success since the key is cleared locally.
		if ( is_wp_error( $deactivate ) ) {
			if ( $ajax ) {
				wp_send_json_success(
					[
						'message' => esc_html__( 'License key removed successfully.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					]
				);
			} else {
				$this->errors[] = $this->sanitize_api_message( $deactivate->get_error_message() );

				return;
			}
		}

		// Otherwise, our request has been done successfully. Set the success message.
		$success         = isset( $deactivate['success'] ) ? $deactivate['success'] : esc_html__( 'You have deactivated the key from this site successfully.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$this->success[] = $success;

		if ( $ajax ) {
			wp_send_json_success(
				[
					'message' => $success,
				]
			);
		}
	}

	/**
	 * Output any notices generated by the class (Pro only).
	 *
	 * @since 1.0.0
	 */
	public function notices() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		// Only users with sufficient capability can see the notices.
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$notices = Plugin::get( Notices::class );

		// If there is no license key, output nag about ensuring key is set for automatic updates.
		if ( empty( $this->options->get( 'license.key' ) ) ) {
			$notices->info(
				sprintf(
					wp_kses( /* translators: %s - plugin settings page URL. */
						__( 'Please <a href="%s">enter and activate</a> your license key for Sugar Calendar Bookings to enable automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						[
							'a' => [
								'href' => [],
							],
						]
					),
					esc_url( LicenseSettingsTab::get_url() )
				)
			);
		}

		// If a key has expired, output nag about renewing the key.
		if ( $this->options->get( 'license.is_expired' ) ) {
			$notices->error(
				sprintf(
					wp_kses( /* translators: %s - SugarCalendar.com login page URL. */
						__( 'Your license key for Sugar Calendar Bookings has expired. <a href="%s" target="_blank" rel="noopener noreferrer">Please click here to renew your license key and continue receiving automatic updates.</a>', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						[
							'a' => [
								'href'   => [],
								'target' => [],
								'rel'    => [],
							],
						]
					),
					esc_url( $this->get_renewal_link( 'renew your license key' ) )
				)
			);
		}

		// If a key has been disabled, output nag about using another key.
		if ( $this->options->get( 'license.is_disabled' ) ) {
			$notices->error(
				esc_html__( 'Your license key for Sugar Calendar Bookings has been disabled. Please use a different key to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' )
			);
		}

		// If a key is invalid, output nag about using another key.
		if ( $this->options->get( 'license.is_invalid' ) ) {
			$notices->error(
				esc_html__( 'Your license key for Sugar Calendar Bookings is invalid. The key no longer exists or the user associated with the key has been deleted. Please use a different key to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' )
			);
		}

		// If a key site activations limit reached, output nag about increasing the limit.
		if ( $this->options->get( 'license.is_limit_reached' ) ) {
			$notices->error(
				sprintf(
					wp_kses( /* translators: %1$s - SugarCalendar.com account area URL; %2$s - SugarCalendar.com pricing page URL. */
						__( '<b>Your Sugar Calendar Bookings license has no site activations left.</b> You can update the list of your sites or upgrade the license in the <a href="%1$s" target="_blank" rel="noopener noreferrer">Account area</a>. Or you can <a href="%2$s" target="_blank" rel="noopener noreferrer">purchase a new license key</a>.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
						[
							'b' => [],
							'a' => [
								'href'   => [],
								'target' => [],
								'rel'    => [],
							],
						]
					),
					esc_url( 'https://sugarcalendar.com/account/licenses/' ),
					esc_url(
						Helpers::get_utm_url(
							'https://sugarcalendar.com/pricing/bookings/',
							[
								'medium'  => 'license-notice',
								'content' => 'limit-reached-admin',
							]
						)
					)
				)
			);
		}

		// If there are any license errors, output them now.
		if ( ! empty( $this->errors ) ) {
			$notices->error(
				implode( '<br>', array_map( 'esc_html', $this->errors ) ),
				[
					'dismiss' => Notices::DISMISS_USER,
					'slug'    => 'license_errors',
				]
			);
		}

		// If there are any success messages, output them now.
		if ( ! empty( $this->success ) ) {
			$notices->success(
				implode( '<br>', array_map( 'esc_html', $this->success ) ),
				[
					'dismiss' => Notices::DISMISS_USER,
					'slug'    => 'license_success',
				]
			);
		}
	}

	/**
	 * The status of the license.
	 *
	 * @since 1.0.0
	 *
	 * @return array The results array with 'valid' (bool) and 'message' (string) attributes.
	 */
	public function get_status() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$license_key = $this->options->get( 'license.key' );

		$result = [
			'valid' => false,
		];

		if ( empty( $license_key ) ) {
			$result['message'] = sprintf(
				wp_kses( /* translators: %s - plugin settings page URL. */
					__( 'Please <a href="%s">enter and activate</a> your license key for Sugar Calendar Bookings to enable automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					[
						'a' => [
							'href' => [],
						],
					]
				),
				esc_url( LicenseSettingsTab::get_url() )
			);

			return $result;
		}

		$license_status = $this->validate_key( $license_key, false, false, true );

		if ( $license_status === false ) {
			$result['message'] = esc_html__( 'There was an error connecting to the remote server. Please try again later.', 'sugar-calendar-bookings-scheduling-appointments-lite' );

			return $result;
		}

		if ( $license_status === 'expired' ) {
			$result['message'] = sprintf(
				wp_kses( /* translators: %s - SugarCalendar.com login page URL. */
					__( 'Your license key for Sugar Calendar Bookings has expired. <a href="%s" target="_blank" rel="noopener noreferrer">Please click here to renew your license key and continue receiving automatic updates.</a>', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					[
						'a' => [
							'href'   => [],
							'target' => [],
							'rel'    => [],
						],
					]
				),
				esc_url( $this->get_renewal_link( 'renew your license key' ) )
			);

			return $result;
		}

		if ( $license_status === 'disabled' ) {
			$result['message'] = sprintf(
				wp_kses( /* translators: %s - plugin settings page URL. */
					__( 'Your license key for Sugar Calendar Bookings has been disabled. Please <a href="%s">enter and activate</a> a different key for Sugar Calendar Bookings to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					[
						'a' => [
							'href' => [],
						],
					]
				),
				esc_url( LicenseSettingsTab::get_url() )
			);

			return $result;
		}

		if ( $license_status === 'invalid' ) {
			$result['message'] = sprintf(
				wp_kses( /* translators: %s - plugin settings page URL. */
					__( 'Your license key for Sugar Calendar Bookings is invalid. Please <a href="%s">enter and activate</a> a different key for Sugar Calendar Bookings to continue receiving automatic updates.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					[
						'a' => [
							'href' => [],
						],
					]
				),
				esc_url( LicenseSettingsTab::get_url() )
			);

			return $result;
		}

		if ( $license_status === 'limit_reached' ) {
			$result['message'] = sprintf(
				wp_kses( /* translators: %1$s - SugarCalendar.com account area URL; %2$s - SugarCalendar.com pricing page URL. */
					__( 'Your Sugar Calendar Bookings license has no site activations left. You can update the list of your sites or upgrade the license in the <a href="%1$s" target="_blank" rel="noopener noreferrer">Account area</a>. Or you can <a href="%2$s" target="_blank" rel="noopener noreferrer">purchase a new license key</a>.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					[
						'a' => [
							'href'   => [],
							'target' => [],
							'rel'    => [],
						],
					]
				),
				esc_url(
					'https://sugarcalendar.com/account/licenses/'
				),
				esc_url(
					Helpers::get_utm_url(
						'https://sugarcalendar.com/pricing/bookings/',
						[
							'medium'  => 'license-validation',
							'content' => 'limit-reached',
						]
					)
				)
			);

			return $result;
		}

		return [
			'valid'   => true,
			'message' => esc_html__( 'Your Sugar Calendar Bookings license is active and valid.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
		];
	}

	/**
	 * Check whether the license is valid.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $remote Perform remote request or use DB license data.
	 *
	 * @return bool
	 */
	public function is_valid( $remote = false ) {

		if ( $remote ) {
			return $this->get_status()['valid'];
		}

		$saved_license = $this->options->get( 'license' );

		return ! empty( $saved_license['key'] ) &&
			empty( $saved_license['is_expired'] ) &&
			empty( $saved_license['is_disabled'] ) &&
			empty( $saved_license['is_invalid'] ) &&
			empty( $saved_license['is_limit_reached'] );
	}

	/**
	 * Check whether the license is expired.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_expired() {

		$saved_license = $this->options->get( 'license' );

		return ! empty( $saved_license['is_expired'] );
	}

	/**
	 * Renewal link used within the various admin pages.
	 *
	 * @since 1.0.0
	 *
	 * @param array|string $utm Array of UTM params, or if string provided - utm_content URL parameter.
	 *
	 * @return string
	 */
	public function get_renewal_link( $utm ) {

		$license_key = $this->options->get( 'license.key' );

		if ( ! empty( $license_key ) && strlen( $license_key ) === 32 ) {
			return add_query_arg(
				'edd_license_key',
				$license_key,
				'https://sugarcalendar.com/checkout/'
			);
		}

		return 'https://sugarcalendar.com/account/licenses/';
	}

	/**
	 * Sanitize a message returned by the remote license API.
	 *
	 * Remote messages may contain basic HTML (e.g. a "renew" link for expired keys).
	 * Allow only a small, safe set of tags so the message can be rendered as HTML
	 * in the admin UI without risking XSS.
	 *
	 * @since 1.4.0
	 *
	 * @param string $message Raw message from the remote API.
	 *
	 * @return string Sanitized HTML-safe message.
	 */
	protected function sanitize_api_message( $message ) {

		return wp_kses(
			(string) $message,
			[
				'a'      => [
					'href'   => [],
					'title'  => [],
					'target' => [],
					'rel'    => [],
				],
				'br'     => [],
				'strong' => [],
				'em'     => [],
				'b'      => [],
				'i'      => [],
			]
		);
	}

	/**
	 * Fetch the remote latest version.
	 *
	 * This method returns empty string in Lite version.
	 * Override in Pro version to fetch actual latest version.
	 *
	 * @since 1.0.0
	 * @since 1.2.0 Made overridable for Pro version.
	 *
	 * @param bool $force_remote Whether or not to force remote fetch. Optional. Default `false`.
	 *
	 * @return string
	 */
	public function fetch_latest_plugin_version( $force_remote = false ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found

		return '';
	}
}
