<?php
/**
 * Display notices in the WordPress admin for CoCart.
 *
 * @author  Sébastien Dumont
 * @package CoCart\Admin\Notices
 * @since   1.2.0 Introduced.
 * @version 4.3.7
 * @license GPL-3.0
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'CoCart_Admin_Notices' ) ) {

	class CoCart_Admin_Notices {

		/**
		 * Activation date.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @var string
		 */
		public static $install_date;

		/**
		 * Stores notices.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @var array
		 */
		public static $notices = array();

		/**
		 * Array of notices - name => callback.
		 *
		 * @access private
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @var array
		 */
		private static $core_notices = array(
			'update_db'           => 'update_db_notice',
			'check_php'           => 'check_php_notice',
			'check_wp'            => 'check_wp_notice',
			'check_wc'            => 'check_woocommerce_notice',
			'plugin_review'       => 'plugin_review_notice',
			'check_beta'          => 'check_beta_notice',
			'upgrade_warning'     => 'upgrade_warning_notice',
			'base_tables_missing' => 'base_tables_missing_notice',
			'setup_wizard'        => 'setup_wizard_notice',
			'disabled_wp_source'  => 'disabled_wp_source_notice',
		);

		/**
		 * Stores a flag indicating if the code is running in a multisite setup.
		 *
		 * @access private
		 *
		 * @static
		 *
		 * @var bool
		 */
		private static bool $is_multisite;

		/**
		 * Constructor
		 *
		 * @access public
		 *
		 * @since 1.2.0  Introduced.
		 * @since 3.10.0 Added notice for WordPress Playground
		 */
		public function __construct() {
			self::$is_multisite = is_multisite();
			self::$install_date = get_option( 'cocart_install_date', time() );
			self::set_notices( get_option( 'cocart_admin_notices', array() ) );

			add_action( 'switch_theme', array( $this, 'reset_admin_notices' ) );
			add_action( 'cocart_installed', array( $this, 'reset_admin_notices' ) );
			add_action( 'wp_loaded', array( $this, 'hide_notices' ) );
			add_action( 'wp_loaded', array( $this, 'timed_notices' ), 11 );

			add_action( 'shutdown', array( $this, 'store_notices' ) );

			// If the current user has capabilities then add notices.
			if ( CoCart_Helpers::user_has_capabilities() ) {
				add_action( 'admin_print_styles', array( $this, 'add_notices' ) );
			}

			// Notice for WordPress Playground.
			add_action( 'admin_notices', array( $this, 'wordpress_playground_notice' ) );
		} // END __construct()

		/**
		 * Store notices to DB.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 */
		public static function store_notices() {
			update_option( 'cocart_admin_notices', self::get_notices() );
		} // END store_notices()

		/**
		 * Get notices.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 * @since 4.3.7 Adjusted to get cached notices for the current site should it be a multisite.
		 *
		 * @return array Cached notices for current site.
		 */
		public static function get_notices() {
			if ( ! self::$is_multisite ) {
				return self::$notices;
			}

			$blog_id = get_current_blog_id();
			$notices = self::$notices[ $blog_id ] ?? null;
			if ( ! is_null( $notices ) ) {
				return $notices;
			}

			self::$notices[ $blog_id ] = get_option( 'cocart_admin_notices', array() );
			return self::$notices[ $blog_id ];
		} // END get_notices()

		/**
		 * Set the locally cached notices array for the current site.
		 *
		 * @access private
		 *
		 * @static
		 *
		 * @since 4.3.7 Introduced.
		 *
		 * @param array $notices New value for the locally cached notices array.
		 */
		private static function set_notices( array $notices ) {
			if ( self::$is_multisite ) {
				self::$notices[ get_current_blog_id() ] = $notices;
			} else {
				self::$notices = $notices;
			}
		} // END set_notices()

		/**
		 * Remove all notices.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 */
		public static function remove_all_notices() {
			self::set_notices( array() );
		} // END remove_all_notices()

		/**
		 * Reset notices for when new version of CoCart is installed.
		 *
		 * @access public
		 *
		 * @since 3.0.0 Introduced.
		 */
		public function reset_admin_notices() {
			self::add_notice( 'check_php' );
			self::add_notice( 'check_wp' );
			self::add_notice( 'check_wc' );
			self::add_notice( 'check_beta' );
			self::add_notice( 'disabled_wp_source' );
		} // END reset_admin_notices()

		/**
		 * Show a notice.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @param string $notice_name Notice name.
		 * @param bool   $force_save  Force saving inside this method instead of at the 'shutdown'.
		 */
		public static function add_notice( string $notice_name, bool $force_save = false ) {
			self::set_notices( array_unique( array_merge( self::get_notices(), array( $notice_name ) ) ) );

			if ( $force_save ) {
				// Adding early save to prevent more race conditions with notices.
				self::store_notices();
			}
		} // END add_notice()

		/**
		 * Remove a notice from being displayed.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @param string $notice_name Notice name.
		 * @param bool   $force_save  Force saving inside this method instead of at the 'shutdown'.
		 */
		public static function remove_notice( string $notice_name, bool $force_save = false ) {
			// Check that the notice exists before attempting to remove it.
			if ( self::has_notice( $notice_name ) ) {
				self::set_notices( array_diff( self::get_notices(), array( $notice_name ) ) );

				delete_option( 'cocart_admin_notice_' . $notice_name );

				if ( $force_save ) {
					// Adding early save to prevent more conditions with notices.
					self::store_notices();
				}

				/**
				 * Hook: Hide a CoCart notice.
				 *
				 * Example: `cocart_hide_plugin_review_notice'
				 */
				do_action( "cocart_hide_{$notice_name}_notice" );
			}
		} // END remove_notice()

		/**
		 * Remove a given set of notices.
		 *
		 * An array of notice names or a regular expression string can be passed, in the later case
		 * all the notices whose name matches the regular expression will be removed.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 4.3.7 Introduced.
		 *
		 * @param array|string $names_array_or_regex An array of notice names, or a string representing a regular expression.
		 * @param bool         $force_save           Force saving inside this method instead of at the 'shutdown'.
		 *
		 * @return void
		 */
		public static function remove_notices( $names_array_or_regex, $force_save = false ) {
			if ( ! is_array( $names_array_or_regex ) ) {
				$names_array_or_regex = array_filter( self::get_notices(), fn( $notice_name ) => 1 === preg_match( $names_array_or_regex, $notice_name ) );
			}

			self::set_notices( array_diff( self::get_notices(), $names_array_or_regex ) );

			if ( $force_save ) {
				// Adding early save to prevent more race conditions with notices.
				self::store_notices();
			}
		} // END remove_notices()

		/**
		 * See if a notice is being shown.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since   3.0.0 Introduced.
		 * @version 3.1.0
		 *
		 * @param string $notice_name Notice name.
		 *
		 * @return boolean
		 */
		public static function has_notice( string $notice_name ) {
			return in_array( $notice_name, self::get_notices(), true );
		} // END has_notice()

		/**
		 * Hide a notice if the GET variable is set.
		 *
		 * @access public
		 *
		 * @since 3.0.0 Introduced.
		 */
		public function hide_notices() {
			if ( isset( $_GET['cocart-hide-notice'] ) && isset( $_GET['_cocart_notice_nonce'] ) ) {
				if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_cocart_notice_nonce'] ) ), 'cocart_hide_notices_nonce' ) ) {
					wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'cocart-core' ) );
				}

				if ( ! CoCart_Helpers::user_has_capabilities() ) {
					wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'cocart-core' ) );
				}

				$notice_name = sanitize_text_field( wp_unslash( $_GET['cocart-hide-notice'] ) );

				self::hide_notice( $notice_name );

				wp_safe_redirect( remove_query_arg( array( 'cocart-hide-notice', '_cocart_notice_nonce' ), CoCart_Helpers::cocart_get_current_admin_url() ) );
				exit;
			}
		} // END hide_notices()

		/**
		 * Hide a single notice.
		 *
		 * @access private
		 *
		 * @static
		 *
		 * @since 4.3.7 Introduced.
		 *
		 * @param string $notice_name Notice name.
		 */
		private static function hide_notice( string $notice_name ) {
			self::remove_notice( $notice_name );

			update_user_meta( get_current_user_id(), 'dismissed_cocart_' . $notice_name . '_notice', true );

			/**
			 * Hook: Hide a CoCart notice.
			 *
			 * Example: `cocart_hide_plugin_review_notice'
			 */
			do_action( "cocart_hide_{$notice_name}_notice" );
		} // END hide_notice()

		/**
		 * Check if a given user has dismissed a given admin notice.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 4.3.7 Introduced.
		 *
		 * @param string   $notice_name The name of the admin notice to check.
		 * @param int|null $user_id     User id, or null for the current user.
		 *
		 * @return bool True if the user has dismissed the notice.
		 */
		public static function user_has_dismissed_notice( string $notice_name, ?int $user_id = null ): bool {
			return (bool) get_user_meta( $user_id ?? get_current_user_id(), "dismissed_cocart_{$notice_name}_notice", true );
		} // END user_has_dismissed_notice()

		/**
		 * Add notices.
		 *
		 * @access public
		 *
		 * @since 3.0.0  Introduced.
		 * @since 3.10.0 Prevented notices from showing if on WordPress Playground.
		 */
		public function add_notices() {
			// Prevent notices from loading on the frontend.
			if ( ! is_admin() ) {
				return;
			}

			// Don't show notices if on WordPress Playground.
			if ( CoCart_Helpers::is_on_wordpress_playground() ) {
				return;
			}

			$notices = self::get_notices();

			if ( empty( $notices ) ) {
				return;
			}

			// Notice should only show on specific pages.
			if ( ! CoCart_Helpers::is_cocart_admin_page( array( 'update-core' ) ) ) {
				return;
			}

			foreach ( $notices as $notice ) {
				if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'cocart_show_admin_notice', true, $notice ) ) {
					add_action( 'admin_notices', array( $this, self::$core_notices[ $notice ] ) );
				} else {
					add_action( 'admin_notices', array( $this, 'output_custom_notices' ) );
				}
			}
		} // END add_notices()

		/**
		 * Add a custom notice.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @param string $notice_name Notice name.
		 * @param string $notice_html Notice HTML.
		 */
		public static function add_custom_notice( string $notice_name, string $notice_html ) {
			self::add_notice( $notice_name );
			update_option( 'cocart_admin_notice_' . $notice_name, wp_kses_post( $notice_html ) );
		} // END add_custom_notice()

		/**
		 * Output any stored custom notices.
		 *
		 * @access public
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @return void
		 */
		public function output_custom_notices() {
			$notices = self::get_notices();

			if ( ! empty( $notices ) ) {
				foreach ( $notices as $notice ) {
					if ( empty( self::$core_notices[ $notice ] ) ) {
						$notice_html = get_option( 'cocart_admin_notice_' . $notice );

						if ( $notice_html ) {
							include __DIR__ . '/views/html-notice-custom.php';
						}
					}
				}
			}
		} // END output_custom_notices()

		/**
		 * Notice about base tables missing.
		 *
		 * @access public
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @return void
		 */
		public function base_tables_missing_notice() {
			$notice_dismissed = apply_filters(
				'cocart_hide_base_tables_missing_nag',
				get_user_meta( get_current_user_id(), 'dismissed_cocart_base_tables_missing_notice', true )
			);

			if ( $notice_dismissed ) {
				self::remove_notice( 'base_tables_missing' );
			}

			include_once __DIR__ . '/views/html-notice-base-table-missing.php';
		} // END base_tables_missing_notice()

		/**
		 * Shows a notice asking the user for a review of CoCart.
		 *
		 * @access public
		 *
		 * @since   3.0.0 Introduced.
		 * @version 3.2.0
		 *
		 * @return void
		 */
		public function timed_notices() {
			// Add review notice first. We will remove it after if already dismissed.
			self::add_notice( 'plugin_review' );

			// Was the plugin review notice dismissed?
			$hide_review_notice = self::user_has_dismissed_notice( 'plugin_review' );

			// If review plugin notice dismissed, remove it.
			if ( $hide_review_notice ) {
				self::remove_notice( 'plugin_review' );
			}
		} // END timed_notices()

		/**
		 * Shows an upgrade warning notice if the installed version is less
		 * than the new release coming soon.
		 *
		 * @access public
		 *
		 * @since 1.2.3 Introduced.
		 * @since 3.10.4 Check how long CoCart has been installed before showing.
		 *
		 * @return void
		 */
		public function upgrade_warning_notice() {
			$version = strstr( COCART_VERSION, '-', true );

			// If version returns empty then just set as the current plugin version.
			if ( empty( $version ) ) {
				$version = COCART_VERSION;
			}

			// If CoCart has only been installed for less than 6 weeks then do not show this notice.
			if ( ( intval( time() - self::$install_date ) ) < WEEK_IN_SECONDS * 6 ) {
				return;
			}

			if ( ! CoCart_Helpers::is_cocart_pre_release() && version_compare( $version, COCART_NEXT_VERSION, '<' ) ) {
				include_once __DIR__ . '/views/html-notice-upgrade-warning.php';
			}
		} // END upgrade_warning_notice()

		/**
		 * If we need to update the database, include a message with the DB update button.
		 *
		 * @access public
		 *
		 * @static
		 *
		 * @since 3.0.0 Introduced.
		 */
		public static function update_db_notice() {
			$screen    = get_current_screen();
			$screen_id = $screen ? $screen->id : '';

			if ( CoCart_Install::needs_db_update() ) {
				$next_scheduled_date = WC()->queue()->get_next( 'cocart_run_update_callback', null, 'cocart-db-updates' );

				if ( $next_scheduled_date || ! empty( $_GET['do_update_cocart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
					include __DIR__ . '/views/html-notice-updating.php';
				} else {
					include __DIR__ . '/views/html-notice-update.php';
				}
			} else {
				include __DIR__ . '/views/html-notice-updated.php';
				self::remove_notice( 'update_db', true );
				update_user_meta( get_current_user_id(), 'dismissed_cocart_update_db_notice', true );
			}
		} // END update_db_notice()

		/**
		 * Checks the environment on loading WordPress, just in case the environment changes after activation.
		 *
		 * Shows the PHP requirement notice if minimum requirement does not meet.
		 *
		 * @access public
		 *
		 * @since   2.6.0 Introduced.
		 * @version 3.0.0
		 *
		 * @return void
		 */
		public function check_php_notice() {
			if ( ! CoCart_Helpers::is_environment_compatible() && is_plugin_active( plugin_basename( COCART_FILE ) ) ) {
				CoCart::deactivate_plugin();

				include_once __DIR__ . '/views/html-notice-requirement-php.php';
			}
		} // END check_php_notice()

		/**
		 * Checks that the WordPress version meets the plugin requirement before deciding
		 * to deactivate the plugin and show the WordPress requirement notice if it doesn't meet.
		 *
		 * @access public
		 *
		 * @since   1.2.0 Introduced.
		 * @version 3.0.0
		 *
		 * @return void
		 */
		public function check_wp_notice() {
			if ( ! CoCart_Helpers::is_wp_version_gte( CoCart::$required_wp ) ) {
				CoCart::deactivate_plugin();
				include_once __DIR__ . '/views/html-notice-requirement-wp.php';
			}
		} // END check_wp_notice()

		/**
		 * Check WooCommerce Dependency.
		 *
		 * @access public
		 *
		 * @since 3.0.0 Introduced.
		 *
		 * @return void
		 */
		public function check_woocommerce_notice() {
			if ( ! defined( 'WC_VERSION' ) ) {
				// Deactivate plugin.
				CoCart::deactivate_plugin();

				// WooCommerce is Not Installed or Activated Notice.
				include_once __DIR__ . '/views/html-notice-wc-not-installed.php';
			} elseif ( version_compare( WC_VERSION, CoCart::$required_woo, '<' ) ) {
				/**
				 * Displays a warning message if minimum version of WooCommerce check fails and
				 * provides an update button if the user has admin capabilities to update the plugin.
				 */
				include_once __DIR__ . '/views/html-notice-required-wc.php';
			}
		} // END check_woocommerce_notice()

		/**
		 * Displays notice if user is testing pre-release version of the plugin.
		 *
		 * @access public
		 *
		 * @since   1.2.0 Introduced.
		 * @version 3.0.0
		 *
		 * @return void
		 */
		public function check_beta_notice() {
			// Is this version of CoCart a pre-release?
			if ( CoCart_Helpers::is_cocart_pre_release() ) {
				include_once __DIR__ . '/views/html-notice-trying-beta.php';
			}
		} // END check_beta_notice()

		/**
		 * Displays plugin review notice.
		 *
		 * Shown after 2 weeks or more from the time the plugin was installed.
		 *
		 * @access public
		 *
		 * @since   1.2.0 Introduced.
		 * @version 3.0.0
		 *
		 * @return void
		 */
		public function plugin_review_notice() {
			// If it has been 2 weeks or more since activating the plugin then display the review notice.
			if ( ( intval( time() - self::$install_date ) ) > WEEK_IN_SECONDS * 2 ) {
				include_once __DIR__ . '/views/html-notice-please-review.php';
			}
		} // END plugin_review_notice()

		/**
		 * Displays setup wizard notice.
		 *
		 * Shows only for those new to CoCart or setup wizard has not be done.
		 *
		 * @access public
		 *
		 * @since 3.1.0 Introduced.
		 *
		 * @return void
		 */
		public function setup_wizard_notice() {
			include_once __DIR__ . '/views/html-notice-setup-wizard.php';
		} // END setup_wizard_notice()

		/**
		 * Displays a notice once activated only if a legacy version of CoCart
		 * from WordPress dot ORG was detected.
		 *
		 * @access public
		 *
		 * @since 5.0.0 Introduced.
		 *
		 * @return void
		 */
		public function disabled_wp_source_notice() {
			$deactivated_notice = (int) get_transient( 'cocart_legacy_deactivated' );
			if ( ! $deactivated_notice ) {
				return;
			}

			include_once __DIR__ . '/views/html-notice-disabled-wp-source.php';

			CoCart_Utilities_Cache_Helpers::queue_delete_transient( 'cocart_legacy_deactivated' );
		} // END disabled_wp_source_notice()

		/**
		 * Displays a notice if the user installed CoCart on WordPress Playground.
		 *
		 * @access public
		 *
		 * @since 3.10.0 Introduced.
		 *
		 * @return void
		 */
		public function wordpress_playground_notice() {
			if ( CoCart_Helpers::is_on_wordpress_playground() ) {
				include_once __DIR__ . '/views/html-notice-playground.php';
			}
		} // END wordpress_playground_notice()
	} // END class.
} // END if class exists.

return new CoCart_Admin_Notices();
