<?php
/**
 * AG WP Plugin Install Manager — 管理画面クラス。
 *
 * 管理メニューの登録・ページのレンダリング・
 * ダッシュボード通知の表示を担う。
 *
 * @package AG_WP_Plugin_Install_Manager
 */

// WordPress 外からの直接アクセスを禁止。
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'AG_WP_Plugin_Admin' ) ) {

	/**
	 * 管理画面・通知を担うクラス。
	 *
	 * @since 1.0.0
	 */
	class AG_WP_Plugin_Admin {

		/**
		 * コアクラスのインスタンス。
		 *
		 * @since 1.0.0
		 * @var AG_WP_Plugin_Install_Manager
		 */
		protected $manager;

		/**
		 * コンストラクタ。
		 *
		 * @since 1.0.0
		 *
		 * @param AG_WP_Plugin_Install_Manager $manager コアクラスのインスタンス。
		 */
		public function __construct( AG_WP_Plugin_Install_Manager $manager ) {
			$this->manager = $manager;
		}

		/**
		 * WordPressフックに登録する。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function hooks() {
			add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
			add_action( 'admin_notices', array( $this, 'show_notices' ) );
			add_action( 'admin_init', array( $this, 'handle_dismiss' ) );
			add_action( 'admin_init', array( $this, 'dispatch_actions' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
		}

		/**
		 * admin_init のタイミングでアクションを検出し、該当する場合は即座に処理を開始する。
		 *
		 * admin_notices よりも前に実行されるため、出力前にページ全体を制御できる。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function dispatch_actions() {
			$config = $this->manager->get_config();

			// 自分のページへのリクエストか確認する。
			$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification

			if ( $page !== $config['menu_slug'] ) {
				return;
			}

			// 単体アクション（GET: install / activate）。
			$action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
			$slug   = isset( $_GET['plugin'] ) ? sanitize_key( $_GET['plugin'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification

			if ( ! empty( $action ) && ! empty( $slug ) ) {
				if ( ! in_array( $action, array( 'install', 'activate', 'install-activate' ), true ) ) {
					return;
				}

				// 登録済みプラグインか確認する。
				$plugins = $this->manager->get_plugins();
				if ( ! isset( $plugins[ $slug ] ) ) {
					return;
				}

				// 権限チェック。
				if ( ! current_user_can( $config['capability'] ) ) {
					wp_die( esc_html__( 'このページにアクセスする権限がありません。', AG_WPIM_TEXTDOMAIN ) );
				}

				// この時点ではまだ何も出力されていないので、ページ全体を乗っ取って処理する。
				if ( 'install' === $action ) {
					$this->handle_install( $slug, $plugins[ $slug ] );
				} elseif ( 'activate' === $action ) {
					$this->handle_activate( $slug );
				} elseif ( 'install-activate' === $action ) {
					$this->handle_install_activate( $slug, $plugins[ $slug ] );
				}
				return;
			}

			// バルクアクション（POST: install / activate）。
			$bulk_action = '-1';
			if ( isset( $_POST['action'] ) && '-1' !== $_POST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification
				$bulk_action = sanitize_key( wp_unslash( $_POST['action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			} elseif ( isset( $_POST['action2'] ) && '-1' !== $_POST['action2'] ) { // phpcs:ignore WordPress.Security.NonceVerification
				$bulk_action = sanitize_key( wp_unslash( $_POST['action2'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			}

			if ( '-1' === $bulk_action || ! in_array( $bulk_action, array( 'install', 'activate', 'install-activate' ), true ) ) {
				return;
			}

			// ノンス検証。
			$nonce = isset( $_POST['ag_wpim_nonce'] ) ? sanitize_key( wp_unslash( $_POST['ag_wpim_nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
			if ( ! wp_verify_nonce( $nonce, 'ag-wpim-bulk-action' ) ) {
				wp_die( esc_html__( 'セキュリティチェックに失敗しました。', AG_WPIM_TEXTDOMAIN ) );
			}

			// 権限チェック。
			if ( ! current_user_can( $config['capability'] ) ) {
				wp_die( esc_html__( 'このページにアクセスする権限がありません。', AG_WPIM_TEXTDOMAIN ) );
			}

			// 登録済みプラグインのスラッグのみを対象とする（未登録スラッグを排除）。
			$plugins    = $this->manager->get_plugins();
			$raw_slugs  = isset( $_POST['plugins'] ) ? (array) wp_unslash( $_POST['plugins'] ) : array();
			$slugs      = array_values(
				array_filter(
					array_map( 'sanitize_key', $raw_slugs ),
					function ( $s ) use ( $plugins ) {
						return isset( $plugins[ $s ] );
					}
				)
			);

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

			if ( 'install' === $bulk_action ) {
				$this->handle_bulk_install( $slugs );
			} elseif ( 'activate' === $bulk_action ) {
				$this->handle_bulk_activate( $slugs );
			} elseif ( 'install-activate' === $bulk_action ) {
				$this->handle_bulk_install_activate( $slugs );
			}
		}

		/**
		 * プラグインをインストールする。
		 *
		 * WordPress 標準の update.php へリダイレクトしてインストールを実行する。
		 * 完了後は管理ページへ戻る。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug   プラグインスラッグ。
		 * @param array  $plugin プラグイン設定。
		 * @return void
		 */
		protected function handle_install( $slug, $plugin ) {
			// ノンス検証。
			if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'ag-wpim-install-' . $slug ) ) {
				wp_die( esc_html__( 'セキュリティチェックに失敗しました。', AG_WPIM_TEXTDOMAIN ) );
			}

			$config       = $this->manager->get_config();
			$redirect_url = admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] );

			// バンドルまたは外部URLの場合は独自インストール処理を使う。
			if ( ! empty( $plugin['source'] ) ) {
				$this->run_upgrader( $slug, $plugin['source'], $redirect_url );
				return;
			}

			// WordPress.org のプラグインは update.php 標準フローへリダイレクトする。
			// update.php は install-plugin_{slug} というノンスを自前で検証するので
			// ここで wp_nonce_url() は使わず、wp_create_nonce() で直接生成して渡す。
			$install_url = add_query_arg(
				array(
					'action'      => 'install-plugin',
					'plugin'      => $slug,
					'redirect_to' => urlencode( $redirect_url ),
					'_wpnonce'    => wp_create_nonce( 'install-plugin_' . $slug ),
				),
				admin_url( 'update.php' )
			);

			wp_safe_redirect( $install_url );
			exit;
		}

		/**
		 * プラグインをインストールして有効化する。
		 *
		 * WordPress.org プラグインも plugins_api() でダウンロードURLを取得して
		 * Plugin_Upgrader で直接インストールし、完了後に有効化する。
		 *
		 * @since 1.1.0
		 *
		 * @param string $slug   プラグインスラッグ。
		 * @param array  $plugin プラグイン設定。
		 * @return void
		 */
		protected function handle_install_activate( $slug, $plugin ) {
			// ノンス検証。
			if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'ag-wpim-install-activate-' . $slug ) ) {
				wp_die( esc_html__( 'セキュリティチェックに失敗しました。', AG_WPIM_TEXTDOMAIN ) );
			}

			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

			$config       = $this->manager->get_config();
			$redirect_url = admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] );

			// ソース解決: バンドル・外部URL、または WordPress.org から取得。
			$source = '';
			if ( ! empty( $plugin['source'] ) ) {
				$source = $plugin['source'];
			} else {
				$api = plugins_api(
					'plugin_information',
					array(
						'slug'   => $slug,
						'fields' => array( 'sections' => false ),
					)
				);
				if ( ! is_wp_error( $api ) && ! empty( $api->download_link ) ) {
					$source = $api->download_link;
				}
			}

			if ( empty( $source ) ) {
				wp_die( esc_html__( 'プラグインのダウンロードURLを取得できませんでした。', AG_WPIM_TEXTDOMAIN ) );
			}

			global $title, $parent_file, $submenu_file, $hook_suffix;
			$title        = __( 'プラグインのインストール', AG_WPIM_TEXTDOMAIN );
			$parent_file  = $config['parent_slug'];
			$submenu_file = $config['menu_slug'];
			if ( empty( $hook_suffix ) ) {
				$hook_suffix = sanitize_key( $config['menu_slug'] );
			}

			require_once ABSPATH . 'wp-admin/admin-header.php';

			$skin = new AG_WP_Installer_View(
				array(
					'plugin_slug'      => $slug,
					'menu_slug'        => $config['menu_slug'],
					'redirect_url'     => $redirect_url,
					'nonce'            => 'ag-wpim-install-activate-' . $slug,
					'url'              => '',
					'complete_message' => __( 'プラグインのインストール・有効化が完了しました。', AG_WPIM_TEXTDOMAIN ),
				)
			);

			$upgrader = new Plugin_Upgrader( $skin );
			$result   = $upgrader->install( $source );

			// インストール成功後に有効化する。
			if ( true === $result ) {
				if ( ! function_exists( 'get_plugins' ) ) {
					require_once ABSPATH . 'wp-admin/includes/plugin.php';
				}
				foreach ( get_plugins() as $file => $data ) {
					if ( 0 === strpos( $file, $slug . '/' ) ) {
						activate_plugin( $file );
						break;
					}
				}
			}

			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}

		/**
		 * バンドル・外部URLのプラグインをアップグレーダーで直接インストールする。
		 *
		 * WordPress.org 以外のソースはリダイレクト方式が使えないため
		 * ob_start() でバッファリングしてヘッダー衝突を回避する。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug         プラグインスラッグ。
		 * @param string $source       インストール元のURLまたはパス。
		 * @param string $redirect_url 完了後のリダイレクト先。
		 * @return void
		 */
		protected function run_upgrader( $slug, $source, $redirect_url ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

			// WordPress の update.php (install-plugin アクション) と同じ方式で
			// 管理ページを構成する: グローバル変数を設定してから admin-header.php を読み込む。
			$config = $this->manager->get_config();

			// admin_init 時点では admin_menu がまだ発火しておらず $hook_suffix が未設定のため、
			// admin-header.php の preg_replace() に null が渡るのを防ぐ。
			global $title, $parent_file, $submenu_file, $hook_suffix;
			$title        = __( 'プラグインのインストール', AG_WPIM_TEXTDOMAIN );
			$parent_file  = $config['parent_slug'];
			$submenu_file = $config['menu_slug'];
			if ( empty( $hook_suffix ) ) {
				$hook_suffix = sanitize_key( $config['menu_slug'] );
			}

			require_once ABSPATH . 'wp-admin/admin-header.php';

			$skin = new AG_WP_Installer_View(
				array(
					'plugin_slug'  => $slug,
					'menu_slug'    => $config['menu_slug'],
					'redirect_url' => $redirect_url,
					'nonce'        => 'ag-wpim-install-' . $slug,
					'url'          => '',
				)
			);

			$upgrader = new Plugin_Upgrader( $skin );
			$upgrader->install( $source );

			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}

		/**
		 * 複数プラグインをまとめてインストールする。
		 *
		 * WordPress.org 以外のソースはプラグイン設定の source から、
		 * WordPress.org プラグインは plugins_api() でダウンロードURLを取得してインストールする。
		 *
		 * @since 1.0.0
		 *
		 * @param string[] $slugs インストール対象のプラグインスラッグ一覧。
		 * @return void
		 */
		protected function handle_bulk_install( array $slugs ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

			$config       = $this->manager->get_config();
			$redirect_url = admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] );
			$plugins      = $this->manager->get_plugins();

			// ソース（インストール元URL/パス）を解決する。
			$sources = array();
			foreach ( $slugs as $slug ) {
				if ( ! isset( $plugins[ $slug ] ) ) {
					continue;
				}
				$plugin = $plugins[ $slug ];

				if ( ! empty( $plugin['source'] ) ) {
					// バンドルまたは外部URL。
					$sources[ $slug ] = $plugin['source'];
				} else {
					// WordPress.org からダウンロードURLを取得する。
					$api = plugins_api(
						'plugin_information',
						array(
							'slug'   => $slug,
							'fields' => array( 'sections' => false ),
						)
					);
					if ( ! is_wp_error( $api ) && ! empty( $api->download_link ) ) {
						$sources[ $slug ] = $api->download_link;
					}
				}
			}

			if ( empty( $sources ) ) {
				wp_safe_redirect( $redirect_url );
				exit;
			}

			// グローバル変数を設定してから admin-header.php を読み込む（run_upgrader() と同じパターン）。
			global $title, $parent_file, $submenu_file, $hook_suffix;
			$title        = __( 'プラグインのインストール', AG_WPIM_TEXTDOMAIN );
			$parent_file  = $config['parent_slug'];
			$submenu_file = $config['menu_slug'];
			if ( empty( $hook_suffix ) ) {
				$hook_suffix = sanitize_key( $config['menu_slug'] );
			}

			require_once ABSPATH . 'wp-admin/admin-header.php';

			$skin = new AG_WP_Bulk_Installer_View(
				array(
					'redirect_url' => $redirect_url,
					'plugin_slugs' => array_keys( $sources ),
					'menu_slug'    => $config['menu_slug'],
					'url'          => '',
					'nonce'        => '',
				)
			);

			$skin->bulk_header();

			foreach ( $sources as $slug => $source ) {
				$plugin_name = isset( $plugins[ $slug ]['name'] ) ? $plugins[ $slug ]['name'] : $slug;
				$skin->before( $plugin_name );
				$upgrader = new Plugin_Upgrader( $skin );
				$upgrader->install( $source );
				$skin->after( $plugin_name );
			}

			$skin->bulk_footer();

			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}

		/**
		 * 複数プラグインをまとめて有効化する。
		 *
		 * @since 1.0.0
		 *
		 * @param string[] $slugs 有効化対象のプラグインスラッグ一覧。
		 * @return void
		 */
		protected function handle_bulk_activate( array $slugs ) {
			if ( ! function_exists( 'get_plugins' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}

			$installed = get_plugins();

			foreach ( $slugs as $slug ) {
				$plugin_file = '';

				foreach ( $installed as $file => $data ) {
					if ( 0 === strpos( $file, $slug . '/' ) ) {
						$plugin_file = $file;
						break;
					}
				}

				if ( empty( $plugin_file ) ) {
					continue; // 未インストールのものはスキップ。
				}

				$result = activate_plugin( $plugin_file );

				// WP_Error の場合はスキップして次へ（バルク処理は続行）。
				if ( is_wp_error( $result ) ) {
					continue;
				}
			}

			$config = $this->manager->get_config();
			wp_safe_redirect( admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] ) );
			exit;
		}

		/**
		 * 複数プラグインをまとめてインストールして有効化する。
		 *
		 * @since 1.1.0
		 *
		 * @param string[] $slugs インストールおよび有効化対象のプラグインスラッグ一覧。
		 * @return void
		 */
		protected function handle_bulk_install_activate( array $slugs ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

			$config       = $this->manager->get_config();
			$redirect_url = admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] );
			$plugins      = $this->manager->get_plugins();

			// ソース（インストール元URL/パス）を解決する。
			$sources = array();
			foreach ( $slugs as $slug ) {
				if ( ! isset( $plugins[ $slug ] ) ) {
					continue;
				}
				$plugin = $plugins[ $slug ];

				if ( ! empty( $plugin['source'] ) ) {
					$sources[ $slug ] = $plugin['source'];
				} else {
					$api = plugins_api(
						'plugin_information',
						array(
							'slug'   => $slug,
							'fields' => array( 'sections' => false ),
						)
					);
					if ( ! is_wp_error( $api ) && ! empty( $api->download_link ) ) {
						$sources[ $slug ] = $api->download_link;
					}
				}
			}

			if ( empty( $sources ) ) {
				wp_safe_redirect( $redirect_url );
				exit;
			}

			// グローバル変数を設定してから admin-header.php を読み込む（run_upgrader() と同じパターン）。
			global $title, $parent_file, $submenu_file, $hook_suffix;
			$title        = __( 'プラグインのインストール', AG_WPIM_TEXTDOMAIN );
			$parent_file  = $config['parent_slug'];
			$submenu_file = $config['menu_slug'];
			if ( empty( $hook_suffix ) ) {
				$hook_suffix = sanitize_key( $config['menu_slug'] );
			}

			require_once ABSPATH . 'wp-admin/admin-header.php';

			$skin = new AG_WP_Bulk_Installer_View(
				array(
					'redirect_url' => $redirect_url,
					'plugin_slugs' => array_keys( $sources ),
					'menu_slug'    => $config['menu_slug'],
					'action_type'  => 'install-activate',
					'url'          => '',
					'nonce'        => '',
				)
			);

			$skin->bulk_header();

			if ( ! function_exists( 'get_plugins' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}

			foreach ( $sources as $slug => $source ) {
				$plugin_name = isset( $plugins[ $slug ]['name'] ) ? $plugins[ $slug ]['name'] : $slug;
				$skin->before( $plugin_name );
				$upgrader = new Plugin_Upgrader( $skin );
				$result   = $upgrader->install( $source );
				$skin->after( $plugin_name );

				// インストール成功後に有効化する。
				if ( true === $result ) {
					foreach ( get_plugins() as $file => $data ) {
						if ( 0 === strpos( $file, $slug . '/' ) ) {
							activate_plugin( $file );
							break;
						}
					}
				}
			}

			$skin->bulk_footer();

			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}

		/**
		 * プラグインを有効化する。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug プラグインスラッグ。
		 * @return void
		 */
		protected function handle_activate( $slug ) {
			// ノンス検証。
			if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'ag-wpim-activate-' . $slug ) ) {
				wp_die( esc_html__( 'セキュリティチェックに失敗しました。', AG_WPIM_TEXTDOMAIN ) );
			}

			if ( ! function_exists( 'get_plugins' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}

			// スラッグからプラグインファイルを特定する。
			$plugin_file = '';
			foreach ( get_plugins() as $file => $data ) {
				if ( 0 === strpos( $file, $slug . '/' ) ) {
					$plugin_file = $file;
					break;
				}
			}

			if ( empty( $plugin_file ) ) {
				wp_die( esc_html__( 'プラグインが見つかりません。', AG_WPIM_TEXTDOMAIN ) );
			}

			$result = activate_plugin( $plugin_file );

			if ( is_wp_error( $result ) ) {
				wp_die( esc_html( $result->get_error_message() ) );
			}

			$config = $this->manager->get_config();
			wp_safe_redirect( admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] ) );
			exit;
		}

		/**
		 * アップグレード処理用のページを独自にレンダリングする。
		 *
		 * admin_init のタイミングで呼ばれるため iframe_header() は使えない。
		 * WordPress の管理画面ヘッダー・フッターを直接出力してページを構成する。
		 *
		 * @since 1.0.0
		 *
		 * @param callable $callback インストール処理のコールバック。
		 * @return void
		 */
		protected function render_upgrader_page( $callback ) {
			// WordPress 管理画面のヘッダーを出力する。
			require_once ABSPATH . 'wp-admin/admin-header.php';

			echo '<div class="wrap">';
			echo '<h1>' . esc_html__( 'プラグインのインストール', AG_WPIM_TEXTDOMAIN ) . '</h1>';

			call_user_func( $callback );

			echo '</div>';

			// WordPress 管理画面のフッターを出力する。
			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}

		/**
		 * 管理メニューを登録する。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function add_admin_menu() {
			$config = $this->manager->get_config();

			add_submenu_page(
				$config['parent_slug'],
				__( '推奨プラグイン', AG_WPIM_TEXTDOMAIN ),
				__( '推奨プラグイン', AG_WPIM_TEXTDOMAIN ),
				$config['capability'],
				$config['menu_slug'],
				array( $this, 'render_page' )
			);
		}

		/**
		 * 管理ページをレンダリングする。
		 *
		 * アクション処理は dispatch_actions() で admin_init のタイミングに移譲済み。
		 * ここではプラグイン一覧の表示のみを行う。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function render_page() {
			$list_table = new AG_WP_Plugin_List_Table( $this->manager );
			$list_table->prepare_items();
			?>
			<div class="wrap ag-wpim-wrap">
				<h1><?php esc_html_e( '推奨プラグイン', AG_WPIM_TEXTDOMAIN ); ?></h1>
				<p class="ag-wpim-intro">
					<?php esc_html_e( '以下のプラグインをインストール・有効化してください。', AG_WPIM_TEXTDOMAIN ); ?>
				</p>
				<form method="post">
					<?php
					$list_table->display();
					wp_nonce_field( 'ag-wpim-bulk-action', 'ag_wpim_nonce' );
					?>
				</form>
			</div>
			<?php
		}

		/**
		 * 管理通知を表示する。
		 *
		 * 未インストール・未有効化のプラグインがある場合のみ表示。
		 * 通知を非表示にした場合、またはインストール処理中は表示しない。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function show_notices() {
			$config = $this->manager->get_config();

			// has_notices が false なら通知を表示しない。
			if ( empty( $config['has_notices'] ) ) {
				return;
			}

			// 権限チェック。
			if ( ! current_user_can( $config['capability'] ) ) {
				return;
			}

			// インストール・有効化アクション中は通知を表示しない（出力衝突を防ぐ）。
			$page   = isset( $_GET['page'] )   ? sanitize_key( $_GET['page'] )   : ''; // phpcs:ignore WordPress.Security.NonceVerification
			$action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
			if ( $page === $config['menu_slug'] && in_array( $action, array( 'install', 'activate' ), true ) ) {
				return;
			}

			// 非表示フラグが立っていたら表示しない。
			if ( $config['dismissable'] && $this->is_dismissed() ) {
				return;
			}

			$missing  = $this->manager->get_plugins_by_status( 'missing' );
			$inactive = $this->manager->get_plugins_by_status( 'inactive' );
			$all      = array_merge( $missing, $inactive );

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

			// parent_slug を使って正しい管理URLを生成する（themes.php / plugins.php）。
			$url     = admin_url( $config['parent_slug'] . '?page=' . $config['menu_slug'] );
			$dismiss = wp_nonce_url(
				add_query_arg( 'ag-wpim-dismiss', '1' ),
				'ag-wpim-dismiss'
			);

			// 未対応プラグイン名の一覧を生成する。
			$plugin_names = array_map(
				function ( $plugin ) {
					return esc_html( $plugin['name'] );
				},
				$all
			);

			?>
			<div class="notice notice-warning ag-wpim-notice">
				<p><?php echo esc_html__( '推奨プラグインがインストールされていない、または有効化されていません。', AG_WPIM_TEXTDOMAIN );?></p>
				<p><?php echo esc_html( implode( '、', $plugin_names ) ); ?></p>
				<?php
					echo '<a class="button button-primary" href="' . esc_url( $url ) . '">' . esc_html__( '推奨プラグインを管理する', AG_WPIM_TEXTDOMAIN ) . '</a>';
				?>
				<?php if ( $config['dismissable'] ) : ?>
					<p style="text-align:right;">
						<a href="<?php echo esc_url( $dismiss ); ?>" class="ag-wpim-dismiss-link">
							<?php esc_html_e( 'このメッセージを非表示にする', AG_WPIM_TEXTDOMAIN ); ?>
						</a>
					</p>
				<?php endif; ?>
			</div>
			<?php
		}

		/**
		 * 通知の非表示リクエストを処理する。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function handle_dismiss() {
			if ( ! isset( $_GET['ag-wpim-dismiss'] ) ) {
				return;
			}

			if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'ag-wpim-dismiss' ) ) {
				return;
			}

			set_transient( $this->get_dismiss_transient_key(), true, 0 );
			wp_safe_redirect( remove_query_arg( array( 'ag-wpim-dismiss', '_wpnonce' ) ) );
			exit;
		}

		/**
		 * 管理画面用のスタイルを読み込む。
		 *
		 * @since 1.0.0
		 *
		 * @param string $hook_suffix 現在の管理ページのフックサフィックス。
		 * @return void
		 */
		public function enqueue_styles( $hook_suffix ) {
			$config = $this->manager->get_config();

			// parent_slug に応じてフックサフィックスのプレフィックスを決定する。
			// themes.php  → appearance_page_{slug}
			// plugins.php → settings_page_{slug} または plugins_page_{slug}
			// admin_page_{slug} はどちらでも使われることがあるため両方チェックする。
			$expected = array(
				'appearance_page_' . $config['menu_slug'],
				'plugins_page_' . $config['menu_slug'],
				'admin_page_' . $config['menu_slug'],
			);

			if ( ! in_array( $hook_suffix, $expected, true ) ) {
				return;
			}

			wp_enqueue_style(
				'ag-wpim-admin',
				AG_WPIM_URL . '/assets/css/admin.css',
				array(),
				AG_WPIM_VERSION
			);
		}

		// =========================================================
		// 内部ヘルパー
		// =========================================================

		/**
		 * dismiss 用の Transient キーを返す。
		 * version が設定されている場合はキーに含め、バージョン変更時に通知が再表示されるようにする。
		 *
		 * @since 1.0.0
		 * @return string
		 */
		protected function get_dismiss_transient_key() {
			$config    = $this->manager->get_config();
			$menu_slug = sanitize_key( $config['menu_slug'] );
			$base      = 'ag_wpim_dismissed_' . $menu_slug . '_' . get_current_user_id();
			$version   = ! empty( $config['version'] )
				? '_' . sanitize_key( str_replace( '.', '-', $config['version'] ) )
				: '';
			return $base . $version;
		}

		/**
		 * 現在のユーザーが通知を非表示にしているかどうかを返す。
		 *
		 * @since 1.0.0
		 * @return bool
		 */
		protected function is_dismissed() {
			return (bool) get_transient( $this->get_dismiss_transient_key() );
		}
	}
}