<?php
/**
 * Elite Licenser plugin update checker for WP PowerSuite.
 *
 * Update fetch/inject logic adapted from the Elite Licenser WordPress sample
 * ({@see product/update/{product_id}/{license_key}/{version}}). Download access
 * is gated by {@see WPPS_License_Updates} when the license is not current.
 *
 * @package WP_PowerSuite
 * @since 1.1.0
 */

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

/**
 * @since 1.1.0
 */
final class WPPS_License_Updater {

	private const CACHE_TTL = DAY_IN_SECONDS;

	/** @var string|null Package URL held when download is blocked locally. */
	private static $stashed_package = null;

	/** @var array<string,mixed>|null|false Memoized pending update summary for this request. */
	private static $pending_update_memo = false;

	/**
	 * Plugin file commercial updates attach to (Pro add-on on Lite, else this plugin).
	 */
	private static function target_plugin_file(): string {
		if ( function_exists( 'wp_powersuite_licensed_plugin_file' ) ) {
			$file = wp_powersuite_licensed_plugin_file();
			if ( $file !== '' ) {
				return $file;
			}
		}

		return defined( 'WP_POWERSUITE_PLUGIN_FILE' ) ? WP_POWERSUITE_PLUGIN_FILE : '';
	}

	/**
	 * Lite on wordpress.org must never receive commercial update payloads.
	 */
	private static function targets_lite_core_plugin( string $plugin_file ): bool {
		if ( $plugin_file === '' || ! function_exists( 'wp_powersuite_is_lite_edition' ) || ! wp_powersuite_is_lite_edition() ) {
			return false;
		}

		if ( ! defined( 'WP_POWERSUITE_PLUGIN_FILE' ) ) {
			return false;
		}

		return wp_normalize_path( $plugin_file ) === wp_normalize_path( (string) WP_POWERSUITE_PLUGIN_FILE );
	}

	/**
	 * Register WordPress update hooks.
	 */
	public static function register(): void {
		$plugin_file = self::target_plugin_file();
		if ( $plugin_file === '' || self::targets_lite_core_plugin( $plugin_file ) ) {
			return;
		}

		add_filter( 'pre_set_site_transient_update_plugins', [ self::class, 'inject_plugin_update' ], 10, 1 );
		add_filter( 'plugins_api', [ self::class, 'plugins_api' ], 10, 3 );
		add_filter( 'upgrader_source_selection', [ self::class, 'guard_pro_package_root' ], 10, 4 );
		add_action( 'upgrader_process_complete', [ self::class, 'handle_upgrade_complete' ], 10, 2 );
		add_action(
			'in_plugin_update_message-' . plugin_basename( $plugin_file ),
			[ self::class, 'update_message' ],
			20,
			2
		);
	}

	/**
	 * Reject commercial packages that are not the Pro add-on (would overwrite Lite).
	 *
	 * @param string|\WP_Error $source        Unpacked source.
	 * @param string           $remote_source Remote source.
	 * @param \WP_Upgrader     $upgrader      Upgrader.
	 * @param array<string,mixed> $hook_extra Extra data.
	 * @return string|\WP_Error
	 */
	public static function guard_pro_package_root( $source, $remote_source, $upgrader, $hook_extra ) {
		unset( $remote_source, $upgrader );

		if ( is_wp_error( $source ) ) {
			return $source;
		}

		$target = self::target_plugin_file();
		if ( $target === '' || ! function_exists( 'wp_powersuite_is_lite_edition' ) || ! wp_powersuite_is_lite_edition() ) {
			return $source;
		}

		$plugin = isset( $hook_extra['plugin'] ) ? (string) $hook_extra['plugin'] : '';
		if ( $plugin !== plugin_basename( $target ) ) {
			return $source;
		}

		$source_dir = trailingslashit( (string) $source );
		if ( ! is_readable( $source_dir . 'wp-powersuite-pro.php' ) ) {
			return new \WP_Error(
				'wpps_pro_package_mismatch',
				__( 'This update package is not WP PowerSuite Pro. Refusing to install it over the Pro add-on.', 'wp-powersuite' )
			);
		}

		return $source;
	}

	/**
	 * Clear caches after upgrade and show a one-time success notice.
	 *
	 * @param \WP_Upgrader $upgrader Upgrader instance.
	 * @param array<string,mixed> $options Upgrade options.
	 */
	public static function handle_upgrade_complete( $upgrader, $options ): void {
		if ( is_array( $options ) && class_exists( 'WPPS_License_Admin', false ) ) {
			WPPS_License_Admin::note_successful_upgrade( $options );
		}

		self::clear_update_caches( $upgrader, $options );
	}

	/**
	 * Delete WordPress and Elite Licenser update caches.
	 */
	public static function clear_update_caches( $upgrader = null, $options = null ): void {
		unset( $upgrader, $options );

		delete_site_transient( 'update_plugins' );
		delete_transient( self::get_update_transient_key() );
		self::$stashed_package     = null;
		self::$pending_update_memo = false;
	}

	/**
	 * Whether the site may download and install plugin updates.
	 */
	public static function can_download_updates(): bool {
		return function_exists( 'wp_powersuite_is_license_current' ) && wp_powersuite_is_license_current();
	}

	/**
	 * Reason code when downloads are blocked: inactive, expired, or invalid.
	 */
	public static function get_download_block_reason(): string {
		if ( self::can_download_updates() ) {
			return '';
		}

		if ( ! class_exists( 'WPPS_License_Manager', false ) ) {
			return 'inactive';
		}

		$status = sanitize_key( (string) ( WPPS_License_Manager::get_instance()->get_status_data()['status'] ?? 'inactive' ) );

		if ( $status === 'expired' ) {
			return 'expired';
		}

		if ( $status === 'invalid' ) {
			return 'invalid';
		}

		return 'inactive';
	}

	/**
	 * Human-readable message when an update cannot be downloaded.
	 */
	public static function get_download_block_message( string $reason = '' ): string {
		if ( $reason === '' ) {
			$reason = self::get_download_block_reason();
		}
		$message = '';

		switch ( $reason ) {
			case 'expired':
				$message = __( 'You cannot install this update because your license has expired. Renew your license to download updates.', 'wp-powersuite' );
				break;
			case 'invalid':
				$message = __( 'You cannot install this update because your license is invalid. Activate a valid license to download updates.', 'wp-powersuite' );
				break;
			case 'inactive':
				$message = __( 'You cannot install this update because no active license is registered on this site. Activate your license to download updates.', 'wp-powersuite' );
				break;
			case 'untrusted_package':
				$message = __( 'You cannot install this update because the download URL is not from a trusted host.', 'wp-powersuite' );
				break;
		}

		if ( $message === '' ) {
			$message = __( 'You cannot install this update because your license is not active.', 'wp-powersuite' );
		}

		return (string) apply_filters( 'wp_powersuite_license_update_block_message', $message, $reason );
	}

	/**
	 * Short notice for admin banners when an update exists but download is blocked.
	 */
	public static function get_update_available_notice_message( ?array $pending = null ): string {
		if ( $pending === null ) {
			$pending = self::get_pending_update();
		}
		if ( ! is_array( $pending ) ) {
			return '';
		}

		$version_line = sprintf(
			/* translators: 1: installed version, 2: available version */
			__( 'WP PowerSuite %2$s is available (you have %1$s).', 'wp-powersuite' ),
			$pending['installed_version'],
			$pending['new_version']
		);

		if ( ! empty( $pending['can_download'] ) ) {
			return $version_line . ' ' . __( 'Visit Dashboard → Updates to install it.', 'wp-powersuite' );
		}

		return $version_line . ' ' . self::get_download_block_message();
	}

	/**
	 * Pending update summary for admin UI, or null when up to date / unknown.
	 *
	 * @return array<string,mixed>|null
	 */
	public static function get_pending_update(): ?array {
		if ( self::$pending_update_memo !== false ) {
			return is_array( self::$pending_update_memo ) ? self::$pending_update_memo : null;
		}

		$update = self::fetch_update_info();
		if ( ! is_object( $update ) || empty( $update->new_version ) ) {
			self::$pending_update_memo = null;
			return null;
		}

		$installed = W_P_Power_Suite_Premium_Base::get_installed_version_from_disk();
		if ( version_compare( $installed, (string) $update->new_version, '>=' ) ) {
			self::$pending_update_memo = null;
			return null;
		}

		$can_download = self::can_download_updates();
		$license_url  = self::get_license_admin_url();

		self::$pending_update_memo = [
			'installed_version' => $installed,
			'new_version'       => (string) $update->new_version,
			'can_download'      => $can_download,
			'block_reason'      => $can_download ? '' : self::get_download_block_reason(),
			'block_message'     => $can_download ? '' : self::get_download_block_message(),
			'license_url'       => $license_url,
			'updates_url'       => self::get_updates_admin_url(),
			'plugins_url'       => self::get_plugins_admin_url(),
		];

		return self::$pending_update_memo;
	}

	/**
	 * Package URL stashed when download is blocked (for upgrader matching).
	 */
	public static function get_stashed_package_url(): string {
		return is_string( self::$stashed_package ) ? self::$stashed_package : '';
	}

	/**
	 * Strip download package when license is not current but keep update visible.
	 *
	 * @param object $update Update metadata object.
	 */
	public static function apply_download_gate( object $update ): object {
		$package = ! empty( $update->package ) ? (string) $update->package : '';
		if ( $package === '' && ! empty( $update->download_link ) ) {
			$package = (string) $update->download_link;
		}

		if ( self::can_download_updates() ) {
			if ( $package !== '' && ! self::is_trusted_package_url( $package ) ) {
				self::$stashed_package        = $package;
				$update->package              = '';
				$update->download_link        = '';
				$update->wpps_update_blocked  = 'untrusted_package';
			}
			return $update;
		}

		if ( $package !== '' ) {
			self::$stashed_package = $package;
			$update->package       = '';
		}

		if ( ! empty( $update->download_link ) ) {
			if ( self::$stashed_package === null || self::$stashed_package === '' ) {
				self::$stashed_package = (string) $update->download_link;
			}
			$update->download_link = '';
		}

		$update->wpps_update_blocked = self::get_download_block_reason();

		return $update;
	}

	/**
	 * Whether a plugin package URL is from an allowed update host.
	 */
	public static function is_trusted_package_url( string $url ): bool {
		if ( $url === '' ) {
			return false;
		}

		$host = strtolower( trim( (string) wp_parse_url( $url, PHP_URL_HOST ) ) );
		if ( $host === '' ) {
			return false;
		}

		foreach ( self::get_trusted_update_hosts() as $allowed_host ) {
			$allowed_host = strtolower( trim( (string) $allowed_host ) );
			if ( $allowed_host === '' ) {
				continue;
			}

			if ( $host === $allowed_host || str_ends_with( $host, '.' . $allowed_host ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @return list<string>
	 */
	public static function get_trusted_update_hosts(): array {
		$server_host = wp_parse_url( W_P_Power_Suite_Premium_Base::get_server_host(), PHP_URL_HOST );
		$hosts       = array_filter(
			[
				is_string( $server_host ) ? $server_host : '',
				'wppowersuite.com',
			]
		);

		$hosts = apply_filters( 'wp_powersuite_trusted_update_hosts', array_values( array_unique( $hosts ) ) );

		return is_array( $hosts ) ? array_values( array_filter( array_map( 'strval', $hosts ) ) ) : [];
	}

	/**
	 * Inject update metadata before WordPress saves the plugin update transient.
	 *
	 * @param mixed $transient Update transient value.
	 * @return mixed
	 */
	public static function inject_plugin_update( $transient ) {
		if ( ! is_object( $transient ) ) {
			$transient = new stdClass();
		}

		if ( empty( $transient->response ) || ! is_array( $transient->response ) ) {
			$transient->response = [];
		}

		if ( self::targets_lite_core_plugin( self::target_plugin_file() ) ) {
			return $transient;
		}

		$update = self::fetch_update_info();
		if ( ! is_object( $update ) || empty( $update->plugin ) ) {
			return $transient;
		}

		// Compare against the on-disk version. WordPress rebuilds this transient
		// during the same request that ran the update (upgrader_process_complete),
		// when the in-memory version constant is still the pre-update value; using
		// it here would re-inject a phantom "update available" for the version we
		// just installed until a later request or license refresh flushed it.
		$current_version = W_P_Power_Suite_Premium_Base::get_installed_version_from_disk( self::target_plugin_file() );
		$plugin_index    = (string) $update->plugin;

		if ( version_compare( $current_version, (string) $update->new_version, '<' ) ) {
			unset( $update->is_stopped_update );
			$transient->response[ $plugin_index ] = self::apply_download_gate( $update );
		} elseif ( isset( $transient->response[ $plugin_index ] ) ) {
			unset( $transient->response[ $plugin_index ] );
		}

		return $transient;
	}

	/**
	 * Provide changelog/details for the update thickbox.
	 *
	 * @param false|object|array $result Result object or array.
	 * @param string             $action API action.
	 * @param object             $args   Request arguments.
	 * @return false|object|array
	 */
	public static function plugins_api( $result, string $action, $args ) {
		if ( $action !== 'plugin_information' || ! is_object( $args ) || empty( $args->slug ) ) {
			return $result;
		}

		$plugin_file = self::target_plugin_file();
		if ( $plugin_file === '' || self::targets_lite_core_plugin( $plugin_file ) ) {
			return $result;
		}

		if ( plugin_basename( $plugin_file ) !== (string) $args->slug ) {
			return $result;
		}

		$update = self::fetch_update_info();
		if ( is_object( $update ) && ! empty( $update->new_version ) ) {
			return self::apply_download_gate( $update );
		}

		return $result;
	}

	/**
	 * Show update denial hints under the plugin row on Plugins → Updates.
	 *
	 * @param array<string,mixed> $data Plugin update data.
	 * @param object              $response Update metadata.
	 */
	public static function update_message( array $data, $response ): void {
		unset( $data );

		if ( ! is_object( $response ) ) {
			return;
		}

		if ( self::can_download_updates() && ! empty( $response->package ) ) {
			return;
		}

		$reason = ! empty( $response->wpps_update_blocked )
			? sanitize_key( (string) $response->wpps_update_blocked )
			: self::get_download_block_reason();

		if ( $reason !== '' || empty( $response->package ) ) {
			echo '<br /><span class="wpps-license-update-inline-notice" style="display:block;border-top:1px solid #ccc;padding-top:5px;margin-top:10px;">';
			echo esc_html( self::get_download_block_message( $reason ) );
			if ( self::get_license_admin_url() !== '' ) {
				echo ' <a href="' . esc_url( self::get_license_admin_url() ) . '">';
				esc_html_e( 'Manage license', 'wp-powersuite' );
				echo '</a>';
			}
			echo '</span>';
			return;
		}

		if ( ! empty( $response->package ) ) {
			return;
		}

		$denied = isset( $response->update_denied_type ) ? (string) $response->update_denied_type : '';

		if ( $denied === 'L' ) {
			echo '<br /><span style="display:block;border-top:1px solid #ccc;padding-top:5px;margin-top:10px;">';
			esc_html_e( 'Please activate your license to download the latest version.', 'wp-powersuite' );
			echo '</span>';
			return;
		}

		if ( $denied === 'S' ) {
			echo '<br /><span style="display:block;border-top:1px solid #ccc;padding-top:5px;margin-top:10px;">';
			esc_html_e( 'Please renew your support period to download the latest version.', 'wp-powersuite' );
			echo '</span>';
			return;
		}

		echo '<br /><span style="display:block;border-top:1px solid #ccc;padding-top:5px;margin-top:10px;">';
		esc_html_e( 'Please activate your license or renew support to download the latest version.', 'wp-powersuite' );
		echo '</span>';
	}

	/**
	 * Fetch update metadata from Elite Licenser (cached for 24 hours).
	 */
	private static function fetch_update_info(): ?object {
		if ( ! function_exists( 'wp_remote_get' ) ) {
			return null;
		}

		$transient_key = self::get_update_transient_key();
		$cached        = get_transient( $transient_key );
		$body          = null;

		if ( is_array( $cached ) && ! empty( $cached['data'] ) && is_string( $cached['data'] ) ) {
			$plain = W_P_Power_Suite_Premium_Base::decrypt_for_cache( $cached['data'] );
			if ( is_string( $plain ) && $plain !== '' ) {
				$payload = @unserialize( $plain, [ 'allowed_classes' => false ] );
				if ( is_array( $payload ) && ! empty( $payload['body'] ) && is_string( $payload['body'] ) ) {
					$body = $payload['body'];
				}
			}
		}

		if ( $body === null ) {
			$body = self::request_update_body();
			if ( $body === null || $body === '' ) {
				return null;
			}

			$encrypted = W_P_Power_Suite_Premium_Base::encrypt_for_cache( serialize( [ 'body' => $body ] ) );
			if ( is_string( $encrypted ) && $encrypted !== '' ) {
				set_transient( $transient_key, [ 'data' => $encrypted ], self::CACHE_TTL );
			}
		}

		return self::parse_update_body( $body );
	}

	/**
	 * GET product/update/{product_id}/{license_key}/{version} from Elite Licenser.
	 *
	 * Always uses real HTTP (including on the vendor host). Elite Licenser API
	 * handlers echo+die; in-process REST would leak ciphertext into admin HTML.
	 */
	private static function request_update_body(): ?string {
		$plugin_file = self::target_plugin_file();
		if ( $plugin_file === '' || ! function_exists( 'wp_remote_get' ) ) {
			return null;
		}

		$version      = W_P_Power_Suite_Premium_Base::get_installed_version( $plugin_file );
		$product_id   = W_P_Power_Suite_Premium_Base::get_product_id();
		$license_info = W_P_Power_Suite_Premium_Base::get_register_info( $plugin_file );

		$url = trailingslashit( W_P_Power_Suite_Premium_Base::get_server_host() ) . 'product/update/' . rawurlencode( $product_id );
		if ( is_object( $license_info ) && ! empty( $license_info->license_key ) ) {
			$url .= '/' . rawurlencode( (string) $license_info->license_key ) . '/' . rawurlencode( $version );
		}

		// Update metadata must never retry with sslverify=false — forged update
		// JSON could supply a malicious package URL. Insecure SSL is license-diagnostics only.
		$args = [
			'sslverify'   => true,
			'timeout'     => 15,
			'redirection' => 3,
		];

		$response = wp_remote_get( $url, $args );

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = (string) wp_remote_retrieve_body( $response );
		if ( $code !== 200 || $body === '' || $body === 'GET404' ) {
			return null;
		}

		return $body;
	}

	/**
	 * Parse Elite Licenser update JSON (plain or encrypted body).
	 */
	private static function parse_update_body( string $body ): ?object {
		$response_json = json_decode( $body );

		if ( ! ( is_object( $response_json ) && isset( $response_json->status ) ) ) {
			$plain = W_P_Power_Suite_Premium_Base::decrypt_update_body( $body );
			if ( is_string( $plain ) && $plain !== '' ) {
				$response_json = json_decode( $plain );
			}
		}

		if ( ! is_object( $response_json ) || empty( $response_json->status ) || empty( $response_json->data->new_version ) ) {
			return null;
		}

		$plugin_file = self::target_plugin_file();
		if ( $plugin_file === '' ) {
			return null;
		}

		$data = $response_json->data;
		if ( ! is_object( $data ) ) {
			return null;
		}

		$data->slug               = plugin_basename( $plugin_file );
		$data->plugin             = plugin_basename( $plugin_file );
		if ( class_exists( '\WP_PowerSuite\Core\WhiteLabel', false ) ) {
			$data->name = \WP_PowerSuite\Core\WhiteLabel::addonPluginName();
			if ( \WP_PowerSuite\Core\WhiteLabel::isActive() && \WP_PowerSuite\Core\WhiteLabel::shouldHideExternalLinks() ) {
				$data->url      = '';
				$data->homepage = '';
			}
		}
		$data->new_version        = ! empty( $data->new_version ) ? (string) $data->new_version : '';
		$data->url                = ! empty( $data->url ) ? (string) $data->url : '';
		$data->package            = ! empty( $data->download_link ) ? (string) $data->download_link : ( ! empty( $data->package ) ? (string) $data->package : '' );
		$data->update_denied_type = ! empty( $data->update_denied_type ) ? (string) $data->update_denied_type : '';
		$data->sections           = isset( $data->sections ) ? (array) $data->sections : [];
		$data->icons              = self::ensure_update_icons( isset( $data->icons ) ? (array) $data->icons : [] );
		$data->banners            = isset( $data->banners ) ? (array) $data->banners : [];
		$data->banners_rtl        = isset( $data->banners_rtl ) ? (array) $data->banners_rtl : [];
		unset( $data->is_stopped_update );

		if ( class_exists( 'WPPS_Package_Integrity', false ) ) {
			$data = WPPS_Package_Integrity::attach_from_update_data( $data, $data );
		}

		return $data;
	}

	/**
	 * Ensure update metadata includes plugin icons for wp-admin/update-core.php.
	 *
	 * WordPress looks for svg, 2x, 1x, or default keys on $plugin_data->update->icons.
	 * Elite Licenser responses often omit icons; fall back to the bundled brand asset.
	 *
	 * @param array<string, string> $icons Icon URLs keyed by WordPress size slug.
	 * @return array<string, string>
	 */
	private static function ensure_update_icons( array $icons ): array {
		foreach ( [ 'svg', '2x', '1x', 'default' ] as $key ) {
			if ( ! empty( $icons[ $key ] ) ) {
				return $icons;
			}
		}

		$icon_url = self::get_default_update_icon_url();
		if ( $icon_url === '' ) {
			return $icons;
		}

		$icons['svg']     = $icon_url;
		$icons['default'] = $icon_url;

		return $icons;
	}

	/**
	 * Bundled plugin icon URL for update screens (same asset as the admin menu).
	 */
	private static function get_default_update_icon_url(): string {
		if ( class_exists( \WP_PowerSuite\Admin\BrandAssets::class ) ) {
			return \WP_PowerSuite\Admin\BrandAssets::default_icon_url();
		}

		if ( ! defined( 'WP_POWERSUITE_PLUGIN_URL' ) || ! defined( 'WP_POWERSUITE_PLUGIN_DIR' ) ) {
			return '';
		}

		$relative_path = 'assets/icon.svg';
		if ( ! is_readable( WP_POWERSUITE_PLUGIN_DIR . $relative_path ) ) {
			return '';
		}

		$version = defined( 'WP_POWERSUITE_VERSION' ) ? WP_POWERSUITE_VERSION : '1.0.0';

		return WP_POWERSUITE_PLUGIN_URL . $relative_path . '?ver=' . rawurlencode( (string) $version );
	}

	private static function get_update_transient_key(): string {
		return W_P_Power_Suite_Premium_Base::get_product_base() . '_up';
	}

	private static function get_license_admin_url(): string {
		if ( ! class_exists( 'WPPS_License_Manager', false ) ) {
			return '';
		}

		return \WP_PowerSuite\Core\NetworkScope::adminUrl( 'admin.php?page=' . WPPS_License_Manager::get_license_page_slug() );
	}

	private static function get_updates_admin_url(): string {
		return \WP_PowerSuite\Core\NetworkScope::adminUrl( 'update-core.php' );
	}

	private static function get_plugins_admin_url(): string {
		return \WP_PowerSuite\Core\NetworkScope::adminUrl( 'plugins.php' );
	}
}
