<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
 * Hustle_Providers
 *
 * @package Hustle
 */

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

/**
 * Class Hustle Providers
 */
class Hustle_Providers {

	/**
	 * The wp_option name of the activated providers
	 *
	 * @since 4.0
	 * @var string
	 */
	private static $active_addons_option = 'hustle_activated_providers';

	/**
	 * Instance of Hustle Providers.
	 *
	 * @since 3.0.5
	 * @var self
	 */
	private static $instance = null;

	/**
	 * Returns the existing instance of Hustle_Providers, or creates a new one if none exists.
	 *
	 * @since 3.0.5
	 * @return Hustle_Providers
	 */
	public static function get_instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Container for all the instantiated providers.
	 *
	 * @since 3.0.5
	 * @var Hustle_Provider_Container
	 */
	private $providers;

	/**
	 * Array with the slugs of the activated providers
	 *
	 * @since 4.0
	 * @var array
	 */
	private $activated_addons = array();

	/**
	 * Default error messages
	 * will be used when an error happens and the loader can't get the provider's error message
	 *
	 * @since 4.0
	 * @var array
	 */
	private $default_addon_error_messages = array();

	/**
	 * Last error message on loader
	 *
	 * @since 4.0
	 * @var string
	 */
	private $last_error_message = '';

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->providers = new Hustle_Provider_Container();

		/**
		 * Initiate activated addons
		 */
		$active_addons = get_option( self::$active_addons_option, false );

		if ( empty( $active_addons ) ) {
			$active_addons = array();
		}

		// Local list is always activated.
		$active_addons = array_unique( array_merge( array( 'local_list' ), $active_addons ) );

		$this->activated_addons = $active_addons;

		/**
		 * Initiate standard default error messages
		 */
		$this->default_addon_error_messages = array(
			'activate'             => __( 'Failed to activate addon', 'hustle' ),
			'deactivate'           => __( 'Failed to deactivate addon', 'hustle' ),
			'update_settings'      => __( 'Failed to update settings', 'hustle' ),
			'update_form_settings' => __( 'Failed to update form settings', 'hustle' ),
		);

		// Only enable wp_ajax hooks.
		if ( wp_doing_ajax() ) {
			Hustle_Provider_Admin_Ajax::get_instance();
		}
	}

	/**
	 * Registers a new Provider.
	 * Created just to avoid third parties having to use Hustle_Providers::get_instance().
	 *
	 * @since 3.0.5
	 * @param Hustle_Provider_Abstract|string $class_name instance of Provider or its classname.
	 * @return bool True if the provider was successfully instantiated and registered. False otherwise.
	 */
	public static function register_provider( $class_name ) {
		return self::get_instance()->register( $class_name );
	}

	/**
	 * Registers a new Provider.
	 *
	 * @since 3.0.5
	 * @param Hustle_Provider_Abstract|string $class_name instance of Provider or its classname.
	 * @return bool True if the provider was successfully instantiated and registered. False otherwise.
	 */
	public function register( $class_name ) {
		try {
			/**
			 * Fires when a provider is registered.
			 *
			 * This action is executed before the whole process of registering a provider.
			 * Validation and requirement check has not been done at this point,
			 * so it's possible that a registered class ends up not being instantiated nor registered
			 * at the end of the process when the validation of the requirements fails.
			 *
			 * @since 3.0.5
			 * @param Hustle_Provider_Abstract|string $class_name instance of Provider or its class name
			 * @return bool True if the provider was registered. False otherwise.
			 */
			do_action( 'hustle_before_provider_registered', $class_name );

			if ( $class_name instanceof Hustle_Provider_Abstract ) {
				$provider_class = $class_name;
			} else {
				$provider_class = $this->validate_provider_class( $class_name );
				if ( ! $provider_class ) {
					return false;
				}
			}
			$registered_providers = $this->providers;

			/**
			 * Filter provider instance.
			 *
			 * It's possible to replace / modify the provider instance when it's registered.
			 * Keep in mind that the instance returned by this filter will be used throughout the plugin.
			 * Return must be instance of @see Hustle_Provider_Abstract.
			 * It will be then validated by @see Hustle_Providers::validate_provider_instance().
			 *
			 * @since 3.0.5
			 * @param Hustle_Provider_Abstract $provider_class Current Provider class instance
			 * @param array $registered_providers Current registered providers
			 */

			$provider_class = apply_filters( 'hustle_provider_instance', $provider_class, $registered_providers );

			$provider_class = $this->validate_provider_instance( $provider_class );

			$this->providers[ $provider_class->get_slug() ] = $provider_class;

			/**
			 * Fires after the provider is successfully registered.
			 *
			 * If the provider is not registered because any reason,
			 * this action will not be executed.
			 *
			 * @since 3.0.5
			 * @param Hustle_Provider_Abstract $provider_class Current provider that's successfully registered
			 */
			do_action( 'hustle_after_provider_registered', $provider_class );

			return true;
		} catch ( Exception $e ) {
			Opt_In_Utils::maybe_log( __METHOD__, $class_name, $e->getMessage() );
			return false;
		}
	}

	/**
	 * Validates provider by its class name.
	 * Validation will fail if:
	 * -The class name passed on $class_name does not exist.
	 * -The provider doesn't have a callable 'get_instance' method. It's properly defined by default on @see Hustle_Provider_Abstract.
	 * -The provider doesn't have a callable 'check_is_compatible' method. It's properly defined by default on @see Hustle_Provider_Abstract.
	 * -The provider's 'check_is_compatible' returns false.
	 *
	 * @since 3.0.5
	 * @param string $class_name Clas name.
	 * @return Hustle_Provider_Abstract
	 * @throws Exception Provider class isn't compatible.
	 */
	private function validate_provider_class( $class_name ) {
		if ( ! class_exists( $class_name ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'Provider with %s does not exist', 'hustle' ), esc_html( $class_name ) ) );
		}

		if ( ! is_callable( array( $class_name, 'get_instance' ) ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'Provider with %s does not have get_instance method', 'hustle' ), esc_html( $class_name ) ) );
		}

		if ( ! is_callable( array( $class_name, 'check_is_compatible' ) ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'Provider with %s does not have check_is_compatible method', 'hustle' ), esc_html( $class_name ) ) );
		}

		if ( ! call_user_func( array( $class_name, 'check_is_compatible' ), $class_name ) ) {
			return false;
		}

		$provider_class = call_user_func( array( $class_name, 'get_instance' ), $class_name );

		return $provider_class;
	}

	/**
	 * Validates the provider instance.
	 * Validation will fail if the provider instance:
	 * -Is not an instance of @see Hustle_Provider_Abstract.
	 * -Doesn't have a _slug property.
	 * -Doesn't have a _title property.
	 * -Doesn't have a _version property.
	 * -Doesn't have a _class property.
	 * -Has the same slug of an existing provider.
	 *
	 * @since 3.0.5
	 * @param Hustle_Provider_Abstract $instance Instance.
	 * @return Hustle_Provider_Abstract
	 * @throws Exception Provider class isn't compatible.
	 */
	private function validate_provider_instance( Hustle_Provider_Abstract $instance ) {
		/** Hustle_Provider_Abstract $provider_class */
		$provider_class = $instance;
		$class_name     = get_class( $instance );

		if ( ! $provider_class instanceof Hustle_Provider_Abstract ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'The provider %s is not instanceof Hustle_Provider_Abstract', 'hustle' ), esc_html( $class_name ) ) );
		}
		$slug    = $provider_class->get_slug();
		$title   = $provider_class->get_title();
		$version = $provider_class->get_version();
		$class   = $provider_class->get_class();

		if ( empty( $slug ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'The provider %s does not have the required _slug property.', 'hustle' ), esc_html( $class_name ) ) );
		}
		if ( empty( $title ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'The provider %s does not have the required _title property.', 'hustle' ), esc_html( $class_name ) ) );
		}
		if ( empty( $class ) ) {
			throw new Exception( sprintf( /* translators: %s: provider class name */ esc_html__( 'The provider %s does not the required _class property.', 'hustle' ), esc_html( $class_name ) ) );
		}

		// FIFO.
		if ( isset( $this->providers[ $slug ] ) ) {
			throw new Exception( sprintf( /* translators: %s: provider slug */ esc_html__( 'The provider with the slug %s already exists.', 'hustle' ), esc_html( $slug ) ) );
		}
		if ( empty( $version ) ) {
			throw new Exception( sprintf( /* translators: %s: provider slug */ esc_html__( 'Provider with the slug %s does not have a valid _version property.', 'hustle' ), esc_html( $slug ) ) );
		}

		// check if the version changed if active.
		if ( $this->addon_is_active( $slug ) ) {
			try {
				// silent.
				if ( $provider_class->is_version_changed() ) {
					$provider_class->version_changed( $provider_class->get_installed_version(), $provider_class->get_installed_version() );
				}
			} catch ( Exception $e ) {
				Hustle_Provider_Utils::maybe_log( $provider_class->get_slug(), 'failed to trigger version_changed', $e->getMessage() );
			}
		}

		return $provider_class;
	}

	/**
	 * Gets an instace of a provider by its slug.
	 *
	 * @param string $slug Slug of the provider to be retrieved.
	 * @return Hustle_Provider_Abstract|mixed|null
	 */
	public function get_provider( $slug ) {
		if ( isset( $this->providers[ $slug ] ) ) {
			return $this->providers[ $slug ];
		}
	}

	/**
	 * Activate provider
	 * This function will call the 'activate' method of the provider if available.
	 *
	 * @since 4.0
	 *
	 * @param string $slug Slug.
	 * @return bool
	 */
	public function activate_addon( $slug ) {
		$addon = $this->get_provider( $slug );

		if ( is_null( $addon ) ) {
			$this->last_error_message = __( 'Provider not found', 'hustle' );
			return false;
		}

		if ( $this->addon_is_active( $slug ) ) {
			$this->last_error_message = __( 'The provider is already active', 'hustle' );
			return false;
		}

		if ( ! $addon->is_activable() ) {
			$this->last_error_message = __( 'The provider is not activable', 'hustle' );
			return false;
		}

		$activated = $addon->activate();
		if ( ! $activated ) {
			$error_message = $addon->get_activation_error_message();
			if ( empty( $error_message ) ) {
				$error_message = $this->default_addon_error_messages['activate'];
			}
			$this->last_error_message = $error_message;

			return false;
		}

		$this->add_activated_addons( $slug );

		return true;
	}

	/**
	 * Get last error
	 *
	 * @return string
	 */
	public function get_last_error_message() {
		return $this->last_error_message;
	}

	/**
	 * Get default error messages
	 *
	 * @return array
	 */
	public function get_default_messages() {
		return $this->default_addon_error_messages;
	}

	/**
	 * Returns the container of all registered providers.
	 * Keep in mind that a provider that is successfully registered and listed here
	 * might not be included on the application if its 'check_is_activable' method returns false.
	 *
	 * @return Hustle_Provider_Container
	 */
	public function get_providers() {
		return $this->providers;
	}

	/**
	 * Check if the provider is active
	 *
	 * @since 4.0
	 *
	 * @param string $slug Slug.
	 * @return bool
	 */
	public function addon_is_active( $slug ) {
		if ( in_array( $slug, $this->activated_addons, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Disconnect the addon.
	 *
	 * @since 3.0.5
	 *
	 * @param string $slug provider slug.
	 * @param array  $submitted_data Submitted data.
	 * @return bool
	 */
	public function deactivate_addon( $slug, $submitted_data = array() ) {
		$addon = $this->get_provider( $slug );
		if ( is_null( $addon ) ) {
			$this->last_error_message = __( 'Provider not found', 'hustle' );
			return false;
		}

		if ( ! $this->addon_is_active( $slug ) ) {
			$this->last_error_message = __( 'Provider is not activated before', 'hustle' );
			return false;
		}

		$deactivated = $addon->deactivate( $submitted_data );
		if ( ! $deactivated ) {
			$error_message = $addon->get_deactivation_error_message();
			if ( empty( $error_message ) ) {
				$error_message = $this->default_addon_error_messages['deactivate'];
			}
			$this->last_error_message = $error_message;
			return false;
		}

		// If the provider allows having multiple instances globally, remove that instance only.
		if ( $addon->is_allow_multi_on_global() ) {
			if ( ! empty( $submitted_data['global_multi_id'] ) ) {
				$settings_values = $addon->get_settings_values();
				unset( $settings_values[ $submitted_data['global_multi_id'] ] );

				if ( ! empty( $settings_values ) ) {
					// Simply remove this instance from the global ones if there are other instances connected.
					$addon->save_settings_values( $settings_values );
				} else {
					// Remove the global provider's settings if there aren't more global instances of this one.
					$this->force_remove_activated_addons( $slug );
				}
			}
		} else {
			// Do this only if global_multi_id is disabled.
			$this->force_remove_activated_addons( $slug );
		}

		// Remove the provider from the modules.
		$this->disconnect_provider_instance_from_modules( $addon, $submitted_data );

		return true;
	}

	/**
	 * Disconnect the given provider from all the modules.
	 *
	 * @since 4.0.1
	 *
	 * @param Hustle_Provider_Abstract $provider Provider.
	 * @param array                    $submitted_data Submitted data.
	 */
	private function disconnect_provider_instance_from_modules( Hustle_Provider_Abstract $provider, $submitted_data ) {

		$is_multi_on_global = $provider->is_allow_multi_on_global();
		$is_multi_on_form   = $provider->is_allow_multi_on_form();

		$global_multi_id = ( $is_multi_on_global && ! $is_multi_on_form && ! empty( $submitted_data['global_multi_id'] ) ) ? $submitted_data['global_multi_id'] : false;

		$modules = Hustle_Provider_Utils::get_modules_by_active_provider( $provider->get_slug(), $global_multi_id );

		foreach ( $modules as $module ) {

			$form_settings = $provider->get_provider_form_settings( $module->module_id );

			if ( $form_settings instanceof Hustle_Provider_Form_Settings_Abstract ) {
				$form_settings->disconnect_form( $submitted_data );
			}
		}
	}

	/**
	 * Add activated provider to wp_options
	 *
	 * @since 4.0
	 * @param string $slug Slug.
	 */
	private function add_activated_addons( $slug ) {
		$addon                    = $this->get_provider( $slug );
		$this->activated_addons[] = $slug;
		update_option( self::$active_addons_option, $this->activated_addons );
		// take from get_version() since it's a new provider.
		update_option( $addon->get_version_options_name(), $addon->get_version() );
	}

	/**
	 * Force Remove activated addons
	 * remove activated addons from wp options, without calling deactivate on addon function
	 *
	 * @since 4.0
	 * @param string $slug Slug.
	 */
	public function force_remove_activated_addons( $slug ) {
		$addon = $this->get_provider( $slug );

		$index = array_search( $slug, $this->activated_addons, true );
		if ( false !== $index ) {
			unset( $this->activated_addons[ $index ] );
			// reset keys.
			$this->activated_addons = array_values( $this->activated_addons );
			update_option( self::$active_addons_option, $this->activated_addons );
		}

		if ( $addon ) {
			$version_options_name  = $addon->get_version_options_name();
			$settions_options_name = $addon->get_settings_options_name();
		} else {
			// probably just want to remove the options.
			$version_options_name  = 'hustle_provider_' . $slug . '_version';
			$settions_options_name = 'hustle_provider_' . $slug . '_settings';
		}

		// delete version.
		delete_option( $version_options_name );
		// delete general settings.
		delete_option( $settions_options_name );

		$addon->remove_wp_options();
	}


	/**
	 * Get an array with the slug of the activated providers
	 *
	 * @since 4.0
	 * @return array
	 */
	public function get_activated_addons() {
		return $this->activated_addons;
	}
}
