<?php
/**
 * License management service.
 *
 * @package   PopupMaker
 * @copyright Copyright (c) 2024, Code Atlantic LLC
 */

namespace PopupMaker\Services;

use PopupMaker\Base\Service;

defined( 'ABSPATH' ) || exit;

/**
 * Pro license management.
 *
 * Retained as a thin, temporary compatibility bridge for active Pro 1.1.0 and
 * older installations, including when their license needs activation. Pro
 * 1.2.0+ does not use this Core lifecycle.
 *
 * @package PopupMaker
 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
 */
class License extends Service {

	/**
	 * EDD API URL.
	 *
	 * @var string
	 */
	const API_URL = 'https://wppopupmaker.com/edd-sl-api/';

	/**
	 * Item ID.
	 *
	 * @var int
	 */
	const ID = 480187;

	/**
	 * Option key.
	 *
	 * @var string
	 */
	const OPTION_KEY = 'popup_maker_license';

	/**
	 * Settings key.
	 *
	 * @var string
	 */
	const SETTINGS_KEY = 'popup_maker_pro_license_key';

	/**
	 * Loss delay transient.
	 *
	 * @var string
	 */
	const DELAY_TRANSIENT = 'popup_maker_license_loss_delay';

	/**
	 * Loss check transient.
	 *
	 * @var string
	 */
	const CHECK_TRANSIENT = 'popup_maker_license_loss_check';

	/**
	 * License data
	 *
	 * @var array{key:string|null,status:array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null,auto_activation?:array{enabled:bool,first_activated:string|null,last_seen:string|null,key_missing_since:string|null,key_hash:string|null}}|null
	 */
	private $license_data;

	/**
	 * Whether the guarded legacy hooks have been registered.
	 *
	 * @var bool
	 */
	private $hooks_registered = false;

	/**
	 * Initialize license management.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @param \PopupMaker\Plugin\Core $container Plugin container.
	 */
	public function __construct( $container ) {
		parent::__construct( $container );

		// Core initializes at plugins_loaded priority 11 and Pro at 12.
		// Defer until Pro is loaded so its active state and version are available.
		add_action( 'plugins_loaded', [ $this, 'maybe_register_hooks' ], 100 );
	}

	/**
	 * Register the legacy hooks only for an active old Pro install.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return void
	 */
	public function maybe_register_hooks() {
		if ( ! $this->should_run_license_lifecycle() ) {
			return;
		}

		$this->register_hooks();
	}

	/**
	 * Register hooks.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->hooks_registered || ! $this->should_run_license_lifecycle() ) {
			return;
		}

		$this->hooks_registered = true;

		add_action( 'init', [ $this, 'autoregister' ] );
		add_action( 'popup_maker_license_status_check', [ $this, 'refresh_license_status' ] );
		add_action( 'admin_init', [ $this, 'schedule_crons' ] );
		add_filter( 'pum_settings_editor_args', [ $this, 'filter_settings_editor_args' ] );
	}

	/**
	 * Autoregister license with simplified security approach.
	 *
	 * For auto-activated licenses:
	 * - Never stores the actual key in database (uses placeholder)
	 * - Simple detection and setup without complex monitoring
	 * - Self-healing: automatically corrects DB state if needed
	 * - Detects loss of auto-activation for debugging
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return void
	 */
	public function autoregister() {
		if ( ! $this->should_run_license_lifecycle() ) {
			return;
		}

		$constant_key = defined( 'POPUP_MAKER_PRO_LICENSE' ) && '' !== POPUP_MAKER_PRO_LICENSE ? POPUP_MAKER_PRO_LICENSE : false;
		$license_data = $this->get_license_data();

		// Handle new auto-activation setup.
		if ( $constant_key ) {
			// Clear any existing delay transients when key is detected.
			$this->clear_transients();

			$this->ensure_auto_activation_setup( $constant_key );
		} else {
			$is_auto_activated = ! empty( $license_data['auto_activation']['enabled'] );
			$constant_lost     = ! empty( $license_data['auto_activation']['constant_lost_at'] );

			// Only auto-managed placeholder keys are subject to constant-loss
			// handling. A manually entered key must never be wiped by it, and
			// stale enabled=false metadata must not arm the detector.
			$is_placeholder = '***AUTO***' === ( $license_data['key'] ?? '' );

			if ( $is_auto_activated && $is_placeholder && ! $constant_lost ) {
				$this->handle_auto_activation_loss_with_delay();
			}
		}
	}

	/**
	 * Clear transients.
	 *
	 * @return void
	 */
	private function clear_transients(): void {
		delete_transient( self::DELAY_TRANSIENT );
		delete_transient( self::CHECK_TRANSIENT );
	}

	/**
	 * Handle loss of auto-activation constant with transient-based delay.
	 *
	 * Uses simple transient system to delay loss detection by 5 minutes,
	 * preventing false positives during plugin updates and WordPress operations.
	 *
	 * @return void
	 */
	private function handle_auto_activation_loss_with_delay(): void {
		// If delay transient exists, we're still waiting - do nothing
		if ( get_transient( self::DELAY_TRANSIENT ) ) {
			return;
		}

		// If check transient doesn't exist, start the delay period (first detection)
		if ( ! get_transient( self::CHECK_TRANSIENT ) ) {
			set_transient( self::DELAY_TRANSIENT, true, 10 );
			set_transient( self::CHECK_TRANSIENT, true, 0 ); // Non-expiring transient
			return;
		}

		// Delay expired and we still don't have the constant - mark as lost
		$this->handle_auto_activation_loss();

		// Clean up the check transient
		delete_transient( self::CHECK_TRANSIENT );
	}

	/**
	 * Ensure auto-activation is properly set up with simplified approach.
	 *
	 * @param string $key The license key from constant.
	 *
	 * @return void
	 */
	private function ensure_auto_activation_setup( string $key ): void {
		$license_data = $this->get_license_data();
		$db_key       = isset( $license_data['key'] ) ? $license_data['key'] : '';

		// A manually configured license takes precedence. Add-on packages can
		// ship a POPUP_MAKER_PRO_LICENSE constant; converting an existing real
		// key to auto-management would leave the site unlicensed when the
		// defining add-on is later deactivated.
		if ( '' !== $db_key && '***AUTO***' !== $db_key ) {
			return;
		}

		// Self-healing: ensure DB has placeholder, not real key
		if ( '***AUTO***' !== $db_key || empty( $license_data['auto_activation']['enabled'] ) ) {
			$license_data = [
				'key'             => '***AUTO***',
				'status'          => $license_data['status'] ?? null, // Preserve existing status
				'auto_activation' => [
					'enabled' => true,
				],
			];

			$this->update_license_data( $license_data );

			// Activate using the constant key if not already active
			if ( ! $this->is_license_active() ) {
				try {
					$this->maybe_activate_license( $key );
				// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				} catch ( \Exception $e ) {
					// Do nothing on activation failure.
				}
			}
		} elseif ( ! empty( $license_data['auto_activation']['constant_lost_at'] ) ) {
			// Constant was restored - clear the loss timestamp and grace period
			unset( $license_data['auto_activation']['constant_lost_at'] );
			$this->update_license_data( $license_data );

			// Clear delay transients since constant is back
			$this->clear_transients();

			// Log restoration for debugging
			\PopupMaker\logging()->info( 'Auto-activation constant POPUP_MAKER_PRO_LICENSE was restored at ' . gmdate( 'Y-m-d H:i:s' ) );
		}
	}

	/**
	 * Handle loss of auto-activation constant.
	 *
	 * Tracks when auto-activation is lost for debugging and monitoring.
	 * Updates the license data with loss information and clears transients.
	 * Also clears license status to reflect the loss in UI.
	 *
	 * @return void
	 */
	private function handle_auto_activation_loss(): void {
		$license_data = $this->get_license_data();

		// A real key means the user activated manually — never wipe it.
		if ( '***AUTO***' !== ( $license_data['key'] ?? '' ) ) {
			$this->clear_transients();
			return;
		}

		// Prefer restoring the retained settings key over leaving the site
		// unlicensed — the constant's key is mirrored there on activation.
		$mirror_key = \PUM_Utils_Options::get( self::SETTINGS_KEY, '' );

		if ( is_string( $mirror_key ) && '' !== trim( $mirror_key ) && '***AUTO***' !== trim( $mirror_key ) ) {
			$license_data['key']             = trim( $mirror_key );
			$license_data['status']          = null;
			$license_data['auto_activation'] = null;

			$this->update_license_data( $license_data );
			$this->clear_transients();

			\PopupMaker\logging()->info( 'Auto-activation constant POPUP_MAKER_PRO_LICENSE was removed; restored the retained license key at ' . gmdate( 'Y-m-d H:i:s' ) );

			return;
		}

		// Mark as lost and clear license status so UI reflects the change
		$license_data['auto_activation']['constant_lost_at'] = gmdate( 'Y-m-d H:i:s' );

		// Mark as lost and clear license status so UI reflects the change
		$license_data['auto_activation']['enabled'] = false;

		// Clear the key and status so UI shows inactive.
		$license_data['key']    = '';
		$license_data['status'] = null; // Clear cached status so UI shows inactive

		// Update the license data.
		$this->update_license_data( $license_data );

		// Clear the check transient since we've now marked it as lost
		$this->clear_transients();

		$current_time = gmdate( 'Y-m-d H:i:s' );

		// Log for debugging
		\PopupMaker\logging()->warning( 'Auto-activation constant POPUP_MAKER_PRO_LICENSE was removed at ' . $current_time );
	}

	/**
	 * Schedule cron jobs.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return void
	 */
	public function schedule_crons() {
		if ( ! $this->should_run_license_lifecycle() ) {
			return;
		}

		if ( ! wp_next_scheduled( 'popup_maker_license_status_check' ) ) {
			wp_schedule_event( time(), 'weekly', 'popup_maker_license_status_check' );
		}
	}

	/**
	 * Get license data.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return array{key:string|null,status:array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null,auto_activation?:array{enabled:bool,constant_lost_at?:string|null}}
	 */
	public function get_license_data() {
		if ( ! isset( $this->license_data ) ) {
			$this->license_data = \get_option( self::OPTION_KEY, [
				'key'             => '',
				'status'          => null,
				'auto_activation' => null,
			] );
		}

		return $this->license_data;
	}

	/**
	 * Update license data.
	 *
	 * @param array{key:string|null,status:array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null,auto_activation?:array{enabled:bool,constant_lost_at?:string|null}} $license_data License data.
	 *
	 * @return bool
	 */
	private function update_license_data( array $license_data ): bool {
		if ( \update_option( self::OPTION_KEY, $license_data ) ) {
			$this->license_data = $license_data;

			return true;
		}

		return false;
	}

	/**
	 * Get license key for display purposes.
	 *
	 * For auto-activated licenses, this always returns the constant value
	 * or placeholder - never exposes real keys from database.
	 *
	 * @uses \PopupMaker\Services\License::get_license_data() For source of truth.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string
	 */
	public function get_license_key(): string {
		$license_data = $this->get_license_data();

		// Handle auto-activated licenses securely
		if ( ! empty( $license_data['auto_activation']['enabled'] ) ) {
			// For auto-activated, always return constant or placeholder
			return defined( 'POPUP_MAKER_PRO_LICENSE' ) && ! empty( POPUP_MAKER_PRO_LICENSE )
				? POPUP_MAKER_PRO_LICENSE
				: $license_data['key']; // Will be '***AUTO***' placeholder
		}

		// Return regular stored key
		return ! empty( $license_data['key'] ) ? $license_data['key'] : '';
	}

	/**
	 * Get license key for EDD API calls (extensions, updater, etc.).
	 *
	 * Unlike get_license_key(), this never returns display placeholders.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string
	 */
	public function get_api_license_key(): string {
		$key = $this->get_raw_license_key();

		return null !== $key ? $key : '';
	}

	/**
	 * Get raw license key for internal API calls only.
	 *
	 * This method should NEVER be used for display purposes.
	 * For auto-activated licenses, it always retrieves from the constant.
	 *
	 * @return string|null
	 */
	private function get_raw_license_key(): ?string {
		$license_data = $this->get_license_data();

		// For auto-activated licenses, always use the constant
		if ( ! empty( $license_data['auto_activation']['enabled'] ) ) {
			return defined( 'POPUP_MAKER_PRO_LICENSE' ) && ! empty( POPUP_MAKER_PRO_LICENSE ) ? POPUP_MAKER_PRO_LICENSE : null;
		}

		// For regular licenses, use stored key
		return ! empty( $license_data['key'] ) ? $license_data['key'] : null;
	}

	/**
	 * Get license status.
	 *
	 * @uses \PopupMaker\Services\License::get_license_data() For source of truth.
	 *
	 * @param bool $refresh Whether to refresh license status.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null
	 */
	public function get_license_status_data( ?bool $refresh = false ) {
		if ( $refresh ) {
			$this->refresh_license_status();
		}

		$license_data = $this->get_license_data();

		$license_status = isset( $license_data['status'] ) ? wp_parse_args( $license_data['status'], [
			'success'          => false, // bool.
			'license'          => 'invalid', // valid or invalid.
			'item_id'          => self::ID, // int or false.
			'item_name'        => '', // string.
			'license_limit'    => 1, // int. 0 = unlimited.
			'site_count'       => 0,
			'expires'          => '', // date or 'lifetime'.
			'activations_left' => 0, // int or 'unlimited'.
			'checksum'         => '', // string.
			'payment_id'       => 0, // int.
			'customer_name'    => '', // string.
			'customer_email'   => '', // string.
			'price_id'         => 0, // string or int.
		] ) : null;

		return $license_status;
	}

	/**
	 * Get license status.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return 'empty'|'inactive'|'expired'|'error'|'valid'
	 */
	public function get_license_status(): string {
		$license_key = $this->get_license_key();
		$status_data = $this->get_license_status_data();

		if ( empty( $license_key ) || empty( $status_data ) ) {
			return 'empty';
		}

		$status = 'inactive';

		// activate_license 'invalid' on anything other than valid, so if there was an error capture it
		if ( false === $status_data['success'] ) {
			$error = isset( $status_data['error'] ) ? $status_data['error'] : $status;

			switch ( $error ) {
				case 'expired':
					$status = 'expired';
					break;
				case 'revoked':
				case 'missing':
				case 'invalid':
				case 'site_inactive':
				case 'item_name_mismatch':
				case 'no_activations_left':
				case 'license_not_activable':
				default:
					$status = 'error';
					break;
			}
		} elseif ( 'valid' === $status_data['license'] ) {
				$status = 'valid';
		} else {
			$status = 'deactivated';
		}

		return $status;
	}

	/**
	 * Get license level.
	 *
	 * Only used in pro version.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return int Integer representing license level.
	 */
	public function get_license_level(): int {
		$license_status = $this->get_license_status_data();

		if ( empty( $license_status ) ) {
			return -1;
		}

		$price_id = isset( $license_status['price_id'] ) ? $license_status['price_id'] : null;

		switch ( $price_id ) {
			default:
				return -1;

			case false:
			case 0:
				return 0;

			case 1:
			case 2:
			case 3:
			case 4:
				return absint( $price_id );
		}
	}

	/**
	 * Get license tier (pro or pro_plus).
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string 'pro' or 'pro_plus' based on license data.
	 */
	public function get_license_tier(): string {
		$license_status = $this->get_license_status_data();

		if ( empty( $license_status ) ) {
			return 'pro';
		}

		// Check if license_tier is explicitly set in the license data.
		if ( ! empty( $license_status['license_tier'] ) ) {
			return in_array( $license_status['license_tier'], [ 'pro', 'pro_plus' ], true )
				? $license_status['license_tier']
				: 'pro';
		}

		// Fallback to 'pro' if not specified.
		return 'pro';
	}

	/**
	 * Update license key.
	 *
	 * Side Effects:
	 * - Update License Data
	 * - Reset License Status
	 * - Fire action: popup_maker_license_key_changed
	 *
	 * @param string $key License key.
	 *
	 * @return bool
	 */
	private function update_license_key( string $key ): bool {
		$license_data = $this->get_license_data();

		$new_key = trim( $key );
		$old_key = isset( $license_data['key'] ) ? $license_data['key'] : '';

		if ( $old_key === $new_key ) {
			return false;
		}

		$updated = $this->update_license_data( [
			'key'    => $new_key,
			'status' => null,
		] );

		if ( $updated ) {
			\PUM_Utils_Options::update( self::SETTINGS_KEY, $new_key );

			/**
			 * Fires when license key is changed.
			 *
			 * @param string $key License key.
			 * @param string $old_key Old license key.
			 */
			\do_action( 'popup_maker_license_key_changed', $new_key, $old_key );
		}

		return $updated;
	}

	/**
	 * Update license status.
	 *
	 * @param array<string,mixed> $license_status License status data.
	 *
	 * @return bool
	 */
	private function update_license_status( ?array $license_status ): bool {
		// Callers may pass null/empty on a failed or empty API response; never
		// fatal the typed setter and never clobber stored status with nothing.
		if ( empty( $license_status ) ) {
			return false;
		}

		$license_data = $this->get_license_data();

		$previous_status = isset( $license_data['status'] ) ? $license_data['status'] : [];

		if ( ! empty( $license_status['error'] ) ) {
			$license_status['error_message'] = $this->get_license_error_message( $license_status );
		} else {
			unset( $license_status['error_message'] );
			unset( $license_status['error'] );
		}

		$license_data['status'] = $license_status;

		if ( $this->update_license_data( $license_data ) ) {
			/**
			 * Fires when license status is updated.
			 *
			 * @param array $license_status License status data.
			 * @param array $previous_status Previous license status data.
			 */
			\do_action( 'popup_maker_license_status_updated', $license_status, $previous_status );

			return true;
		}

		return false;
	}

	/**
	 * Get license expiration from license status data.
	 *
	 * @param bool $as_datetime Whether to return as DateTime object.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string|null|\DateTime
	 */
	public function get_license_expiration( ?bool $as_datetime = false ) {
		$status_data = $this->get_license_status_data();

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

		$expiration = isset( $status_data['expires'] ) ? $status_data['expires'] : null;

		return $as_datetime ?
			\DateTime::createFromFormat( 'Y-m-d H:i:s', $expiration ) :
			$expiration;
	}

	/**
	 * Fetch license status from remote server.
	 * This is a blocking request.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function refresh_license_status(): bool {
		if ( ! $this->should_run_license_lifecycle() ) {
			return false;
		}

		$key = $this->get_raw_license_key();

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

		try {
			$status = $this->check_license_status();
		} catch ( \Exception $e ) {
			$status = null;
		}

		// A transient failure must not overwrite a previously valid status.
		if ( empty( $status ) ) {
			return false;
		}

		return $this->update_license_status( $status );
	}

	/**
	 * Call the API.
	 *
	 * @param string     $action The action to call.
	 * @param array|null $params The parameters to pass to the API.
	 *
	 * @return array|null
	 *
	 * @throws \Exception If there is an error.
	 */
	private function api_call( string $action, ?array $params = null ) {
		$key = $this->get_raw_license_key();

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

		$api_params = [
			'edd_action'  => $action,
			'license'     => $key,
			'item_id'     => self::ID,
			'item_name'   => rawurlencode( 'Popup Maker Pro' ),
			'url'         => home_url(),
			'environment' => function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production',
		];

		// Call the custom API.
		$response = wp_remote_post(
			self::API_URL,
			array_merge( $params ?? [], [
				'timeout'   => 15,
				'sslverify' => ! in_array( wp_get_environment_type(), [ 'local', 'development' ], true ),
				'body'      => $api_params,
			] )
		);

		// Make sure the response came back okay.
		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			if ( is_wp_error( $response ) ) {
				$message = $response->get_error_message();
			} else {
				$message = __( 'An error occurred, please try again.', 'popup-maker' );
			}

			throw new \Exception( esc_html( $message ) );
		}

		$license_status = json_decode( wp_remote_retrieve_body( $response ), true );

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

		// No structure validation Fix: Validate expected fields exist and types match

		return $license_status;
	}

	/**
	 * Get license status from remote server.
	 *
	 * Side Effects:
	 * - API Call: Check License Status
	 *
	 * @return array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null
	 *
	 * @throws \Exception If there is an error.
	 */
	private function check_license_status() {
		return $this->api_call( 'check_license' );
	}

	/**
	 * Activate license.
	 *
	 * Side Effects:
	 * - API Call: Check License Status
	 * - Update Pro Activation Date
	 * - Update License Status
	 *
	 * Kept callable so existing Core 1.23.x and Pro versions older than 1.2.0
	 * can activate, deactivate, and recover licenses during the short migration
	 * window. Pro 1.2.0+ does not use this method.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 *
	 * @throws \Exception If there is an error.
	 */
	public function activate_license(): bool {
		$license_status = $this->api_call( 'activate_license' );

		// Bail early if the license status is empty.
		if ( empty( $license_status ) ) {
			return false;
		}

		$this->update_license_status( $license_status );

		if ( $this->is_license_active() ) {
			if ( ! \get_option( 'popup_maker_pro_activation_date', false ) ) {
				\update_option( 'popup_maker_pro_activation_date', time() );
			}
		}

		/**
		 * Fires when license is activated.
		 *
		 * @param array $license_status License status data.
		 */
		\do_action( 'popup_maker_license_activated', $license_status );

		return $this->is_license_active();
	}

	/**
	 * Deactivate license.
	 *
	 * Side Effects:
	 * - API Call: Deactivate License
	 * - Update License Status
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 *
	 * @throws \Exception If there is an error.
	 */
	public function deactivate_license(): bool {
		$license_status = $this->api_call( 'deactivate_license' );

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

		$this->update_license_status( $license_status );

		$succeeded = isset( $license_status['license'] ) && 'deactivated' === $license_status['license'];

		/**
		 * Fires when license is activated.
		 *
		 * @param array $license_status License status data.
		 * @param bool  $succeeded      Whether the license was deactivated successfully.
		 */
		\do_action( 'popup_maker_license_deactivated', $license_status, $succeeded );

		return $succeeded;
	}

	/**
	 * Convert license error to human readable message.
	 *
	 * @param array{success:bool,license:'invalid'|'valid',item_id:int|false,item_name:string,license_limit:int,site_count:int,expires:string,activations_left:int,checksum:string,payment_id:int,customer_name:string,customer_email:string,price_id:string|int,error?:'no_activations_left'|'license_not_activable'|'missing'|'invalid'|'expired'|'revoked'|'item_name_mismatch'|'site_inactive'|'no_activations_left'|string|null,error_message?:string}|null $license_status License status data.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string
	 */
	public function get_license_error_message( ?array $license_status = null ): string {
		if ( empty( $license_status ) ) {
			$license_status = $this->get_license_status_data();
		}

		if ( empty( $license_status['error'] ) ) {
			return '';
		}

		switch ( $license_status['error'] ) {
			case 'expired':
				$message = sprintf(
				/* translators: the license key expiration date */
					__( 'Your license key expired on %s.', 'popup-maker' ),
					date_i18n( \get_option( 'date_format' ), strtotime( $license_status['expires'], time() ) )
				);
				break;

			case 'disabled':
			case 'revoked':
				$message = __( 'Your license key has been disabled.', 'popup-maker' );
				break;

			case 'missing':
				$message = __( 'Invalid license.', 'popup-maker' );
				break;

			case 'invalid':
			case 'site_inactive':
				$message = __( 'Your license is not active for this URL.', 'popup-maker' );
				break;

			case 'item_name_mismatch':
				$message = sprintf(
				/* translators: the plugin name */
					__( 'This appears to be an invalid license key for %s.', 'popup-maker' ),
					__( 'Popup Maker', 'popup-maker' )
				);
				break;

			case 'no_activations_left':
				$message = __( 'Your license key has reached its activation limit.', 'popup-maker' );
				break;

			default:
				$message = __( 'An error occurred, please try again.', 'popup-maker' );
				break;
		}

		return $message;
	}

	/**
	 * Remove license.
	 *
	 * Caution: Destructive, this will remove the license key and status.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function remove_license(): bool {
		try {
			// Might be an invalid but used key, so we want to deactivate it. without checking if active.
			$this->deactivate_license();

			if ( \delete_option( self::OPTION_KEY ) ) {
				// Delete the old license key option & status as well.
				\PUM_Utils_Options::delete( self::SETTINGS_KEY );
				delete_option( 'popup_maker_pro_license_active' );

				/**
				 * Fires when license is removed.
				 */
				\do_action( 'popup_maker_license_removed' );

				return true;
			}
		} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Do nothing.
		}

		return false;
	}

	/**
	 * Safely update license key.
	 *
	 * Side effect:
	 * - This will remove the license if the key is empty.
	 * - This will update the license key if its different from the current key.
	 * - Auto-activated licenses cannot be modified via this method.
	 *
	 * @param string $key License key.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function maybe_update_license_key( string $key ): bool {
		// Prevent modification of auto-activated licenses
		$license_data = $this->get_license_data();
		if ( ! empty( $license_data['auto_activation']['enabled'] ) ) {
			return false;
		}

		if ( empty( $key ) ) {
			$this->remove_license();
			return true;
		} else {
			// If the key is starred, get the original key.
			if ( strpos( $key, '*' ) !== false ) {
				$key = $this->get_license_key();
			}

			return $this->update_license_key( $key );
		}
	}

	/**
	 * Safely attempt to activate the license.
	 *
	 * Proper external handlers with error control.
	 *
	 * @param string $key License key.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function maybe_activate_license( ?string $key = null ): bool {
		try {
			// Update the license key separately to avoid race condition.
			if ( $key ) {
				$this->maybe_update_license_key( $key );
			}

			// Activate the license.
			return $this->activate_license();
		} catch ( \Exception $e ) {
			return false;
		}
	}

	/**
	 * Check if license is active.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function is_license_active() {
		return 'valid' === $this->get_license_status();
	}

	/**
	 * Check if license is auto-activated via POPUP_MAKER_PRO_LICENSE constant.
	 *
	 * Uses database flag for reliable detection even when constant is removed.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return bool
	 */
	public function is_auto_activated(): bool {
		$license_data = $this->get_license_data();
		return ! empty( $license_data['auto_activation']['enabled'] );
	}

	/**
	 * Get auto-activation info for debugging.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return array{enabled:bool,constant_present:bool,db_key:string,constant_lost_at:string|null,status:string}|null
	 */
	public function get_auto_activation_info(): ?array {
		$license_data = $this->get_license_data();

		if ( ! isset( $license_data['auto_activation']['enabled'] ) ) {
			return null;
		}

		$constant_key     = defined( 'POPUP_MAKER_PRO_LICENSE' ) && ! empty( POPUP_MAKER_PRO_LICENSE ) ? POPUP_MAKER_PRO_LICENSE : null;
		$constant_present = null !== $constant_key;
		$constant_lost_at = $license_data['auto_activation']['constant_lost_at'] ?? null;

		// Determine status
		$status = 'unknown';
		if ( $constant_present ) {
			$status = $constant_lost_at ? 'restored' : 'active';
		} elseif ( $constant_lost_at ) {
			$status = 'lost';
		} else {
			$status = 'missing';
		}

		return [
			'enabled'          => true,
			'constant_present' => $constant_present,
			'db_key'           => $license_data['key'] ?? '',
			'constant_lost_at' => $constant_lost_at,
			'status'           => $status,
		];
	}

	/**
	 * Filter settings editor args.
	 *
	 * Add the license key value to the settings editor args, since its not actually stored in options array.
	 *
	 * @param mixed $args Settings editor args.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return mixed Settings editor args.
	 */
	public function filter_settings_editor_args( $args ) {
		if ( ! $this->should_run_license_lifecycle() ) {
			return $args;
		}

		$value = isset( $args['current_values'][ self::SETTINGS_KEY ] ) ? $args['current_values'][ self::SETTINGS_KEY ] : '';

		try {
			$license_service = \PopupMaker\plugin( 'license' );
			$license_status  = $license_service->get_license_status_data();

			// Get the comprehensive license status from service
			$actual_status = $license_service->get_license_status();

			// Get the actual license key (either from form input or service)
			// $license_key = ! empty( $value ) ? trim( $value ) : $license_service->get_license_key();
			$license_key = $license_service->get_license_key();

			$license_key = self::star_key( $license_key );

			// Use the mapping function to get proper status and classes
			$status_mapping = $this->map_license_status( $actual_status );

			// Get the license tier (pro or pro_plus).
			$license_tier = $license_service->get_license_tier();

			// Get Pro installation status for template conditionals.
			$is_pro_installed = $this->container->is_pro_installed();
			$is_pro_active    = $this->container->is_pro_active();
			$pro_version      = $this->container->get_pro_version();

			// Check for auto-activation status messages
			$messages = ! empty( $license_status['error_message'] ) ? [ $license_status['error_message'] ] : [];

			// Add auto-activation disconnect message if applicable
			$auto_info = $license_service->get_auto_activation_info();
			if ( ! empty( $auto_info['constant_lost_at'] ) ) {
				$lost_date  = $auto_info['constant_lost_at'];
				$messages[] = sprintf(
					/* translators: the date and time of the auto-activation disconnect */
					__( 'Auto-activation was disconnected on %s. The POPUP_MAKER_PRO_LICENSE constant was removed from your configuration.', 'popup-maker' ),
					date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $lost_date ) )
				);
			}

			// Pass anything we want to the template here.
			$args['current_values'][ self::SETTINGS_KEY ] = [
				'key'              => $license_key,
				'status'           => $status_mapping['status'],
				'messages'         => $messages,
				'expires'          => $license_service->get_license_expiration(),
				'classes'          => $status_mapping['classes'],
				'license_tier'     => $license_tier,
				'is_pro_installed' => $is_pro_installed,
				'is_pro_active'    => $is_pro_active,
				'pro_version'      => $pro_version,
			];
		} catch ( \Exception $e ) {
			$args['current_values'][ self::SETTINGS_KEY ] = [
				'key'              => $this->star_key( trim( $value ) ),
				'status'           => 'invalid',
				/* translators: %s is the error message */
				'messages'         => [ sprintf( __( 'Error loading license status: %s', 'popup-maker' ), $e->getMessage() ) ],
				'expires'          => '',
				'classes'          => 'pum-license-invalid',
				'license_tier'     => 'pro', // Default to pro on error.
				'is_pro_installed' => $this->container->is_pro_installed(),
				'is_pro_active'    => $this->container->is_pro_active(),
				'pro_version'      => $this->container->get_pro_version(),
			];
		}

		return $args;
	}

	/**
	 * Map license status to template data.
	 *
	 * @param string $status License status from service.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return array Template data for license status.
	 */
	public function map_license_status( string $status ): array {
		switch ( $status ) {
			case 'valid':
				return [
					'status'  => 'valid',
					'classes' => 'pum-license-valid',
				];
			case 'deactivated':
				return [
					'status'  => 'deactivated',
					'classes' => 'pum-license-deactivated',
				];
			case 'expired':
				return [
					'status'  => 'expired',
					'classes' => 'pum-license-expired',
				];
			case 'empty':
				return [
					'status'  => 'empty',
					'classes' => 'pum-license-empty',
				];
			case 'error':
			default:
				return [
					'status'  => 'invalid',
					'classes' => 'pum-license-invalid',
				];
		}
	}

	/**
	 * Check whether the compatibility license lifecycle is needed.
	 *
	 * @return bool
	 */
	private function should_run_license_lifecycle() {
		return $this->container->should_run_legacy_license_compatibility();
	}

	/**
	 * Star the key.
	 *
	 * @param string $key The key to star.
	 *
	 * @deprecated 1.23.0 Temporary Pro 1.1.0 compatibility. Scheduled for removal in Core 1.25.0.
	 *
	 * @return string The starred key.
	 */
	public static function star_key( string $key ): string {
		if ( empty( $key ) ) {
			return '';
		}

		return substr( $key, 0, 3 ) . str_repeat( '*', max( 0, strlen( $key ) - 6 ) ) . substr( $key, -3 );
	}
}
