<?php
declare(strict_types=1);
namespace ConfManager\Admin;

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


use ConfManager\Traits\Singleton;
use ConfManager\Traits\HasHooks;
use ConfManager\Capabilities;

/**
 * WordPress管理画面メニューの登録とスクリプトエンキュー
 *
 * @since 1.0.0
 */
class AdminMenu {

	use Singleton;
	use HasHooks;

	/**
	 * 初期化
	 */
	protected function init(): void {
		$this->register_hooks();
	}

	/**
	 * フック登録
	 */
	protected function register_hooks(): void {
		$this->add_action( 'admin_menu', [ $this, 'register_menus' ] );
		$this->add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
		$this->add_action( 'admin_notices', [ $this, 'maybe_show_unpriced_fee_notice' ] );
		$this->add_action( 'admin_post_' . self::UNPRICED_NOTICE_DISMISS_ACTION, [ $this, 'handle_dismiss_unpriced_fee_notice' ] );
		$this->add_action( 'admin_notices', [ $this, 'maybe_show_audit_retention_notice' ] );
		$this->add_action( 'admin_post_' . self::AUDIT_NOTICE_DISMISS_ACTION, [ $this, 'handle_dismiss_audit_retention_notice' ] );
	}

	/**
	 * 監査ログの間引きが始まることを知らせたかどうかの option
	 */
	private const AUDIT_NOTICE_OPTION = 'conf_manager_audit_retention_notice_dismissed';

	/**
	 * その知らせを閉じる admin-post アクション名
	 */
	private const AUDIT_NOTICE_DISMISS_ACTION = 'conf_manager_dismiss_audit_retention_notice';

	/**
	 * 「価格が未設定の区分がある催し」の知らせを閉じたかどうかの option
	 */
	private const UNPRICED_NOTICE_OPTION = 'conf_manager_unpriced_fee_notice_dismissed';

	/**
	 * その知らせを閉じる admin-post アクション名
	 */
	private const UNPRICED_NOTICE_DISMISS_ACTION = 'conf_manager_dismiss_unpriced_fee_notice';

	/**
	 * 知らせに並べる催し名の上限
	 */
	private const UNPRICED_NOTICE_MAX_NAMES = 10;

	/**
	 * 「価格が未設定の区分がある催し」を知らせる
	 *
	 * v1.5.0 から**料金表に行が無い区分は申込を受け付けない**。それまでは
	 * ¥0・即確定で通っていたので、行を空欄にしたまま無料で運用していた催しが
	 * ありうる。移行が拾えるのは**実績のある組み合わせだけ**なので、
	 * まだ申込の無い区分は気づけない。ここで一覧にして気づけるようにする。
	 *
	 * 📌 出すのは「受付中（published）で、標準の区分に抜けがある催し」だけ。
	 *    終わった催しや下書きを並べても直しようがない。
	 */
	public function maybe_show_unpriced_fee_notice(): void {
		if ( ! current_user_can( 'conf_manager_edit_events' ) ) {
			return;
		}

		// このプラグインの画面でだけ調べる。**全管理画面で走らせない。**
		// 直す場所（イベント編集）はこのメニューの中なので、出す場所もここでよい。
		// ⚠️ `get_current_screen()` は admin_notices より前に張られる場面でも
		//    null になりうるので、取れないときは出さない（＝勝手に走らせない）。
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen instanceof \WP_Screen || ! str_contains( (string) $screen->id, 'conferencemanager' ) ) {
			return;
		}

		if ( get_option( self::UNPRICED_NOTICE_OPTION, '' ) ) {
			return;
		}

		$affected = self::events_with_unpriced_categories();
		if ( empty( $affected ) ) {
			return;
		}

		$list = self::describe_affected( $affected );

		echo '<div class="notice notice-warning">';
		printf(
			'<p><strong>Conference Manager:</strong> %s</p>',
			esc_html__( 'Some published events do not have a price set for every fee category. Members in those categories can no longer register under them — they will be offered the categories that do have a price. Open the event and either enter a price, tick "Free", or remove the row with × if you do not use it.', 'conferencemanager' )
		);
		printf( '<p>%s</p>', esc_html( $list ) );

		printf(
			'<p><a href="%s">%s</a></p>',
			esc_url( admin_url( 'admin.php?page=conferencemanager#/events' ) ),
			esc_html__( 'Open Events', 'conferencemanager' )
		);

		// 状態を変える操作なので GET リンクにはしない（先読みで通ってしまう）。
		printf(
			'<form method="post" action="%s"><input type="hidden" name="action" value="%s" />',
			esc_url( admin_url( 'admin-post.php' ) ),
			esc_attr( self::UNPRICED_NOTICE_DISMISS_ACTION )
		);
		wp_nonce_field( self::UNPRICED_NOTICE_DISMISS_ACTION );
		printf(
			'<p><button type="submit" class="button-link">%s</button></p></form>',
			esc_html__( 'I have checked these events — stop showing this', 'conferencemanager' )
		);
		echo '</div>';
	}

	/**
	 * 知らせに並べる催し名の文字列を作る
	 *
	 * 多い場合は先頭だけ出して残りは件数で言う。⚠️ **黙って切らない**
	 * （切ったことを書かないと「これで全部」と読まれる）。
	 *
	 * @param string[] $titles 対象の催し名
	 */
	private static function describe_affected( array $titles ): string {
		$titles = array_map( 'wp_strip_all_tags', $titles );
		$shown  = array_slice( $titles, 0, self::UNPRICED_NOTICE_MAX_NAMES );
		$rest   = count( $titles ) - count( $shown );
		$list   = implode( ' / ', $shown );

		if ( $rest > 0 ) {
			$list .= ' ' . sprintf(
				/* translators: %d: number of further events not listed by name */
				_n( 'and %d more', 'and %d more', $rest, 'conferencemanager' ),
				$rest
			);
		}

		return $list;
	}

	/**
	 * 知らせを閉じる
	 *
	 * ⚠️ 一度閉じると、後から別の催しで同じ状態を作っても知らせは出ない。
	 * そのときは wp_options の conf_manager_unpriced_fee_notice_dismissed を消すこと。
	 */
	public function handle_dismiss_unpriced_fee_notice(): void {
		// ⚠️ **admin-post.php は $_REQUEST['action'] だけを見てここへ来る＝GET でも届く。**
		// フォームを POST にしても入口は塞がっていない。
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 方式の判定のみ。nonce は下で check_admin_referer() が検証する。
		$request_method = isset( $_SERVER['REQUEST_METHOD'] )
			? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) )
			: '';
		if ( 'POST' !== $request_method ) {
			wp_die(
				esc_html__( 'This action must be submitted with the button in the warning.', 'conferencemanager' ),
				'',
				[ 'response' => 405 ]
			);
		}

		if ( ! current_user_can( 'conf_manager_edit_events' ) ) {
			wp_die(
				esc_html__( 'You do not have permission to do this.', 'conferencemanager' ),
				'',
				[ 'response' => 403 ]
			);
		}

		check_admin_referer( self::UNPRICED_NOTICE_DISMISS_ACTION );

		update_option( self::UNPRICED_NOTICE_OPTION, '1' );

		wp_safe_redirect( admin_url( 'admin.php?page=conferencemanager#/events' ) );
		exit;
	}

	/**
	 * 標準の区分に価格の抜けがある公開中の催しを返す
	 *
	 * 「抜け」は 2 通りあり、**どちらも申し込めない**ので両方拾う:
	 *   1. 区分の行そのものが無い（料金表から ✖️ で外した／最初から入れていない）
	 *   2. 行はあるが `onsite` も `online` も入っていない（セルだけ全部空にした）
	 *
	 * ⚠️ `search()` は 1 頁 100 件で頭打ちなので**頁を送って全件見る**。
	 * 🔴 **途中で打ち切らないこと。** 打ち切ると、抜けのある催しが打ち切りの外側に
	 *    あるときに**何も出ない＝「問題なし」に見える**。走らせるのはこのプラグインの
	 *    画面だけなので（`maybe_show_unpriced_fee_notice()` の画面判定）、全件見てよい。
	 *
	 * @return string[] 抜けのある催しの名前
	 */
	public static function events_with_unpriced_categories(): array {
		$standard = [ 'regular', 'student', 'associate', 'honorary', 'general' ];
		$per_page = 100;
		$out      = [];
		$page     = 1;

		do {
			$events = \ConfManager\Events\EventManager::instance()->search( [
				'status'   => 'published',
				'orderby'  => 'date_start',
				'order'    => 'DESC',
				'page'     => $page,
				'per_page' => $per_page,
			] );

			$items = (array) ( $events['items'] ?? [] );

			foreach ( $items as $event ) {
				$rules = $event['fee_rules'] ?? [];
				unset( $rules['_labels'] );

				// 料金表が丸ごと空の催しは「無料イベント」＝従来どおり通るので対象外
				if ( empty( $rules ) ) {
					continue;
				}

				foreach ( $standard as $key ) {
					if ( ! self::fee_category_is_priced( $rules, $key ) ) {
						$out[] = (string) ( $event['title'] ?? '' );
						break;
					}
				}
			}

			++$page;
		} while ( count( $items ) === $per_page );

		return $out;
	}

	/**
	 * その区分に値段が入っているか（行があり、参加形態が 1 つ以上あること）
	 *
	 * ⚠️ 行が `{}` のままでも申し込めない。`isset()` だけ見ると見逃す。
	 *
	 * @param array  $rules 料金表（`_labels` を除いたもの）
	 * @param string $key   料金区分キー
	 */
	private static function fee_category_is_priced( array $rules, string $key ): bool {
		if ( ! isset( $rules[ $key ] ) || ! is_array( $rules[ $key ] ) ) {
			return false;
		}

		$cells = array_intersect_key( $rules[ $key ], [ 'onsite' => 1, 'online' => 1 ] );

		return ! empty( $cells );
	}

	/**
	 * 監査ログの間引きが始まることを、始まる前に知らせる
	 *
	 * 🔴 **WP.org 配布では「更新前に申し出てください」が届かない。**
	 * 自動更新が有効なサイトでは利用者が何も読まないうちに更新が当たり、
	 * 反応があったときにはもう消えている。だから**消える前に**知らせる。
	 *
	 * 出すのは猶予の間だけ（{@see \ConfManager\Security\AuditLogCron::is_in_grace()}）。
	 * ⚠️ **閉じられるようにする。** 閉じられない警告を全サイトに出す型は過去に踏んでいる。
	 * ⚠️ **このプラグインの画面でだけ**出す（全管理画面で走らせない）。
	 */
	public function maybe_show_audit_retention_notice(): void {
		if ( ! current_user_can( 'conf_manager_manage_settings' ) ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen instanceof \WP_Screen || ! str_contains( (string) $screen->id, 'conferencemanager' ) ) {
			return;
		}

		if ( get_option( self::AUDIT_NOTICE_OPTION, '' ) ) {
			return;
		}

		if ( ! \ConfManager\Security\AuditLogCron::is_in_grace() ) {
			return;
		}

		$starts_at = \ConfManager\Security\AuditLogCron::grace_ends_at();

		echo '<div class="notice notice-info">';
		printf(
			'<p><strong>Conference Manager:</strong> %s</p>',
			esc_html(
				sprintf(
					/* translators: %s: date on which trimming of the audit log starts */
					__( 'From %s this plugin starts trimming its audit log. General activity is kept for one year and security records for three years; the IP address and browser recorded with general activity are cleared after 90 days. Nothing has been removed yet. If you need the records kept for longer, change the retention with a filter before that date.', 'conferencemanager' ),
					date_i18n( get_option( 'date_format' ), $starts_at )
				)
			)
		);

		// 状態を変える操作なので GET リンクにはしない（先読みで通ってしまう）。
		printf(
			'<form method="post" action="%s"><input type="hidden" name="action" value="%s" />',
			esc_url( admin_url( 'admin-post.php' ) ),
			esc_attr( self::AUDIT_NOTICE_DISMISS_ACTION )
		);
		wp_nonce_field( self::AUDIT_NOTICE_DISMISS_ACTION );
		printf(
			'<p><button type="submit" class="button-link">%s</button></p></form>',
			esc_html__( 'Understood — stop showing this', 'conferencemanager' )
		);
		echo '</div>';
	}

	/**
	 * 監査ログの知らせを閉じる
	 */
	public function handle_dismiss_audit_retention_notice(): void {
		// ⚠️ **admin-post.php は $_REQUEST['action'] だけを見てここへ来る＝GET でも届く。**
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 方式の判定のみ。nonce は下で check_admin_referer() が検証する。
		$request_method = isset( $_SERVER['REQUEST_METHOD'] )
			? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) )
			: '';
		if ( 'POST' !== $request_method ) {
			wp_die(
				esc_html__( 'This action must be submitted with the button in the notice.', 'conferencemanager' ),
				'',
				[ 'response' => 405 ]
			);
		}

		if ( ! current_user_can( 'conf_manager_manage_settings' ) ) {
			wp_die(
				esc_html__( 'You do not have permission to do this.', 'conferencemanager' ),
				'',
				[ 'response' => 403 ]
			);
		}

		check_admin_referer( self::AUDIT_NOTICE_DISMISS_ACTION );

		update_option( self::AUDIT_NOTICE_OPTION, '1' );

		wp_safe_redirect( admin_url( 'admin.php?page=conferencemanager' ) );
		exit;
	}

	/**
	 * 管理画面メニューを登録
	 */
	public function register_menus(): void {
		add_menu_page(
			__( 'Conference Manager', 'conferencemanager' ),
			__( 'Conference', 'conferencemanager' ),
			'conf_manager_access',
			'conferencemanager',
			[ $this, 'render_admin_page' ],
			'dashicons-groups',
			26
		);
	}

	/**
	 * 管理画面のReactアプリのマウントポイントを出力
	 */
	public function render_admin_page(): void {
		echo '<div id="conf-manager-admin-app"></div>';
	}

	/**
	 * 管理画面用スクリプト・スタイルをエンキュー
	 *
	 * @param string $hook_suffix 現在の管理画面フックサフィックス
	 */
	public function enqueue_scripts( string $hook_suffix ): void {
		if ( 'toplevel_page_conferencemanager' !== $hook_suffix ) {
			return;
		}

		$asset_file = CONF_MANAGER_PLUGIN_DIR . 'build/admin-app.asset.php';
		$asset      = file_exists( $asset_file )
			? require $asset_file
			: [ 'dependencies' => [], 'version' => CONF_MANAGER_VERSION ];

		wp_enqueue_script(
			'conf-manager-admin',
			CONF_MANAGER_PLUGIN_URL . 'build/admin-app.js',
			$asset['dependencies'],
			$asset['version'],
			true
		);

		wp_set_script_translations(
			'conf-manager-admin',
			'conferencemanager',
			CONF_MANAGER_PLUGIN_DIR . 'languages'
		);

		wp_enqueue_style(
			'conf-manager-admin',
			CONF_MANAGER_PLUGIN_URL . 'admin/css/admin.css',
			[ 'wp-components' ],
			CONF_MANAGER_VERSION
		);

		wp_localize_script( 'conf-manager-admin', 'confManagerCaps', [
			'caps' => Capabilities::get_current_user_caps(),
		] );

		// 会員一覧の「いまの承認の運用」を、実際の設定で出し分けるために渡す。
		//
		// 🔴 REST（`/admin/system-status`）から取らせないこと。あの経路は
		// `conf_manager_manage_settings` を要求するが、**「学会編集者」（conf_editor）は
		// 会員を編集できるのに設定権限を持たない**ので、会員管理は開けるのに
		// 案内だけ 403 になる（`includes/class-capabilities.php` の conf_editor に
		// manage_settings は無い）。localize なら追加リクエストも権限の穴も無い。
		//
		// 📌 wp_localize_script は真偽値を "1" / "" にして渡す。JS 側は
		//    `!! window.confManagerSettings?.require_approval` で読む（"" は falsy）。
		// 会員種別の表示に前置する学会の略称（未設定なら空＝何も付かない）。
		// 🔴 公開側（public/class-public-controller.php）にも同じものを渡すこと。
		wp_localize_script(
			'conf-manager-admin',
			'confManagerMemberType',
			\ConfManager\Members\MemberTypeLabel::script_data()
		);

		wp_localize_script( 'conf-manager-admin', 'confManagerSettings', [
			'require_approval' => (bool) get_option( 'conf_manager_require_approval', false ),
		] );
	}
}
