<?php
/**
 * File: Licensing_Plugin_Admin.php
 *
 * @package W3TC
 */

namespace W3TC;

/**
 * Class Licensing_Plugin_Admin
 *
 * phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
 * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
 * phpcs:disable Generic.CodeAnalysis.EmptyStatement
 */
class Licensing_Plugin_Admin {
	/**
	 * Config
	 *
	 * @var Config
	 */
	private $_config = null;

	/**
	 * Constructor for the Licensing Plugin Admin class.
	 *
	 * Initializes the configuration.
	 *
	 * @return void
	 */
	public function __construct() {
		$this->_config = Dispatcher::config();
	}

	/**
	 * Usermeta key for storing dismissed license notices.
	 *
	 * @since 2.9.4
	 *
	 * @var string
	 */
	const NOTICE_DISMISSED_META_KEY = 'w3tc_license_notice_dismissed';

	/**
	 * Time in seconds after which a dismissed notice can reappear if conditions persist.
	 * Set to 6 days (automatic license check interval is 5 days).
	 *
	 * @since 2.9.4
	 *
	 * @var int
	 */
	const NOTICE_DISMISSAL_RESET_TIME = 518400;

	/**
	 * Cache duration for the billing URL transient (19 minutes).
	 *
	 * The HLT (Hosted Login Token) in the billing URL is only valid for 19 minutes,
	 * so we cache for 19 minutes to ensure users always get a valid token with some buffer.
	 *
	 * @since 2.9.4
	 *
	 * @var int
	 */
	const BILLING_URL_CACHE_DURATION = 1140;

	/**
	 * Registers hooks for the plugin's admin functionality.
	 *
	 * Adds actions and filters for admin initialization, AJAX, UI updates, and admin bar menu.
	 *
	 * @return void
	 */
	public function run() {
		add_action( 'admin_init', array( $this, 'admin_init' ) );
		add_action( 'w3tc_config_ui_save-w3tc_general', array( $this, 'possible_state_change' ), 2, 10 );

		add_action( 'w3tc_message_action_licensing_upgrade', array( $this, 'w3tc_message_action_licensing_upgrade' ) );

		add_filter( 'w3tc_admin_bar_menu', array( $this, 'w3tc_admin_bar_menu' ) );

		// AJAX handler for dismissing license notices.
		add_action( 'wp_ajax_w3tc_dismiss_license_notice', array( $this, 'ajax_dismiss_license_notice' ) );

		// AJAX handler for rechecking license status.
		add_action( 'wp_ajax_w3tc_recheck_license', array( $this, 'ajax_recheck_license' ) );

		// Register scripts for license notice dismissal.
		add_action( 'admin_enqueue_scripts', array( $this, 'register_notice_scripts' ) );
	}

	/**
	 * Adds licensing menu items to the admin bar.
	 *
	 * @param array $menu_items Existing admin bar menu items.
	 *
	 * @return array Modified admin bar menu items.
	 */
	public function w3tc_admin_bar_menu( $menu_items ) {
		if ( ! Util_Environment::is_w3tc_pro( $this->_config ) ) {
			$menu_items['00020.licensing'] = array(
				'id'     => 'w3tc_overlay_upgrade',
				'parent' => 'w3tc',
				'title'  => wp_kses(
					sprintf(
						// translators: 1 opening HTML span tag, 2 closing HTML span tag.
						__(
							'%1$sUpgrade Performance%2$s',
							'w3-total-cache'
						),
						'<span style="color: red; background: none;">',
						'</span>'
					),
					array(
						'span' => array(
							'style' => array(),
						),
					)
				),
				'href'   => wp_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&amp;w3tc_message_action=licensing_upgrade' ), 'w3tc' ),
			);
		}

		if ( defined( 'W3TC_DEBUG' ) && W3TC_DEBUG ) {
			$menu_items['90040.licensing'] = array(
				'id'     => 'w3tc_debug_overlay_upgrade',
				'parent' => 'w3tc_debug_overlays',
				'title'  => esc_html__( 'Upgrade', 'w3-total-cache' ),
				'href'   => wp_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&amp;w3tc_message_action=licensing_upgrade' ), 'w3tc' ),
			);
		}

		return $menu_items;
	}

	/**
	 * Handles the licensing upgrade action.
	 *
	 * Adds a hook to modify the admin head for licensing upgrades.
	 *
	 * @return void
	 */
	public function w3tc_message_action_licensing_upgrade() {
		add_action( 'admin_head', array( $this, 'admin_head_licensing_upgrade' ) );
	}

	/**
	 * Outputs JavaScript for the licensing upgrade page.
	 *
	 * @return void
	 */
	public function admin_head_licensing_upgrade() {
		?>
		<script type="text/javascript">
			jQuery(function() {
				w3tc_lightbox_upgrade(w3tc_nonce, 'topbar_performance');
				jQuery('#w3tc-license-instruction').show();
			});
		</script>
		<?php
	}

	/**
	 * Handles possible state changes for plugin licensing.
	 *
	 * @param object $config     Current configuration object.
	 * @param object $old_config Previous configuration object.
	 *
	 * @return void
	 */
	public function possible_state_change( $config, $old_config ) {
		$changed     = false;
		$new_key     = $config->get_string( 'plugin.license_key' );
		$new_key_set = ! empty( $new_key );
		$old_key     = $old_config->get_string( 'plugin.license_key' );
		$old_key_set = ! empty( $old_key );

		// Clear billing URL transient if license key is changing.
		if ( $old_key_set && $old_key !== $new_key ) {
			$old_transient_key = 'w3tc_billing_url_' . md5( $old_key );
			delete_transient( $old_transient_key );
		}

		switch ( true ) {
			// No new key or old key. Do nothing.
			case ( ! $new_key_set && ! $old_key_set ):
				return;

			// Current key set but new is blank, deactivating old.
			case ( ! $new_key_set && $old_key_set ):
				$deactivate_result = Licensing_Core::deactivate_license( $old_key );
				$changed           = true;
				break;

			// Current key is blank but new is not, activating new.
			case ( $new_key_set && ! $old_key_set ):
				$activate_result = Licensing_Core::activate_license( $new_key, W3TC_VERSION );
				$changed         = true;
				if ( $activate_result ) {
					$config->set( 'common.track_usage', true );
				}
				break;

			// Current key is set and new different key provided. Deactivating old and activating new.
			case ( $new_key_set && $old_key_set && $new_key !== $old_key ):
				$deactivate_result = Licensing_Core::deactivate_license( $old_key );
				$activate_result   = Licensing_Core::activate_license( $new_key, W3TC_VERSION );
				$changed           = true;
				break;
		}

		if ( $changed ) {
			$state = Dispatcher::config_state();
			$state->set( 'license.next_check', 0 );

			// If license key was removed, immediately clear the status.
			if ( ! $new_key_set && $old_key_set ) {
				$state->set( 'license.status', 'no_key' );
				$state->set( 'license.paypal_billing_update_required', false );
				// Clear dismissed notices for billing update since license is removed.
				$this->clear_dismissed_notice_for_all_users( 'paypal-billing-update-required' );
				try {
					$config->set( 'plugin.type', '' );
					$config->save();
				} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
					// missing exception handle?
				}
			}

			// Clear billing URL transient for the new key to force fresh fetch.
			if ( $new_key_set ) {
				$new_transient_key = 'w3tc_billing_url_' . md5( $new_key );
				delete_transient( $new_transient_key );
			}

			// Process activation result to update paypal_billing_update_required immediately.
			if ( isset( $activate_result ) && $activate_result ) {
				// Update license status from activation result.
				if ( isset( $activate_result->license_status ) ) {
					$state->set( 'license.status', $activate_result->license_status );
				}

				// Update paypal_billing_update_required if present in activation result.
				if ( isset( $activate_result->paypal_billing_update_required ) ) {
					$paypal_billing_update_required = filter_var(
						$activate_result->paypal_billing_update_required,
						FILTER_VALIDATE_BOOLEAN
					);
					$state->set( 'license.paypal_billing_update_required', $paypal_billing_update_required );
				}
			}

			$state->save();

			delete_transient( 'w3tc_imageservice_limited' );

			$messages = array();

			// If the old key was deactivated, add a message.
			if ( isset( $deactivate_result ) && ! empty( $deactivate_result->license_status ) ) {
				$status = $deactivate_result->license_status;

				switch ( true ) {
					case ( strpos( $status, 'inactive.expired.' ) === 0 ):
						$messages[] = array(
							'message' => sprintf(
								// Translators: 1 Product name.
								__(
									'Your previous %1$s Pro license key is expired and will remain registered to this domain.',
									'w3-total-cache'
								),
								W3TC_POWERED_BY
							),
							'type'    => 'error',
						);
						break;

					case ( strpos( $status, 'inactive.not_present' ) === 0 ):
						$messages[] = array(
							'message' => sprintf(
								// Translators: 1 Product name.
								__(
									'Your previous %1$s Pro license key was not found and cannot be deactivated.',
									'w3-total-cache'
								),
								W3TC_POWERED_BY
							),
							'type'    => 'info',
						);
						break;

					case ( strpos( $status, 'inactive' ) === 0 ):
						$messages[] = array(
							'message' => sprintf(
								// Translators: 1 Product name.
								__(
									'Your previous %1$s Pro license key has been deactivated.',
									'w3-total-cache'
								),
								W3TC_POWERED_BY
							),
							'type'    => 'info',
						);
						break;

					case ( strpos( $status, 'invalid' ) === 0 ):
						$messages[] = array(
							'message' => sprintf(
								// translators: 1 Product name, 2 HTML anchor open tag, 3 HTML anchor close tag.
								__(
									'Your previous %1$s Pro license key is invalid and cannot be deactivated. Please %2$scontact support%3$s for assistance.',
									'w3-total-cache'
								),
								W3TC_POWERED_BY,
								'<a href="' . esc_url( Util_Ui::admin_url( 'admin.php?page=w3tc_support' ) ) . '">',
								'</a>'
							),
							'type'    => 'error',
						);
						break;
				}
			}

			// Handle new activation status.
			if ( isset( $activate_result ) ) {
				$status = $activate_result->license_status;

				switch ( true ) {
					case ( strpos( $status, 'active' ) === 0 ):
						$messages[] = array(
							'message' => sprintf(
								// Translators: 1 Product name.
								__(
									'The %1$s Pro license key you provided is valid and has been applied.',
									'w3-total-cache'
								),
								W3TC_POWERED_BY
							),
							'type'    => 'info',
						);
						break;
				}
			}

			// Store messages for processing.
			update_option( 'license_update_messages', $messages );

			// Only force license check if we didn't just activate a license.
			// If we activated, we already have the status from the activation result.
			if ( $new_key_set && ( ! isset( $activate_result ) || ! $activate_result ) ) {
				// Force immediate license check to get latest status including billing requirements.
				$this->maybe_update_license_status();
			}
		}
	}

	/**
	 * Initializes admin-specific features and hooks.
	 *
	 * Adds admin notices, UI filters, and license status checks.
	 *
	 * @return void
	 */
	public function admin_init() {
		$capability = apply_filters( 'w3tc_capability_admin_notices', 'manage_options' );

		$this->maybe_update_license_status();

		if ( current_user_can( $capability ) ) {
			if ( is_admin() ) {
				/**
				 * Only admin can see W3TC notices and errors
				 */
				if ( ! Util_Environment::is_wpmu() ) {
					add_action( 'admin_notices', array( $this, 'admin_notices' ), 1, 1 );
				}
				add_action( 'network_admin_notices', array( $this, 'admin_notices' ), 1, 1 );

				if ( Util_Admin::is_w3tc_admin_page() ) {
					add_filter( 'w3tc_notes', array( $this, 'w3tc_notes' ) );
				} else {
					// Enqueue lightbox assets on non-W3TC pages if license notices may be shown.
					$this->maybe_enqueue_lightbox_assets();
				}
			}
		}
	}

	/**
	 * Enqueues lightbox assets if license-related notices may need them.
	 *
	 * This is called on non-W3TC admin pages where the lightbox isn't already loaded.
	 * Only enqueues if there's a license status that would display a notice.
	 *
	 * @since 2.9.4
	 *
	 * @return void
	 */
	private function maybe_enqueue_lightbox_assets() {
		$state          = Dispatcher::config_state();
		$license_status = $state->get_string( 'license.status' );
		$license_key    = $this->get_license_key();

		// Check if we have a license notice to display.
		$license_message = $this->get_license_notice( $license_status, $license_key );

		if ( $license_message ) {
			$sanitized_status = $this->sanitize_status_for_id( $license_status );

			// Only enqueue if the notice isn't dismissed.
			if ( ! $this->is_notice_dismissed( 'license-status-' . $sanitized_status ) ) {
				add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_lightbox_assets' ) );
			}
		}
	}

	/**
	 * Enqueues lightbox JavaScript and CSS assets.
	 *
	 * @since 2.9.4
	 *
	 * @return void
	 */
	public function enqueue_lightbox_assets() {
		wp_enqueue_script(
			'w3tc-lightbox',
			plugins_url( 'pub/js/lightbox.js', W3TC_FILE ),
			array( 'jquery' ),
			W3TC_VERSION,
			false
		);

		wp_enqueue_style(
			'w3tc-lightbox',
			plugins_url( 'pub/css/lightbox.css', W3TC_FILE ),
			array(),
			W3TC_VERSION
		);
	}

	/**
	 * Checks if a status starts with a specific prefix.
	 *
	 * @param string $s           The status string.
	 * @param string $starts_with The prefix to check against.
	 *
	 * @return bool True if the status starts with the prefix, false otherwise.
	 */
	private function _status_is( $s, $starts_with ) {
		$s           .= '.';
		$starts_with .= '.';
		return substr( $s, 0, strlen( $starts_with ) ) === $starts_with;
	}

	/**
	 * Sanitizes a license status string for use in HTML IDs.
	 *
	 * Ensures the status only contains valid characters for HTML IDs
	 * (lowercase letters, numbers, hyphens, underscores) and replaces
	 * dots with hyphens for readability.
	 *
	 * @since 2.9.4
	 *
	 * @param string $status The license status to sanitize.
	 *
	 * @return string The sanitized status safe for use in HTML IDs.
	 */
	private function sanitize_status_for_id( $status ) {
		// Replace dots with hyphens for readability (e.g., "active.dunning" -> "active-dunning").
		$status = str_replace( '.', '-', $status );

		// Use WordPress sanitize_html_class which only allows a-z, A-Z, 0-9, _, -.
		return sanitize_html_class( $status, 'unknown' );
	}

	/**
	 * Displays admin notices related to licensing.
	 *
	 * @return void
	 */
	public function admin_notices() {
		$state           = Dispatcher::config_state();
		$license_status  = $state->get_string( 'license.status' );
		$license_key     = $this->get_license_key();
		$has_notices     = false;

		$paypal_billing_update_required = $state->get_boolean( 'license.paypal_billing_update_required' );
		$is_dismissed                   = $this->is_notice_dismissed( 'paypal-billing-update-required' );

		// Check for PayPal billing update requirement (shows on all admin pages).
		// Only show if there's a license key and the status is not 'no_key'.
		if ( $paypal_billing_update_required && ! $is_dismissed && ! empty( $license_key ) && 'no_key' !== $license_status ) {
			$billing_url = $this->get_billing_url( $license_key );
			if ( $billing_url ) {
				$billing_message = sprintf(
					// Translators: 1 Product name, 2 opening HTML a tag to billing portal, 3 closing HTML a tag, 4 opening HTML a tag for recheck, 5 closing HTML a tag.
					__(
						'Your %1$s Pro subscription requires a PayPal billing agreement update. Please %2$sUpdate your billing agreement%3$s to ensure uninterrupted service. Already updated? %4$sRecheck your license status%5$s or dismiss this notice.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a class="button" href="' . esc_url( $billing_url ) . '" target="_blank" rel="noopener noreferrer">',
					'</a>',
					'<a class="button w3tc-recheck-license" href="#">',
					'</a>'
				);
			} else {
				$billing_message = sprintf(
					// Translators: 1 Product name, 2 opening HTML a tag for recheck, 3 closing HTML a tag.
					__(
						'Your %1$s Pro subscription requires a PayPal billing agreement update. Please contact support to ensure uninterrupted service. Already updated? %2$sRecheck your license status%3$s or dismiss this notice.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a class="button w3tc-recheck-license" href="#">',
					'</a>'
				);
			}

			Util_Ui::error_box( '<p>' . $billing_message . '</p>', 'w3tc-paypal-billing-update-required', true );
			$has_notices = true;
		}

		$license_message = $this->get_license_notice( $license_status, $license_key );

		if ( $license_message ) {
			// Sanitize status for use in HTML ID (only allows a-z, 0-9, -, _).
			$sanitized_status = $this->sanitize_status_for_id( $license_status );

			if ( ! $this->is_notice_dismissed( 'license-status-' . $sanitized_status ) ) {
				Util_Ui::error_box( '<p>' . $license_message . '</p>', 'w3tc-license-status-' . $sanitized_status, true );
				$has_notices = true;
			}
		}

		$license_update_messages = get_option( 'license_update_messages' );

		if ( $license_update_messages ) {
			foreach ( $license_update_messages as $message_data ) {
				if ( 'error' === $message_data['type'] ) {
					Util_Ui::error_box( '<p>' . $message_data['message'] . '</p>', 'w3tc-license-update-message', true );
				} elseif ( 'info' === $message_data['type'] ) {
					Util_Ui::e_notification_box( '<p>' . $message_data['message'] . '</p>', 'w3tc-license-update-message', true );
				}
			}
			delete_option( 'license_update_messages' );
			$has_notices = true;
		}

		// Enqueue dismissal script if there are notices.
		if ( $has_notices ) {
			wp_enqueue_script( 'w3tc-license-notice-dismiss' );
		}
	}

	/**
	 * Registers the license notice dismissal script.
	 *
	 * Called on admin_enqueue_scripts to register the script early,
	 * which can then be enqueued later when notices are displayed.
	 *
	 * @since 2.9.4
	 *
	 * @return void
	 */
	public function register_notice_scripts() {
		wp_register_script(
			'w3tc-license-notice-dismiss',
			false, // No external file, inline script only.
			array( 'jquery' ),
			W3TC_VERSION,
			true // Load in footer.
		);

		// Add the inline script.
		$nonce           = wp_create_nonce( 'w3tc' );
		$rechecking_text = __( 'Rechecking...', 'w3-total-cache' );
		$script          = "
			jQuery(function($) {
				// Use event delegation with a namespace to prevent duplicate handlers.
				$(document).off('click.w3tcLicenseNotice').on('click.w3tcLicenseNotice', '.notice.is-dismissible[id^=\"w3tc-\"] .notice-dismiss', function() {
					var \$notice = $(this).closest('.notice');
					var noticeId = \$notice.attr('id');

					if (noticeId && noticeId.indexOf('w3tc-') === 0) {
						// Remove the 'w3tc-' prefix for storage.
						var cleanId = noticeId.replace('w3tc-', '');

						$.post(ajaxurl, {
							action: 'w3tc_dismiss_license_notice',
							notice_id: cleanId,
							_wpnonce: '" . esc_js( $nonce ) . "'
						});
					}
				});

				// Handle recheck license link.
				$(document).off('click.w3tcRecheckLicense').on('click.w3tcRecheckLicense', '.w3tc-recheck-license', function(e) {
					e.preventDefault();
					var \$link = $(this);
					\$link.text('" . esc_js( $rechecking_text ) . "').css('pointer-events', 'none');

					$.post(ajaxurl, {
						action: 'w3tc_recheck_license',
						_wpnonce: '" . esc_js( $nonce ) . "'
					}).always(function() {
						location.reload();
					});
				});
			});
		";

		wp_add_inline_script( 'w3tc-license-notice-dismiss', $script );
	}

	/**
	 * Generates a license notice based on the provided license status and key.
	 *
	 * @since 2.9.4
	 *
	 * @param string $status The current status of the license (e.g., 'active', 'expired').
	 * @param string $license_key The license key associated with the plugin.
	 *
	 * @return string The formatted license notice message.
	 */
	private function get_license_notice( $status, $license_key ) {
		switch ( true ) {
			case $this->_status_is( $status, 'active.dunning' ):
				$billing_url = $this->get_billing_url( $license_key );
				if ( $billing_url ) {
					return sprintf(
						// Translators: 1 Product name, 2 opening HTML a tag to billing portal, 3 closing HTML a tag, 4 opening HTML a tag for recheck, 5 closing HTML a tag.
						__(
							'Your %1$s Pro subscription payment is past due. Please update your %2$sBilling Information%3$s to prevent service interruption. Already updated? %4$sRecheck your license status%5$s.',
							'w3-total-cache'
						),
						W3TC_POWERED_BY,
						'<a class="button" href="' . esc_url( $billing_url ) . '" target="_blank" rel="noopener noreferrer">',
						'</a>',
						'<a class="button w3tc-recheck-license" href="#">',
						'</a>'
					);
				}

				return sprintf(
					// Translators: 1 Product name, 2 opening HTML a tag for recheck, 3 closing HTML a tag.
					__(
						'Your %1$s Pro subscription payment is past due. Please update your billing information or contact us to prevent service interruption. Already updated? %2$sRecheck your license status%3$s.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a class="button w3tc-recheck-license" href="#">',
					'</a>'
				);

			case $this->_status_is( $status, 'inactive.expired' ):
				return sprintf(
					// Translators: 1 Product name, 2 HTML input to renew subscription.
					__(
						'Your %1$s Pro license key has expired. %2$s to continue using the Pro features',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<input type="button" class="button button-renew-plugin" data-nonce="' .
						wp_create_nonce( 'w3tc' ) . '" data-renew-key="' . esc_attr( $license_key ) .
						'" data-src="licensing_expired" value="' . esc_attr__( 'Renew Now', 'w3-total-cache' ) . '" />'
				);

			case $this->_status_is( $status, 'inactive.by_rooturi' ) || $this->_status_is( $status, 'inactive.by_rooturi.activations_limit_not_reached' ):
				// Only show this notice if there's actually a license key.
				if ( empty( $license_key ) ) {
					return '';
				}
				$reset_url = Util_Ui::url(
					array(
						'page'                         => 'w3tc_general',
						'w3tc_licensing_reset_rooturi' => 'y',
					)
				);
				return sprintf(
					// Translators: 1 Product name, 2 opening HTML a tag to reset license URIs, 3 closing HTML a tag.
					__(
						'Your %1$s license key is not active for this site. You can switch your license to this website following %2$sthis link%3$s',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a class="w3tc_licensing_reset_rooturi" href="' . esc_url( $reset_url ) . '">',
					'</a>'
				);

			case $this->_status_is( $status, 'inactive.by_rooturi.activations_limit_reached' ):
				return sprintf(
					// Translators: 1 Product name.
					__(
						'Your %1$s license key is not active and cannot be activated due to the license activation limit being reached.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY
				);

			case $this->_status_is( $status, 'inactive' ):
				return sprintf(
					// Translators: 1 Product name.
					__(
						'The %1$s license key is not active.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY
				);

			case $this->_status_is( $status, 'invalid' ):
				$url = is_network_admin()
					? network_admin_url( 'admin.php?page=w3tc_general#licensing' )
					: admin_url( 'admin.php?page=w3tc_general#licensing' );
				return sprintf(
					// Translators: 1 Product name, 2 opening HTML a tag to license setting, 3 closing HTML a tag.
					__(
						'Your current %1$s Pro license key is not valid. %2$sPlease confirm it%3$s.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a href="' . esc_url( $url ) . '">',
					'</a>'
				);

			case ( 'no_key' === $status || $this->_status_is( $status, 'active' ) || $this->_status_is( $status, 'free' ) ):
				return '';

			default:
				return sprintf(
					// translators: 1: Product name, 2: HTML anchor open tag, 3: HTML anchor close tag.
					__(
						'The %1$s license key cannot be verified. Please %2$scontact support%3$s for assistance.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<a href="' . esc_url( Util_Ui::admin_url( 'admin.php?page=w3tc_support' ) ) . '">',
					'</a>'
				);
		}
	}

	/**
	 * Generates the billing URL for a given license key.
	 *
	 * Uses transient caching to avoid making HTTP requests on every page load.
	 * The URL is cached for 1 hour to balance freshness with performance.
	 *
	 * @since 2.9.4
	 *
	 * @param string $license_key The license key used to generate the billing URL.
	 *
	 * @return string The billing URL associated with the provided license key, or
	 *                an empty string if the request fails or the response is invalid.
	 */
	private function get_billing_url( $license_key ) {
		if ( empty( $license_key ) ) {
			return '';
		}

		// Generate a unique transient key based on the license key.
		$transient_key = 'w3tc_billing_url_' . md5( $license_key );

		// Check for cached URL.
		$cached_url = get_transient( $transient_key );
		if ( false !== $cached_url ) {
			// Return cached value (empty string is a valid cached "no URL" result).
			return $cached_url;
		}

		$billing_url = $this->fetch_billing_url_from_api( $license_key );

		// Cache the result (including empty string for failed requests to avoid repeated failures).
		set_transient( $transient_key, $billing_url, self::BILLING_URL_CACHE_DURATION );

		return $billing_url;
	}

	/**
	 * Fetches the billing URL from the API.
	 *
	 * @since 2.9.4
	 *
	 * @param string $license_key The license key used to generate the billing URL.
	 *
	 * @return string The billing URL or empty string on failure.
	 */
	private function fetch_billing_url_from_api( $license_key ) {
		$api_params = array(
			'edd_action'  => 'get_recurly_hlt_link',
			'license'     => $license_key,
			'license_key' => $license_key,
		);

		$response = wp_remote_get(
			add_query_arg( $api_params, W3TC_LICENSE_API_URL ),
			array(
				'timeout'   => 15,
				'sslverify' => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			return '';
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $response_code ) {
			return '';
		}

		$body = wp_remote_retrieve_body( $response );

		return filter_var( $body, FILTER_VALIDATE_URL ) ? esc_url_raw( $body ) : '';
	}

	/**
	 * Modifies the notes displayed in the W3TC UI.
	 *
	 * @param array $notes Existing notes to display.
	 *
	 * @return array Modified notes with licensing terms.
	 */
	public function w3tc_notes( $notes ) {
		$terms        = '';
		$state_master = Dispatcher::config_state_master();

		if ( Util_Environment::is_pro_constant( $this->_config ) ) {
			$terms = 'accept';
		} elseif ( ! Util_Environment::is_w3tc_pro( $this->_config ) ) {
			$terms = $state_master->get_string( 'license.community_terms' );

			$buttons = sprintf(
				'<br /><br />%s&nbsp;%s',
				Util_Ui::button_link(
					__( 'Accept', 'w3-total-cache' ),
					Util_Ui::url( array( 'w3tc_licensing_terms_accept' => 'y' ) )
				),
				Util_Ui::button_link(
					__( 'Decline', 'w3-total-cache' ),
					Util_Ui::url( array( 'w3tc_licensing_terms_decline' => 'y' ) )
				)
			);
		} else {
			$state = Dispatcher::config_state();
			$terms = $state->get_string( 'license.terms' );

			$return_url = self_admin_url( Util_Ui::url( array( 'w3tc_licensing_terms_refresh' => 'y' ) ) );

			$buttons =
				sprintf( '<form method="post" action="%s">', W3TC_TERMS_ACCEPT_URL ) .
				Util_Ui::r_hidden( 'return_url', 'return_url', $return_url ) .
				Util_Ui::r_hidden( 'license_key', 'license_key', $this->get_license_key() ) .
				Util_Ui::r_hidden( 'home_url', 'home_url', home_url() ) .
				'<input type="submit" class="button" name="answer" value="Accept" />&nbsp;' .
				'<input type="submit" class="button" name="answer" value="Decline" />' .
				'</form>';
		}

		if ( 'accept' !== $terms && 'decline' !== $terms && 'postpone' !== $terms ) {
			if ( $state_master->get_integer( 'common.install' ) < 1542029724 ) {
				/* installed before 2018-11-12 */
				$notes['licensing_terms'] = sprintf(
					// translators: 1 opening HTML a tag to W3TC Terms page, 2 closing HTML a tag.
					esc_html__(
						'Our terms of use and privacy policies have been updated. Please %1$sreview%2$s and accept them.',
						'w3-total-cache'
					),
					'<a target="_blank" href="' . esc_url( W3TC_TERMS_URL ) . '">',
					'</a>'
				) . $buttons;
			} else {
				$notes['licensing_terms'] = sprintf(
					// translators: 1: Product name, 2: HTML break tag, 3: Anchor/link open tag, 4: Anchor/link close tag.
					esc_html__(
						'By allowing us to collect data about how %1$s is used, we can improve our features and experience for everyone. This data will not include any personally identifiable information.%2$sFeel free to review our %3$sterms of use and privacy policy%4$s.',
						'w3-total-cache'
					),
					W3TC_POWERED_BY,
					'<br />',
					'<a target="_blank" href="' . esc_url( W3TC_TERMS_URL ) . '">',
					'</a>'
				) .
					$buttons;
			}
		}

		return $notes;
	}

	/**
	 * Updates the license status if needed.
	 *
	 * Performs a license check and updates the configuration state accordingly.
	 *
	 * @return string The updated license status.
	 */
	private function maybe_update_license_status() {
		$state = Dispatcher::config_state();
		if ( time() < $state->get_integer( 'license.next_check' ) ) {
			return;
		}

		$check_timeout                  = 3600 * 24 * 5;
		$status                         = '';
		$terms                          = '';
		$paypal_billing_update_required = false;
		$license_key                    = $this->get_license_key();

		$old_plugin_type                    = $this->_config->get_string( 'plugin.type' );
		$old_status                         = $state->get_string( 'license.status' );
		$old_paypal_billing_update_required = $state->get_boolean( 'license.paypal_billing_update_required' );
		$plugin_type                        = '';

		if ( ! empty( $license_key ) || defined( 'W3TC_LICENSE_CHECK' ) ) {
			$license = Licensing_Core::check_license( $license_key, W3TC_VERSION );

			if ( $license ) {
				$status = $license->license_status;
				$terms  = $license->license_terms;
				if ( $this->_status_is( $status, 'active' ) ) {
					$plugin_type = 'pro';
				} elseif ( $this->_status_is( $status, 'inactive.by_rooturi' ) &&
					Util_Environment::is_w3tc_pro_dev() ) {
					$status      = 'valid';
					$plugin_type = 'pro_dev';
				}

				// Check for PayPal billing update requirement.
				if ( isset( $license->paypal_billing_update_required ) ) {
					$paypal_billing_update_required = filter_var(
						$license->paypal_billing_update_required,
						FILTER_VALIDATE_BOOLEAN
					);
				}
			}

			$this->_config->set( 'plugin.type', $plugin_type );
		} else {
			$status = 'no_key';
		}

		if ( 'no_key' === $status ) {
			// Do nothing.
		} elseif ( $this->_status_is( $status, 'invalid' ) ) {
			// Do nothing.
		} elseif ( $this->_status_is( $status, 'inactive' ) ) {
			// Do nothing.
		} elseif ( $this->_status_is( $status, 'active' ) ) {
			// Do nothing.
		} else {
			$check_timeout = 60;
		}

		// Clear dismissed notices when conditions change.
		if ( $old_paypal_billing_update_required && ! $paypal_billing_update_required ) {
			// PayPal billing update is no longer required, clear the dismissal for all users.
			$this->clear_dismissed_notice_for_all_users( 'paypal-billing-update-required' );
		}

		if ( $old_status !== $status && ! empty( $old_status ) && ! empty( $status ) ) {
			// License status changed, clear the old status dismissal for all users.
			$this->clear_dismissed_notice_for_all_users( 'license-status-' . $this->sanitize_status_for_id( $old_status ) );
		}

		$state->set( 'license.status', $status );
		$state->set( 'license.next_check', time() + $check_timeout );
		$state->set( 'license.terms', $terms );
		$state->set( 'license.paypal_billing_update_required', $paypal_billing_update_required );
		$state->save();

		if ( $old_plugin_type !== $plugin_type ) {
			try {
				$this->_config->set( 'plugin.type', $plugin_type );
				$this->_config->save();
			} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				// missing exception handle?
			}
		}
		return $status;
	}

	/**
	 * Retrieves the license key for the plugin.
	 *
	 * @return string The license key.
	 */
	public function get_license_key() {
		$license_key = $this->_config->get_string( 'plugin.license_key', '' );
		if ( '' === $license_key ) {
			$license_key = ini_get( 'w3tc.license_key' );
		}
		return $license_key;
	}

	/**
	 * AJAX handler for dismissing license notices.
	 *
	 * Saves the dismissal timestamp in usermeta for persistent per-user dismissal.
	 *
	 * @since 2.9.4
	 *
	 * @return void
	 */
	public function ajax_dismiss_license_notice() {
		check_ajax_referer( 'w3tc', '_wpnonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}

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

		if ( empty( $notice_id ) ) {
			wp_send_json_error( array( 'message' => 'Invalid notice ID' ) );
		}

		$user_id            = get_current_user_id();
		$dismissed_notices  = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true );

		if ( ! is_array( $dismissed_notices ) ) {
			$dismissed_notices = array();
		}

		$dismissed_notices[ $notice_id ] = time();

		update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices );

		wp_send_json_success( array( 'message' => 'Notice dismissed' ) );
	}

	/**
	 * AJAX handler for rechecking license status.
	 *
	 * Forces an immediate license check and clears cached billing URL.
	 *
	 * @since 2.9.4
	 *
	 * @return void
	 */
	public function ajax_recheck_license() {
		check_ajax_referer( 'w3tc', '_wpnonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}

		// Force next check to happen immediately.
		$state = Dispatcher::config_state();
		$state->set( 'license.next_check', 0 );
		$state->save();

		// Clear cached billing URL so a fresh one is fetched.
		$license_key   = $this->get_license_key();
		$transient_key = 'w3tc_billing_url_' . md5( $license_key );
		delete_transient( $transient_key );

		// Perform the license check now.
		$this->maybe_update_license_status();

		wp_send_json_success( array( 'message' => 'License status rechecked' ) );
	}

	/**
	 * Checks if a specific license notice has been dismissed by the current user.
	 *
	 * Returns true if the notice was dismissed and the reset time has not elapsed.
	 * If the reset time has elapsed and the condition still persists, the dismissal
	 * is cleared and the notice will show again.
	 *
	 * @since 2.9.4
	 *
	 * @param string $notice_id The unique identifier for the notice.
	 *
	 * @return bool True if the notice is dismissed and should not be shown.
	 */
	private function is_notice_dismissed( $notice_id ) {
		$user_id           = get_current_user_id();
		$dismissed_notices = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true );

		if ( ! is_array( $dismissed_notices ) || ! isset( $dismissed_notices[ $notice_id ] ) ) {
			return false;
		}

		$dismissed_time = (int) $dismissed_notices[ $notice_id ];
		$time_elapsed   = time() - $dismissed_time;

		// If enough time has passed, clear the dismissal so the notice can show again.
		if ( $time_elapsed > self::NOTICE_DISMISSAL_RESET_TIME ) {
			unset( $dismissed_notices[ $notice_id ] );
			update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices );
			return false;
		}

		return true;
	}

	/**
	 * Clears a specific dismissed notice for all users.
	 *
	 * This should be called when the condition that triggered the notice is resolved.
	 * Uses a targeted query to only retrieve users who have the specific notice dismissed,
	 * rather than all users with any dismissed notices.
	 *
	 * @since 2.9.4
	 *
	 * @param string $notice_id The unique identifier for the notice to clear.
	 *
	 * @return void
	 */
	private function clear_dismissed_notice_for_all_users( $notice_id ) {
		global $wpdb;

		/*
		 * Two-step approach for performance:
		 * 1. Use LIKE query to narrow down candidate users (faster than loading all users).
		 *    Search for '"notice_id"' (with quotes) to match serialized array key format
		 *    and reduce false positives from partial matches.
		 * 2. Validate with isset() to confirm exact key match, handling any edge cases.
		 */
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value LIKE %s",
				self::NOTICE_DISMISSED_META_KEY,
				'%"' . $wpdb->esc_like( $notice_id ) . '"%'
			)
		);

		foreach ( $user_ids as $user_id ) {
			$dismissed_notices = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true );

			// Verify exact key match to handle any edge cases from LIKE query.
			if ( is_array( $dismissed_notices ) && isset( $dismissed_notices[ $notice_id ] ) ) {
				unset( $dismissed_notices[ $notice_id ] );

				if ( empty( $dismissed_notices ) ) {
					delete_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY );
				} else {
					update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices );
				}
			}
		}
	}
}
