<?php
/**
 * GravityView Admin Notices class.
 *
 * PSR-4 migration of the legacy GravityView_Admin_Notices class.
 *
 * @package GravityKit\GravityView\Admin
 * @license GPL2+
 * @since 3.0.0
 */

namespace GravityKit\GravityView\Admin;

use Exception;
use GravityKitFoundation;
use GVCommon;

use function gravityview;
use function gravityview_get_floaty;
use function gravityview_sanitize_html_class;

/**
 * Handles displaying and storing of admin notices for GravityView.
 *
 * @deprecated 3.0.0 Use GravityKitFoundation::notices() directly to add notices.
 *
 * @since      1.12
 * @since 3.0.0 Migrated to PSR-4 namespace.
 */
class Notices {
	/**
	 * @var array
	 */
	private static $admin_notices = [];

	private static $dismissed_notices = [];

	/**
	 * @var array Notices waiting to be registered with Foundation after init.
	 */
	private static $deferred_notices = [];

	/**
	 * @var bool Whether init hook has been registered
	 */
	private static $init_hooked = false;

	/**
	 * Processes notices that were deferred before Foundation was available.
	 *
	 * @internal
	 *
	 * @since 2.45
	 *
	 * @return void
	 */
	public static function process_deferred_notices() {
		if ( empty( self::$deferred_notices ) ) {
			return;
		}

		foreach ( self::$deferred_notices as $notice ) {
			self::register_foundation_notice( $notice );
		}

		self::$deferred_notices = [];
	}

	/**
	 * Clear out the dismissed notices when the plugin gets activated
	 *
	 * @deprecated 3.0.0 Use GravityKitFoundation::notices() to register and manage notices.
	 *
	 * @since      1.15.1
	 *
	 * @see        register_activation_hook
	 *
	 * @return void
	 */
	public static function flush_dismissed_notices() {
		delete_transient( 'gravityview_dismissed_notices' );
	}

	/**
	 * Dismisses a GravityView notice - stores the dismissed notices for 16 weeks.
	 *
	 * @deprecated 3.0.0 Use GravityKitFoundation::notices() to register and manage notices.
	 *
	 * @since      1.12
	 *
	 * @return void
	 */
	public function dismiss_notice() {
		// No dismiss sent.
		if ( empty( $_GET['gv-dismiss'] ) || empty( $_GET['notice'] ) ) {
			return;
		}

		// Invalid nonce.
		if ( ! wp_verify_nonce( $_GET['gv-dismiss'], 'dismiss' ) ) {
			return;
		}

		$notice_id = esc_attr( $_GET['notice'] );

		// don't display a message if use has dismissed the message for this version.
		$dismissed_notices = (array) get_transient( 'gravityview_dismissed_notices' );

		$dismissed_notices[] = $notice_id;

		$dismissed_notices = array_unique( $dismissed_notices );

		// Remind users after 16 weeks.
		set_transient( 'gravityview_dismissed_notices', $dismissed_notices, 16 * WEEK_IN_SECONDS );
	}

	/**
	 * Has the notice been dismissed already in the admin?
	 *
	 * If the passed notice array has a `dismiss` key, the notice is dismissable. If it's dismissable,
	 * we check against other notices that have already been dismissed.
	 *
	 * @deprecated 3.0.0 Use GravityKitFoundation::notices() to register and manage notices.
	 *
	 * @since      1.12
	 *
	 * @see        Notices::add_notice()
	 * @see        Notices::dismiss_notice()
	 *
	 * @param string $notice Notice array, set using `add_notice()`.
	 *
	 * @return boolean                   True: show notice; False: hide notice
	 */
	private function is_notice_dismissed( $notice ) {
		// There are no dismissed notices.
		if ( empty( self::$dismissed_notices ) ) {
			return false;
		}

		$is_dismissed = ! empty( $notice['dismiss'] ) && in_array( $notice['dismiss'], self::$dismissed_notices );

		return $is_dismissed ? true : false;
	}

	/**
	 * Returns admin notices.
	 *
	 * @since 1.12
	 *
	 * @return array
	 */
	public static function get_notices() {
		return self::$admin_notices;
	}

	/**
	 * Handles whether to display notices in multisite based on plugin activation status.
	 *
	 * @since 1.12
	 *
	 * @uses  \GV\Plugin::is_network_activated
	 *
	 * @return bool True: show the notices; false: don't show.
	 */
	private function check_show_multisite_notices() {
		if ( ! is_multisite() ) {
			return true;
		}

		// It's network activated but the user can't manage network plugins; they can't do anything about it.
		if ( gravityview()->plugin->is_network_activated() && ! is_main_site() ) {
			return false;
		}

		// ...or they don't have admin capabilities.
		if ( ! is_super_admin() ) {
			return false;
		}

		return true;
	}

	/**
	 * Outputs the admin notices generated by the plugin.
	 *
	 * @deprecated 3.0.0 Use GravityKitFoundation::notices() to register and manage notices.
	 *
	 * @since      1.12
	 *
	 * @uses       GVCommon::has_cap()
	 *
	 * @return void
	 */
	public function admin_notice() {
		/**
		 * Modifies the notices displayed in the admin.
		 *
		 * @filter     `gravityview/admin/notices`
		 *
		 * @deprecated 3.0.0 Use the filters provided by the GravityKitFoundation::notices() framework.
		 *
		 * @since      1.12
		 *
		 * @param array $notices Array of notices to display.
		 */
		$notices = apply_filters( 'gravityview/admin/notices', self::$admin_notices );

		if ( empty( $notices ) || ! $this->check_show_multisite_notices() ) {
			return;
		}

		// Don't display a message if use has dismissed the message for this version.
		// TODO: Use get_user_meta instead of get_transient.
		self::$dismissed_notices = isset( $_GET['show-dismissed-notices'] ) ? [] : (array) get_transient( 'gravityview_dismissed_notices' );

		$output = '';

		foreach ( $notices as $notice ) {

			// If the user doesn't have the capability to see the warning.
			if ( isset( $notice['cap'] ) && false === GVCommon::has_cap( $notice['cap'] ) ) {
				gravityview()->log->debug( 'Notice not shown because user does not have the capability to view it.', [ 'data' => $notice ] );
				continue;
			}

			if ( true === $this->is_notice_dismissed( $notice ) ) {
				gravityview()->log->debug( 'Notice not shown because the notice has already been dismissed.', [ 'data' => $notice ] );
				continue;
			}

			$output .= '<div id="message" style="position:relative" class="notice ' . gravityview_sanitize_html_class( $notice['class'] ) . '">';

			// Too cute to leave out.
			$output .= gravityview_get_floaty();

			if ( ! empty( $notice['title'] ) ) {
				$output .= '<h3>' . esc_html( $notice['title'] ) . '</h3>';
			}

			$message = isset( $notice['message'] ) ? $notice['message'] : '';

			if ( ! empty( $notice['dismiss'] ) ) {

				$dismiss = esc_attr( $notice['dismiss'] );

				$url = esc_url(
					add_query_arg(
						[
							'gv-dismiss' => wp_create_nonce( 'dismiss' ),
							'notice'     => $dismiss,
						]
					)
				);

				$align   = is_rtl() ? 'alignleft' : 'alignright';
				$message .= '<a href="' . $url . '" data-notice="' . $dismiss . '" class="' . $align . ' button button-link">' . esc_html__( 'Dismiss', 'gk-gravityview' ) . '</a></p>';
			}

			$output .= wpautop( $message );

			$output .= '<div class="clear"></div>';
			$output .= '</div>';

		}

		echo $output;

		unset( $output, $align, $message, $notices );

		// Reset the notices handler.
		self::$admin_notices = [];
	}

	/**
	 * Adds a notice to be displayed in the admin.
	 *
	 * @deprecated 3.0.0 Use GravityKitFoundation::notices() to register and manage notices.
	 *
	 * @since      1.12 Moved from {@see GravityView_Admin::add_notice() }
	 * @since      1.15.1 Allows for `cap` key, passing capability required to show the message
	 * @since      2.45 Integrates with Foundation notices framework while maintaining backward compatibility.
	 *
	 * @param array       $notice  {
	 *
	 * @type string       $class   HTML class to be used for the notice. Default: 'error'
	 * @type string       $message Notice message, not escaped. Allows HTML.
	 * @type string       $dismiss Unique key used to determine whether the notice has been dismissed. Set to false if not dismissable.
	 * @type string|array $cap     The capability or caps required for an user to see the notice
	 *                             }
	 * @return void
	 */
	public static function add_notice( $notice = [] ) {
		if ( ! isset( $notice['message'] ) ) {
			gravityview()->log->error( 'Notice not set', [ 'data' => $notice ] );

			return;
		}

		$notice['class'] = empty( $notice['class'] ) ? 'error' : $notice['class'];

		self::$admin_notices[] = $notice;
		self::register_foundation_notice( $notice );
	}


	/**
	 * Registers a notice with Foundation notices framework..
	 *
	 * @since 2.45
	 *
	 * @param array $notice Legacy notice array.
	 *
	 * @return void
	 */
	private static function register_foundation_notice( $notice ) {
		if ( ! class_exists( '\GravityKitFoundation' ) ) {
			self::$deferred_notices[] = $notice;

			if ( ! self::$init_hooked ) {
				add_action( 'gk/foundation/initialized', [ __CLASS__, 'process_deferred_notices' ] );

				self::$init_hooked = true;
			}

			return;
		}

		try {
			$notice_manager = GravityKitFoundation::notices();

			if ( ! $notice_manager ) {
				return;
			}

			$notice = array_merge(
				self::convert_notice_to_new_format( $notice ),
				[
					'screens' => $notice['screens'] ?? [ 'dashboard', 'plugins' ],
				]
			);

			$notice_manager->add_runtime( $notice );
		} catch ( Exception $e ) {
			gravityview()->log->debug( 'Failed to register notice with Foundation: ' . $e->getMessage(), [ 'notice' => $notice ] );
		}
	}

	/**
	 * Converts legacy notice object to the new Notices framework format.
	 *
	 * @since 2.45
	 *
	 * @param array $legacy_notice Legacy notice array.
	 *
	 * @return array Foundation notice array.
	 */
	private static function convert_notice_to_new_format( $legacy_notice ) {
		// Determine the appropriate namespace (text domain) for the calling plugin.
		$namespace = $legacy_notice['namespace'] ?? self::determine_plugin_namespace( $legacy_notice );

		$new_notice = [
			'namespace' => $namespace,
			'slug'      => self::generate_notice_slug( $legacy_notice, $namespace ),
			'message'   => $legacy_notice['message'],
		];

		// Map severity from legacy class names.
		$severity_map = [
			'error'   => 'error',
			'warning' => 'warning',
			'success' => 'success',
			'info'    => 'info',
			'notice'  => 'info',
			'updated' => 'success',
		];

		$class                  = isset( $legacy_notice['class'] ) ? $legacy_notice['class'] : 'error';
		$new_notice['severity'] = isset( $severity_map[ $class ] ) ? $severity_map[ $class ] : 'info';

		if ( ! empty( $legacy_notice['dismiss'] ) ) {
			$new_notice['dismissible'] = true;
			// Use the dismiss key as part of the slug for uniqueness.
			$new_notice['slug'] = $legacy_notice['dismiss'];
		} else {
			$new_notice['dismissible'] = false;
		}

		if ( ! empty( $legacy_notice['cap'] ) ) {
			$capabilities               = is_array( $legacy_notice['cap'] ) ? $legacy_notice['cap'] : [ $legacy_notice['cap'] ];
			$new_notice['capabilities'] = $capabilities;
		}

		return $new_notice;
	}

	/**
	 * Generates a unique slug for a notice.
	 *
	 * @since 2.45
	 *
	 * @param array  $notice    Legacy notice array.
	 * @param string $namespace Plugin namespace/text domain.
	 *
	 * @return string
	 */
	private static function generate_notice_slug( $notice, $namespace = 'gk-gravityview' ) {
		// If dismiss key is provided, use it (already unique per plugin).
		if ( ! empty( $notice['dismiss'] ) ) {
			return $notice['dismiss'];
		}

		// Generate slug with namespace prefix to avoid collisions between plugins.
		$message_hash     = substr( md5( $notice['message'] ), 0, 8 );
		$namespace_prefix = str_replace( [ '-', '_' ], '', $namespace );

		return $namespace_prefix . '-notice-' . $message_hash;
	}

	/**
	 * Determines the appropriate plugin namespace (text domain) for the calling plugin.
	 *
	 * @since 2.45
	 *
	 * @return string Plugin text domain or fallback to 'gk-gravityview'.
	 */
	private static function determine_plugin_namespace( $notice = null ) {
		// Check if plugin context was passed from Extension class.
		if ( $notice && isset( $notice['_calling_plugin_file'] ) ) {
			$calling_plugin_file = $notice['_calling_plugin_file'];
		} else {
			// Try to determine calling plugin using debug backtrace.
			$calling_plugin_file = self::get_calling_plugin_file();
		}

		if ( $calling_plugin_file ) {
			// Use Foundation's Core helper to get installed plugins by text domain.
			$installed_plugins = \GravityKit\GravityView\Foundation\Helpers\Core::get_installed_plugins();

			// Find plugin by plugin file path.
			foreach ( $installed_plugins as $plugin ) {
				if ( isset( $plugin['plugin_file'] ) && $plugin['plugin_file'] === $calling_plugin_file ) {
					return $plugin['text_domain'];
				}
			}
		}

		// Fallback to GravityView if we can't determine the caller.
		return 'gk-gravityview';
	}

	/**
	 * Get the plugin file that is calling add_notice().
	 *
	 * @since 2.45
	 *
	 * @return string|null Plugin file path or null if not detected.
	 */
	private static function get_calling_plugin_file() {
		$backtrace         = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 10 );
		$installed_plugins = \GravityKit\GravityView\Foundation\Helpers\Core::get_installed_plugins();

		// Look for the first caller outside this class.
		foreach ( $backtrace as $trace ) {
			if ( ! isset( $trace['file'] ) ) {
				continue;
			}

			$file = $trace['file'];

			// Skip our own class (both legacy and PSR-4 versions).
			if ( false !== strpos( $file, 'class-gravityview-admin-notices.php' ) ) {
				continue;
			}

			if ( false !== strpos( $file, 'src/Admin/Notices.php' ) ) {
				continue;
			}

			// Check if this file belongs to any registered plugin.
			foreach ( $installed_plugins as $plugin ) {
				if ( isset( $plugin['path'] ) && false !== strpos( $file, $plugin['path'] ) ) {
					return $plugin['plugin_file'] ?? null;
				}
			}
		}

		return null;
	}
}
