<?php
/**
 * License Manager
 *
 * Consolidates all license management functionality for WooCommerce Appointments.
 * This class handles SDK initialization, license status tracking, notices, and migrations.
 *
 * @package WooCommerce_Appointments
 * @since 5.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Appointments_License_Manager class.
 */
class WC_Appointments_License_Manager {

	/**
	 * License option name.
	 *
	 * @var string
	 */
	private const LICENSE_OPTION_NAME = 'woocommerce_appointments_license_key';

	/**
	 * Bad license statuses that should trigger notices.
	 *
	 * @var array
	 */
	private const BAD_STATUSES = [ 'inactive', 'expired', 'invalid', 'disabled', 'revoked', 'failed', 'deactivated' ];

	/**
	 * Whether license modal assets have been enqueued.
	 *
	 * @var bool
	 */
	private $license_modal_assets_enqueued = false;

	/**
	 * Instance of this class.
	 *
	 * @var WC_Appointments_License_Manager
	 */
	private static $instance = null;

	/**
	 * Get instance of this class.
	 *
	 * @return WC_Appointments_License_Manager
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		// Only initialize if we're in admin or during cron.
		if ( ! is_admin() && ! wp_doing_cron() ) {
			return;
		}

		$this->init();
	}

	/**
	 * Initialize license management.
	 *
	 * @return void
	 */
	private function init() {
		$this->load_plugin_license();
		add_action( 'admin_notices', [ $this, 'maybe_show_license_notice' ] );
		add_action( 'wc_appointments_refresh_license_status', [ $this, 'refresh_license_status' ] );
		add_action( 'deleted_option', [ $this, 'track_license_status_deletion' ], 10, 1 );
		
		// Customize expired license activation messages by intercepting AJAX response.
		add_action( 'wp_ajax_edd_sl_sdk_activate_woocommerce-appointments', [ $this, 'handle_expired_license_activation' ], 1 );
	}

	/**
	 * Load the plugin license SDK and register with the registry.
	 *
	 * @return void
	 */
	public function load_plugin_license() {
		// Production/distributed builds are expected to ship the SDK here.
		$sdk_path = WC_APPOINTMENTS_PLUGIN_PATH . '/includes/lib/edd-sl-sdk/edd-sl-sdk.php';

		if ( ! file_exists( $sdk_path ) ) {
			return;
		}

		// Load the EDD Software Licensing SDK.
		require_once $sdk_path;

		// Custom messenger to translate SDK strings with this plugin's text domain.
		require_once WC_APPOINTMENTS_PLUGIN_PATH . '/includes/class-wc-appointments-license-messenger.php';

		// Move existing license keys from the legacy storage to the SDK option.
		$this->migrate_legacy_license_key();

		add_action(
			'edd_sl_sdk_registry',
			static function ( $registry ) {
				if ( empty( $registry ) || ! is_callable( [ $registry, 'register' ] ) ) {
					return;
				}

				$registry->register(
					[
						'id'            => 'woocommerce-appointments',
						'url'           => apply_filters( 'woocommerce_appointments_license_store_url', 'https://bookingwp.com' ),
						'item_id'       => apply_filters( 'woocommerce_appointments_license_item_id', 78727 ),
						'version'       => WC_APPOINTMENTS_VERSION,
						'file'          => WC_APPOINTMENTS_MAIN_FILE,
						'option_name'   => self::LICENSE_OPTION_NAME,
						'type'          => 'plugin',
						'messenger_class' => 'WC_Appointments_License_Messenger',
					],
				);
			},
		);
	}

	/**
	 * Migrates the legacy stored license key into the SDK-managed option.
	 *
	 * @return void
	 */
	private function migrate_legacy_license_key() {
		$current_key = get_option( self::LICENSE_OPTION_NAME );

		// If we already have a key, check if we need to migrate status.
		if ( ! empty( $current_key ) ) {
			$this->maybe_migrate_legacy_license_status( $current_key );
			return;
		}

		$legacy_options = get_option( 'bizz_licenses_settings' );
		$legacy_key     = is_array( $legacy_options ) && isset( $legacy_options['bizz_woocommerce_appointments_license_key'] )
			? $legacy_options['bizz_woocommerce_appointments_license_key']
			: '';

		if ( ! empty( $legacy_key ) ) {
			update_option( self::LICENSE_OPTION_NAME, $legacy_key );
			// Track when key was stored for grace period calculation.
			if ( ! get_option( $this->get_stored_time_option_name() ) ) {
				update_option( $this->get_stored_time_option_name(), time() );
			}
			// Try to migrate license status if available.
			$this->maybe_migrate_legacy_license_status( $legacy_key );
		}
	}

	/**
	 * Migrates legacy license status if available and not expired.
	 *
	 * @param string $license_key The license key to check.
	 * @return void
	 */
	private function maybe_migrate_legacy_license_status( $license_key ) {
		$current_status = get_option( $this->get_status_option_name() );
		$license_flag   = $this->get_license_flag( $current_status );

		// If we already have a valid status, don't migrate.
		if ( $this->is_license_valid( $license_flag ) || 'expired' === $license_flag ) {
			return;
		}

		// Check for legacy license status in various possible locations.
		$legacy_options = get_option( 'bizz_licenses_settings' );
		$legacy_status  = null;

		// Check for status in legacy options array.
		if ( is_array( $legacy_options ) ) {
			// Try various possible keys for license status.
			$possible_keys = [
				'bizz_woocommerce_appointments_license_status',
				'woocommerce_appointments_license_status',
				'bizz_woocommerce_appointments_license',
			];

			foreach ( $possible_keys as $key ) {
				if ( isset( $legacy_options[ $key ] ) ) {
					$legacy_status = $legacy_options[ $key ];
					break;
				}
			}
		}

		// Also check for separate option.
		if ( empty( $legacy_status ) ) {
			$legacy_status = get_option( 'bizz_woocommerce_appointments_license_status' );
		}

		// If we found legacy status, migrate it if not expired.
		if ( ! empty( $legacy_status ) ) {
			// Handle both object and array formats.
			if ( is_object( $legacy_status ) || is_array( $legacy_status ) ) {
				$status_obj   = (object) $legacy_status;
				$license_flag = isset( $status_obj->license ) ? $status_obj->license : '';

				// Don't migrate if expired.
				if ( 'expired' === $license_flag ) {
					return;
				}

				// Migrate the status.
				update_option( $this->get_status_option_name(), $status_obj );
			} elseif ( is_string( $legacy_status ) && 'expired' !== $legacy_status ) {
				// Handle simple string status.
				update_option( $this->get_status_option_name(), (object) [ 'license' => $legacy_status ] );
			}
		} elseif ( ! empty( $license_key ) ) {
			// If we have a key but no status, try to refresh it.
			$this->maybe_refresh_license_status( $license_key );
		}
	}

	/**
	 * Attempts to refresh license status if key exists but status is missing or stale.
	 *
	 * @param string $license_key The license key to check.
	 * @param bool   $immediate   Whether to try immediate refresh (if possible) or schedule it.
	 * @return void
	 */
	private function maybe_refresh_license_status( $license_key, $immediate = false ) {
		if ( empty( $license_key ) ) {
			return;
		}

		$status           = get_option( $this->get_status_option_name() );
		$status_timestamp = 0;

		// Check if status has a timestamp.
		if ( is_object( $status ) ) {
			if ( isset( $status->last_check ) ) {
				$status_timestamp = (int) $status->last_check;
			} elseif ( $this->is_license_valid( $this->get_license_flag( $status ) ) ) {
				// If we have a valid status but no timestamp, assume it's fresh enough.
				return;
			} elseif ( ! empty( $status->success ) && empty( $status->license ) ) {
				// If success is true, license is valid even without license field.
				return;
			}
		}

		// Only refresh if status is missing or older than 24 hours.
		$should_refresh = empty( $status ) || ! is_object( $status );
		if ( ! $should_refresh && $status_timestamp > 0 ) {
			$should_refresh = ( time() - $status_timestamp ) > DAY_IN_SECONDS;
		}

		if ( $should_refresh ) {
			// Use a transient to prevent multiple simultaneous refreshes.
			$refresh_lock = get_transient( 'wc_appointments_license_refresh_lock' );
			if ( false === $refresh_lock ) {
				set_transient( 'wc_appointments_license_refresh_lock', time(), 60 ); // Lock for 1 minute.

				if ( $immediate && ! wp_doing_cron() && ! wp_doing_ajax() ) {
					// Schedule immediate refresh (within next minute).
					wp_schedule_single_event( time() + 5, 'wc_appointments_refresh_license_status' );
				} else {
					// Schedule a background refresh to avoid blocking page load.
					wp_schedule_single_event( time() + 10, 'wc_appointments_refresh_license_status' );
				}
			}
		}
	}

	/**
	 * Encourages users to activate the license with a dismissible notice.
	 *
	 * @return void
	 */
	public function maybe_show_license_notice() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		// Avoid showing the license nag on the "What's New" landing page.
		if ( isset( $_GET['page'] ) && 'wc-appointments-whats-new' === sanitize_key( wp_unslash( $_GET['page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}

		$status      = get_option( $this->get_status_option_name() );
		$license_key = get_option( self::LICENSE_OPTION_NAME );
		$license_flag = $this->get_license_flag( $status );

		// If we have a valid/active license, don't show notice.
		if ( $this->is_license_valid( $license_flag ) ) {
			return;
		}

		// If no license key at all, show notice immediately.
		if ( empty( $license_key ) ) {
			$this->render_license_notice( false );
			return;
		}

		// We have a license key - check status.
		$should_show = false;

		// If status exists and has a license flag, check it.
		if ( ! empty( $license_flag ) ) {
			$should_show = $this->is_license_bad( $license_flag );
		} else {
			// Status is missing - this could mean:
			// 1. License was just activated and status hasn't been fetched yet (need grace period)
			// 2. License was deactivated (status was deleted)
			// 3. Temporary API issue

			// Try to refresh immediately.
			$this->maybe_refresh_license_status( $license_key, true );

			// Re-check status after refresh attempt.
			$status       = get_option( $this->get_status_option_name() );
			$license_flag = $this->get_license_flag( $status );

			if ( $this->is_license_valid( $license_flag ) ) {
				return;
			}

			if ( ! empty( $license_flag ) ) {
				$should_show = $this->is_license_bad( $license_flag );
			} else {
				// Status is still missing after refresh attempt.
				$key_stored_time = get_option( $this->get_stored_time_option_name(), 0 );
				if ( empty( $key_stored_time ) ) {
					update_option( $this->get_stored_time_option_name(), time() );
					$key_stored_time = time();
				}

				// Show notice if status missing for >1 hour or recently deleted (likely deactivation).
				$grace_period      = HOUR_IN_SECONDS;
				$time_since_stored = time() - $key_stored_time;
				$status_deleted_time = get_option( $this->get_status_deleted_time_option_name(), 0 );

				$should_show = ( $time_since_stored >= $grace_period ) || ( $status_deleted_time > 0 && ( time() - $status_deleted_time ) < 300 );
			}
		}

		if ( $should_show ) {
			$this->render_license_notice( true );
		}
	}

	/**
	 * Tracks when license status option is deleted (indicates deactivation).
	 *
	 * @param string $option_name The option name being deleted.
	 * @return void
	 */
	public function track_license_status_deletion( $option_name ) {
		if ( $option_name === $this->get_status_option_name() ) {
			update_option( $this->get_status_deleted_time_option_name(), time() );
		}
	}

	/**
	 * Refreshes the license status by checking with the license server.
	 *
	 * @return void
	 */
	public function refresh_license_status() {
		$license_key = get_option( self::LICENSE_OPTION_NAME );
		if ( empty( $license_key ) ) {
			return;
		}

		$api_config = $this->get_api_config();
		if ( ! $api_config ) {
			return;
		}

		$license_data = $api_config['api']->make_request( [
			'edd_action' => 'check_license',
			'license'    => $license_key,
			'item_id'    => $api_config['item_id'],
		] );

		if ( ! empty( $license_data ) && is_object( $license_data ) ) {
			$license_data->last_check = time();
			update_option( $this->get_status_option_name(), $license_data );
			delete_option( $this->get_status_deleted_time_option_name() );
			delete_transient( 'wc_appointments_license_refresh_lock' );
		}
	}

	/**
	 * Enqueue SDK assets and overlay so the notice button can open the modal.
	 *
	 * @return void
	 */
	public function enqueue_license_modal_assets() {
		if ( $this->license_modal_assets_enqueued ) {
			return;
		}

		if ( ! class_exists( 'EasyDigitalDownloads\\Updater\\Utilities\\Path' ) ) {
			return;
		}

		$path    = \EasyDigitalDownloads\Updater\Utilities\Path::class;
		$baseurl = \EasyDigitalDownloads\Updater\Utilities\Path::get_url();
		$version = \EasyDigitalDownloads\Updater\Utilities\Path::get_version();

		echo '<div class="edd-sdk-notice--overlay"></div>';

		wp_enqueue_script( 'edd-sdk-notice', $baseurl . 'assets/build/js/edd-sl-sdk.js', [], $version, true );
		wp_enqueue_style( 'edd-sdk-notice', $baseurl . 'assets/build/css/style-edd-sl-sdk.css', [], $version );
		wp_localize_script(
			'edd-sdk-notice',
			'edd_sdk_notice',
			[
				'ajax_url'     => admin_url( 'admin-ajax.php' ),
				'nonce'        => wp_create_nonce( 'edd_sdk_notice' ),
				'activating'   => esc_html__( 'Activating...', 'woocommerce-appointments' ),
				'deactivating' => esc_html__( 'Deactivating...', 'woocommerce-appointments' ),
				'error'        => esc_html__( 'There was an error. Please try again.', 'woocommerce-appointments' ),
			],
		);

		// Add inline script to hide license notice after successful activation.
		wp_add_inline_script(
			'edd-sdk-notice',
			"(function() {
				'use strict';
				// Use MutationObserver to watch for success notices after activation
				const observer = new MutationObserver(function(mutations) {
					mutations.forEach(function(mutation) {
						mutation.addedNodes.forEach(function(node) {
							if (node.nodeType === 1 && node.classList && node.classList.contains('notice-success')) {
								// Check if this is a license activation success message
								const noticeText = node.textContent || '';
								if (noticeText.includes('successfully activated') || noticeText.includes('activated')) {
									// Hide the license activation notice in admin area
									document.querySelectorAll('.notice.notice-warning').forEach(function(notice) {
										const warningText = notice.textContent || '';
										const activateButton = notice.querySelector('.edd-sdk__notice__trigger--ajax, button.edd-sdk__notice__trigger');
										if (activateButton || warningText.includes('license is not active') || warningText.includes('Activate license')) {
											notice.style.display = 'none';
										}
									});
								}
							}
						});
					});
				});
				// Start observing when DOM is ready
				if (document.readyState === 'loading') {
					document.addEventListener('DOMContentLoaded', function() {
						observer.observe(document.body, { childList: true, subtree: true });
					});
				} else {
					observer.observe(document.body, { childList: true, subtree: true });
				}
			})();",
		);

		$this->license_modal_assets_enqueued = true;
	}

	/**
	 * Render license notice HTML.
	 *
	 * @param bool $has_key Whether a license key is stored.
	 * @return void
	 */
	private function render_license_notice( $has_key = false ) {
		printf(
			'<div class="notice notice-warning"><p>%1$s&nbsp;%2$s</p></div>',
			wp_kses_post( $this->get_notice_message( $has_key ) ),
			wp_kses_post( $this->get_notice_button() )
		);
		add_action( 'admin_footer', [ $this, 'enqueue_license_modal_assets' ] );
	}

	/**
	 * Handle expired license activation errors with custom exciting renewal message.
	 * Intercepts the activation AJAX request to check for expired errors before SDK processes it.
	 *
	 * @return void
	 */
	public function handle_expired_license_activation() {
		// Check permissions first.
		if ( ! current_user_can( 'manage_options' ) ) {
			return; // Let SDK handle permission errors.
		}

		// Check nonce and token (similar to SDK's can_manage_license).
		$nonce     = filter_input( INPUT_POST, 'nonce', FILTER_SANITIZE_SPECIAL_CHARS );
		$token     = filter_input( INPUT_POST, 'token', FILTER_SANITIZE_SPECIAL_CHARS );
		$timestamp = filter_input( INPUT_POST, 'timestamp', FILTER_SANITIZE_SPECIAL_CHARS );
		
		if ( empty( $timestamp ) || empty( $token ) || empty( $nonce ) ) {
			return; // Let SDK handle validation.
		}

		if ( ! class_exists( '\EasyDigitalDownloads\Updater\Utilities\Tokenizer' ) ) {
			return; // SDK not loaded, let it handle.
		}

		if ( ! \EasyDigitalDownloads\Updater\Utilities\Tokenizer::is_token_valid( $token, $timestamp ) || ! wp_verify_nonce( $nonce, 'edd_sl_sdk_license_handler' ) ) {
			return; // Let SDK handle invalid token/nonce.
		}

		$license_key = filter_input( INPUT_POST, 'license', FILTER_SANITIZE_SPECIAL_CHARS );
		if ( empty( $license_key ) ) {
			return;
		}

		$api_config = $this->get_api_config();
		if ( ! $api_config ) {
			return;
		}

		$license_check = $api_config['api']->make_request( [
			'edd_action' => 'check_license',
			'license'    => $license_key,
			'item_id'    => $api_config['item_id'],
		] );

		if ( ! empty( $license_check ) && isset( $license_check->license ) && 'expired' === $license_check->license ) {
			if ( class_exists( 'WC_Appointments_License_Messenger' ) ) {
				$messenger = new WC_Appointments_License_Messenger();
				$expiration_date = $this->format_expiration_date( $license_check->expires ?? '' );
				
				wp_send_json_error( [
					'message' => wpautop( $messenger->get_expired_activation_message( $expiration_date ) ),
				] );
			}
		}
		
		// If not expired or messenger not available, let SDK handle it normally.
		// Just return and let the SDK's hook (priority 10) process the activation.
		return;
	}

	/**
	 * Get the license option name.
	 *
	 * @return string
	 */
	public static function get_license_option_name() {
		return self::LICENSE_OPTION_NAME;
	}

	/**
	 * Get the license status option name.
	 *
	 * @return string
	 */
	private function get_status_option_name() {
		return self::LICENSE_OPTION_NAME . '_license';
	}

	/**
	 * Get the stored time option name.
	 *
	 * @return string
	 */
	private function get_stored_time_option_name() {
		return self::LICENSE_OPTION_NAME . '_stored_time';
	}

	/**
	 * Get the status deleted time option name.
	 *
	 * @return string
	 */
	private function get_status_deleted_time_option_name() {
		return self::LICENSE_OPTION_NAME . '_status_deleted_time';
	}

	/**
	 * Extract license flag from status object.
	 *
	 * @param object|array|null $status The license status object.
	 * @return string The license flag or empty string.
	 */
	private function get_license_flag( $status ) {
		if ( ! is_object( $status ) ) {
			return '';
		}

		$license_flag = isset( $status->license ) ? $status->license : '';
		
		// Also check success flag - if success is true, license is valid even if license field is empty.
		if ( empty( $license_flag ) && ! empty( $status->success ) ) {
			$license_flag = 'valid';
		}

		return $license_flag;
	}

	/**
	 * Check if license status is valid/active.
	 *
	 * @param string $license_flag The license flag to check.
	 * @return bool
	 */
	private function is_license_valid( $license_flag ) {
		return in_array( $license_flag, [ 'valid', 'active' ], true );
	}

	/**
	 * Check if license status is bad (should show notice).
	 *
	 * @param string $license_flag The license flag to check.
	 * @return bool
	 */
	private function is_license_bad( $license_flag ) {
		return in_array( $license_flag, self::BAD_STATUSES, true );
	}

	/**
	 * Get license API configuration.
	 *
	 * @return array Array with 'item_id', 'api_url', and 'api' instance.
	 */
	private function get_api_config() {
		if ( ! class_exists( '\EasyDigitalDownloads\Updater\Requests\API' ) ) {
			return null;
		}

		$item_id = apply_filters( 'woocommerce_appointments_license_item_id', 78727 );
		$api_url = apply_filters( 'woocommerce_appointments_license_store_url', 'https://bookingwp.com' );
		$api     = new \EasyDigitalDownloads\Updater\Requests\API( $api_url );

		return [
			'item_id' => $item_id,
			'api_url' => $api_url,
			'api'     => $api,
		];
	}

	/**
	 * Format expiration date for display.
	 *
	 * @param mixed $expires The expiration value (timestamp or date string).
	 * @return string Formatted date or empty string.
	 */
	private function format_expiration_date( $expires ) {
		if ( empty( $expires ) || 'lifetime' === $expires ) {
			return '';
		}

		$timestamp = is_numeric( $expires ) ? $expires : strtotime( $expires );
		return $timestamp ? date_i18n( get_option( 'date_format' ), $timestamp ) : '';
	}

	/**
	 * Generate license notice button HTML.
	 *
	 * @return string Button HTML.
	 */
	private function get_notice_button() {
		$item_id = apply_filters( 'woocommerce_appointments_license_item_id', 78727 );
		return sprintf(
			'<button type="button" class="button button-primary edd-sdk__notice__trigger edd-sdk__notice__trigger--ajax" data-id="license-control" data-product="%1$s" data-slug="woocommerce-appointments" data-name="%2$s">%3$s</button>',
			esc_attr( $item_id ),
			esc_attr__( 'WooCommerce Appointments', 'woocommerce-appointments' ),
			esc_html__( 'Activate license', 'woocommerce-appointments' )
		);
	}

	/**
	 * Generate license notice message HTML.
	 *
	 * @param bool $has_key Whether a license key is stored.
	 * @return string Message HTML.
	 */
	private function get_notice_message( $has_key = false ) {
		$message = sprintf(
			/* translators: %s: Plugin name */
			esc_html__( '%s license is not active. Activate now to receive updates and support.', 'woocommerce-appointments' ),
			esc_html__( 'WooCommerce Appointments', 'woocommerce-appointments' )
		);

		if ( $has_key ) {
			$message .= ' ' . esc_html__( 'A key is stored but not activated.', 'woocommerce-appointments' );
		}

		return $message;
	}
}

