<?php
/**
 * @license GPL-2.0-or-later
 *
 * Modified using Strauss.
 * @see https://github.com/BrianHenryIE/strauss
 */ declare( strict_types=1 );

namespace RCP\LiquidWeb\Harbor\Features\Update;

use RCP\LiquidWeb\Harbor\Features\Contracts\Installable;
use RCP\LiquidWeb\Harbor\Features\Feature_Repository;
use RCP\LiquidWeb\Harbor\Features\Types\Feature;
use RCP\LiquidWeb\Harbor\Legacy\License_Repository as Legacy_License_Repository;
use RCP\LiquidWeb\Harbor\Licensing\License_Manager;
use RCP\LiquidWeb\Harbor\Traits\With_Debugging;
use stdClass;

/**
 * Consolidated update handler for Plugin features.
 *
 * Filters `plugins_api`, `pre_set_site_transient_update_plugins`,
 * and `site_transient_update_plugins` to provide update information
 * from the consolidation server.
 *
 * @since 1.0.0
 */
class Plugin_Handler {

	use With_Debugging;

	/**
	 * The update data resolver.
	 *
	 * @since 1.0.0
	 *
	 * @var Resolve_Update_Data
	 */
	private Resolve_Update_Data $resolver;

	/**
	 * The feature repository.
	 *
	 * @since 1.0.0
	 *
	 * @var Feature_Repository
	 */
	private Feature_Repository $feature_repository;

	/**
	 * The license manager.
	 *
	 * @since 1.0.0
	 *
	 * @var License_Manager
	 */
	private License_Manager $license_manager;

	/**
	 * The legacy license repository.
	 *
	 * @since 1.3.0
	 *
	 * @var Legacy_License_Repository
	 */
	private Legacy_License_Repository $legacy_repository;

	/**
	 * Constructor for the consolidated update handler.
	 *
	 * @since 1.0.0
	 *
	 * @param Resolve_Update_Data       $resolver           The update data resolver.
	 * @param Feature_Repository        $feature_repository The feature repository.
	 * @param License_Manager           $license_manager    The license manager.
	 * @param Legacy_License_Repository $legacy_repository  The legacy license repository.
	 *
	 * @return void
	 */
	public function __construct(
		Resolve_Update_Data $resolver,
		Feature_Repository $feature_repository,
		License_Manager $license_manager,
		Legacy_License_Repository $legacy_repository
	) {
		$this->resolver           = $resolver;
		$this->feature_repository = $feature_repository;
		$this->license_manager    = $license_manager;
		$this->legacy_repository  = $legacy_repository;
	}

	/**
	 * Filters the plugins_api response for Plugin features.
	 *
	 * Resolves update data by joining the Feature_Repository and Catalog.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed       $result The current result (false or object).
	 * @param string|null $action The API action.
	 * @param object|null $args   The API request args.
	 *
	 * @return mixed
	 */
	public function filter_plugins_api( $result, ?string $action = null, $args = null ) {
		if ( 'plugin_information' !== $action || ! is_object( $args ) || empty( $args->slug ) ) {
			return $result;
		}

		// Cheap gate: skip when there is neither a Unified key nor any legacy entry opted into updates.
		// The downstream resolver still confirms each legacy entry's grant before granting availability.
		if ( empty( $this->license_manager->get_key() ) && ! $this->legacy_repository->any_used_for_updates() ) {
			return $result;
		}

		/** @var string $slug */
		$slug = $args->slug;

		// Check whether the requested slug belongs to a known Plugin feature.
		$features = $this->feature_repository->get();

		if ( is_wp_error( $features ) ) {
			static::debug_log_wp_error(
				$features,
				'Plugin_Handler::filter_plugins_api: feature repository failed'
			);

			return $result;
		}

		$feature = $features->get( $slug );

		if ( $feature === null ) {
			return $result;
		}

		if (
			$feature instanceof Installable
			&& $feature->is_wporg()
		) {
			$wporg_slug = $feature->get_wporg_slug();

			if ( $wporg_slug === null || $wporg_slug === $slug ) {
				return $result;
			}

			return \plugins_api(
				'plugin_information',
				[
					'slug'   => $wporg_slug,
					'fields' => [ 'sections' => false ],
				]
			);
		}

		$response = ( $this->resolver )( Feature::TYPE_PLUGIN );

		if ( is_wp_error( $response ) ) {
			static::debug_log_wp_error(
				$response,
				sprintf(
					'Plugin_Handler::filter_plugins_api: resolver failed for "%s"',
					$slug
				)
			);

			return $result;
		}

		if ( empty( $response[ $slug ] ) ) {
			return $result;
		}

		return $this->to_wp_format( $slug, $response[ $slug ] );
	}

	/**
	 * Filters the update_plugins transient to inject consolidated updates.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $transient The update_plugins transient value.
	 *
	 * @return mixed
	 */
	public function filter_update_check( $transient ) {
		if ( ! is_object( $transient ) ) {
			$transient = new stdClass();
		}

		// Cheap gate: skip when there is neither a Unified key nor any legacy entry opted into updates.
		// The downstream resolver still confirms each legacy entry's grant before granting availability.
		if ( empty( $this->license_manager->get_key() ) && ! $this->legacy_repository->any_used_for_updates() ) {
			return $transient;
		}

		$response = ( $this->resolver )( Feature::TYPE_PLUGIN );

		if ( is_wp_error( $response ) ) {
			static::debug_log_wp_error(
				$response,
				'Plugin_Handler::filter_update_check: resolver failed'
			);

			return $transient;
		}

		if ( empty( $response ) ) {
			return $transient;
		}

		/**
		 * WordPress stores update data in two arrays on the transient object:
		 * - `response`: plugins that have a newer version available.
		 * - `no_update`: plugins that are up-to-date (checked, but no update).
		 *
		 * Both are keyed by plugin file path and contain stdClass objects.
		 */
		/** @var stdClass $transient */
		if ( ! property_exists( $transient, 'response' ) ) {
			$transient->response = [];
		}

		if ( ! property_exists( $transient, 'no_update' ) ) {
			$transient->no_update = [];
		}

		/** @var array<string, stdClass> $wp_response */
		$wp_response = $transient->response;
		/** @var array<string, stdClass> $wp_no_update */
		$wp_no_update = $transient->no_update;

		$installed_plugins = get_plugins();

		foreach ( $response as $slug => $update_data ) {
			/** @var string $plugin_file */
			$plugin_file = $update_data['plugin_file'] ?? '';

			if ( empty( $plugin_file ) ) {
				continue;
			}

			// Skip features that are not installed on this site.
			if ( ! isset( $installed_plugins[ $plugin_file ] ) ) {
				continue;
			}

			$update_object = $this->to_update_object( $slug, $plugin_file, $update_data );

			/**
			 * Place the update object in `response` if a newer version is available.
			 * WordPress uses this distinction to show (or hide) the plugin on the Updates page.
			 *
			 * When we don't have an update, only write to `no_update` if the plugin isn't
			 * already in `response` from another system (e.g. legacy licensing) to avoid
			 * clearing updates we didn't provide.
			 */
			if ( $update_data['has_update'] ?? false ) {
				$wp_response[ $plugin_file ] = $update_object;
				unset( $wp_no_update[ $plugin_file ] );
			} elseif ( ! isset( $wp_response[ $plugin_file ] ) ) {
				$wp_no_update[ $plugin_file ] = $update_object;
			}
		}

		$transient->response  = $wp_response;
		$transient->no_update = $wp_no_update;

		return $transient;
	}

	/**
	 * Builds a WordPress-format plugin info object for plugins_api responses.
	 *
	 * @since 1.0.0
	 *
	 * @param string               $slug        The plugin slug.
	 * @param array<string, mixed> $update_data The update data from the consolidation server.
	 *
	 * @return stdClass
	 */
	private function to_wp_format( string $slug, array $update_data ): stdClass {
		$info = new stdClass();

		$info->name          = $update_data['name'] ?? '';
		$info->slug          = $slug;
		$info->version       = $update_data['version'] ?? '';
		$info->requires      = $update_data['requires'] ?? '';
		$info->tested        = $update_data['tested'] ?? '';
		$info->download_link = $update_data['package'] ?? '';
		$info->author        = $update_data['author'] ?? '';
		$info->homepage      = $update_data['url'] ?? '';
		$info->last_updated  = $update_data['last_updated'] ?? '';
		$info->sections      = $update_data['sections'] ?? [ 'description' => '' ];

		return $info;
	}

	/**
	 * Builds an update object for the update_plugins transient.
	 *
	 * @since 1.0.0
	 *
	 * @param string               $slug        The plugin slug.
	 * @param string               $plugin_file The plugin file path.
	 * @param array<string, mixed> $update_data The update data from the consolidation server.
	 *
	 * @return stdClass
	 */
	private function to_update_object( string $slug, string $plugin_file, array $update_data ): stdClass {
		$update = new stdClass();

		$update->id          = $update_data['id'] ?? sprintf( 'stellarwp/plugins/%s', $slug );
		$update->plugin      = $plugin_file;
		$update->slug        = $slug;
		$update->new_version = $update_data['version'] ?? '';
		$update->url         = $update_data['url'] ?? '';
		$update->package     = $update_data['package'] ?? '';
		$update->tested      = $update_data['tested'] ?? '';
		$update->requires    = $update_data['requires'] ?? '';

		return $update;
	}
}
