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

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


use ConfManager\Traits\Singleton;
use ConfManager\Security\AuditLog;
use ConfManager\Events\EventManager;
use ConfManager\Members\MemberManager;

/**
 * 参加申込管理
 *
 * 参加申込の作成・ステータス管理・料金計算・検索を行う。
 * 登録番号は自動採番（REG-{YYYYMMDD}-{連番5桁}）。
 *
 * @since 1.0.0
 */
class RegistrationManager {

	use Singleton;

	private const TABLE = 'conf_registrations';

	/**
	 * 有効なステータス
	 */
	private const VALID_STATUSES = [ 'pending', 'confirmed', 'paid', 'cancelled', 'attended' ];

	/**
	 * 有効な参加形態
	 */
	private const VALID_PARTICIPATION_TYPES = [ 'onsite', 'online' ];

	/**
	 * 有効な決済手段
	 */
	private const VALID_PAYMENT_METHODS = [ 'bank', 'stripe' ];

	/**
	 * 初期化
	 */
	protected function init(): void {}

	/**
	 * テーブル名
	 */
	private function table(): string {
		global $wpdb;
		return $wpdb->prefix . self::TABLE;
	}

	/**
	 * 参加申込を作成する
	 *
	 * @param array $data {
	 *     @type int    $event_id           イベントID（必須）
	 *     @type int    $member_id          会員ID（必須）
	 *     @type string $participation_type  参加形態 onsite|online（デフォルト: onsite）
	 *     @type string $payment_method     決済手段 bank|stripe（デフォルト: bank）
	 *     @type array  $session_options    選択セッションID配列
	 *     @type string $discount_code      割引コード
	 *     @type string $group_id           団体ID（団体申込時）
	 *     @type bool   $is_representative  代表者フラグ
	 * }
	 * @return int|\WP_Error 登録ID
	 */
	public function create( array $data ): int|\WP_Error {
		global $wpdb;

		$error = $this->validate( $data );
		if ( is_wp_error( $error ) ) {
			return $error;
		}

		// イベント存在確認
		$event = EventManager::instance()->get( absint( $data['event_id'] ) );
		if ( null === $event ) {
			return new \WP_Error( 'event_not_found', __( 'Event not found.', 'conferencemanager' ) );
		}

		// 会員存在確認
		$member = MemberManager::instance()->get( absint( $data['member_id'] ) );
		if ( null === $member ) {
			return new \WP_Error( 'member_not_found', __( 'Member not found.', 'conferencemanager' ) );
		}

		// 重複申込チェック（トランザクションでrace conditionを防止）
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control, no caching applicable.
		$wpdb->query( 'START TRANSACTION' );

		if ( $this->has_active_registration( absint( $data['event_id'] ), absint( $data['member_id'] ) ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control, no caching applicable.
			$wpdb->query( 'ROLLBACK' );
			return new \WP_Error(
				'duplicate_registration',
				__( 'This member has already registered for this event.', 'conferencemanager' )
			);
		}

		$participation_type = sanitize_text_field( $data['participation_type'] ?? 'onsite' );
		$payment_method     = sanitize_text_field( $data['payment_method'] ?? 'bank' );
		$fee_type           = sanitize_text_field( $data['fee_type'] ?? '' );

		// 料金計算（fee_type が指定されていればそちらを優先）
		$amount   = $this->calculate_amount( $event, $member, $participation_type, $data['session_options'] ?? [], $fee_type );
		$tax_rate = (float) ( $event['tax_rate'] ?? 10.0 );
		$tax      = $this->calculate_tax( $amount, $tax_rate );

		$registration_no = $this->generate_registration_no();

		// 料金区分キーとラベルを確定
		$resolved_fee_type  = '' !== $fee_type ? $fee_type : $member['member_type'];
		$fee_type_label     = self::resolve_fee_type_label( $resolved_fee_type, $event );

		// Free registrations (total of ¥0) require no payment, so they are confirmed immediately.
		// Paid registrations remain pending until payment is received.
		$is_free = ( $amount + $tax ) <= 0;

		$insert_data = [
			'registration_no'    => $registration_no,
			'event_id'           => absint( $data['event_id'] ),
			'member_id'          => absint( $data['member_id'] ),
			'participation_type' => $participation_type,
			'payment_method'     => $payment_method,
			'fee_type'           => $resolved_fee_type,
			'fee_type_label'     => $fee_type_label,
			'amount'             => $amount,
			'tax'                => $tax,
			'status'             => $is_free ? 'confirmed' : 'pending',
			'registered_at'      => current_time( 'mysql', true ),
		];

		// 振込口座名の申送り（個人名と振込名義が異なる場合の管理用メモ。任意・HTML不可）。
		$transfer_note = sanitize_text_field( $data['transfer_note'] ?? '' );
		if ( '' !== $transfer_note ) {
			$insert_data['transfer_note'] = $transfer_note;
		}

		if ( ! empty( $data['session_options'] ) ) {
			$insert_data['session_options_json'] = wp_json_encode( array_map( 'absint', $data['session_options'] ) );
		}

		if ( ! empty( $data['discount_code'] ) ) {
			$insert_data['discount_code'] = sanitize_text_field( $data['discount_code'] );
		}

		if ( ! empty( $data['group_id'] ) ) {
			$insert_data['group_id'] = sanitize_text_field( $data['group_id'] );
		}

		$insert_data['is_representative'] = ! empty( $data['is_representative'] ) ? 1 : 0;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$result = $wpdb->insert( $this->table(), $insert_data );

		if ( false === $result ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control, no caching applicable.
			$wpdb->query( 'ROLLBACK' );
			return new \WP_Error(
				'db_insert_error',
				__( 'Failed to create registration.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		$reg_id = (int) $wpdb->insert_id;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control, no caching applicable.
		$wpdb->query( 'COMMIT' );

		AuditLog::instance()->log( 'create_registration', 'registration', $reg_id, [
			'event_id'  => $data['event_id'],
			'member_id' => $data['member_id'],
			'amount'    => $amount,
		] );

		return $reg_id;
	}

	/**
	 * 参加申込を取得する
	 *
	 * @param int $id 登録ID
	 * @return array|null
	 */
	public function get( int $id ): ?array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->table()} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$id
			),
			ARRAY_A
		);

		if ( null === $row ) {
			return null;
		}

		return $this->format_row( $row );
	}

	/**
	 * 登録番号で取得する
	 *
	 * @param string $registration_no 登録番号
	 * @return array|null
	 */
	public function get_by_no( string $registration_no ): ?array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->table()} WHERE registration_no = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$registration_no
			),
			ARRAY_A
		);

		if ( null === $row ) {
			return null;
		}

		return $this->format_row( $row );
	}

	/**
	 * 会員のイベント申込一覧を取得する
	 *
	 * @param int $member_id 会員ID
	 * @return array
	 */
	public function get_by_member( int $member_id ): array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$this->table()} WHERE member_id = %d ORDER BY created_at DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$member_id
			),
			ARRAY_A
		);

		return array_map( [ $this, 'format_row' ], $rows ?: [] );
	}

	/**
	 * ステータスを変更する
	 *
	 * @param int    $id     登録ID
	 * @param string $status 新ステータス
	 * @return true|\WP_Error
	 */
	public function update_status( int $id, string $status ): true|\WP_Error {
		global $wpdb;

		if ( ! in_array( $status, self::VALID_STATUSES, true ) ) {
			return new \WP_Error( 'invalid_status', __( 'Invalid status.', 'conferencemanager' ) );
		}

		$existing = $this->get( $id );
		if ( null === $existing ) {
			return new \WP_Error( 'not_found', __( 'Registration not found.', 'conferencemanager' ) );
		}

		// ステータス遷移の検証
		$valid_transition = $this->is_valid_transition( $existing['status'], $status );
		if ( ! $valid_transition ) {
			return new \WP_Error(
				'invalid_transition',
				sprintf(
					/* translators: 1: current status, 2: new status */
					__( 'Cannot change status from %1$s to %2$s.', 'conferencemanager' ),
					$existing['status'],
					$status
				)
			);
		}

		$update_data = [ 'status' => $status ];

		if ( 'paid' === $status ) {
			$update_data['paid_at'] = current_time( 'mysql', true );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$result = $wpdb->update( $this->table(), $update_data, [ 'id' => $id ] );

		if ( false === $result ) {
			return new \WP_Error(
				'db_update_error',
				__( 'Failed to update status.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		AuditLog::instance()->log( 'update_registration_status', 'registration', $id, [
			'from' => $existing['status'],
			'to'   => $status,
		] );

		return true;
	}

	/**
	 * 申込をキャンセルする
	 *
	 * @param int $id 登録ID
	 * @return true|\WP_Error
	 */
	public function cancel( int $id ): true|\WP_Error {
		return $this->update_status( $id, 'cancelled' );
	}

	/**
	 * 参加区分（料金区分・参加形態）を管理者が修正し、料金を再計算する
	 *
	 * 個人申込（非団体）かつ未入金（pending/confirmed）のみ対象。
	 * 金額・税・ラベル・ステータスを再計算し、未消込の決済レコードと請求書 PDF も同期する。
	 *
	 * @param int    $id                 登録ID
	 * @param string $fee_type           新しい料金区分キー
	 * @param string $participation_type 新しい参加形態 onsite|online
	 * @return array|\WP_Error 更新後の値 [ amount, tax, fee_type, fee_type_label, participation_type, status ]
	 */
	public function update_fee_classification( int $id, string $fee_type, string $participation_type ): array|\WP_Error {
		global $wpdb;

		$registration = $this->get( $id );
		if ( null === $registration ) {
			return new \WP_Error( 'not_found', __( 'Registration not found.', 'conferencemanager' ) );
		}

		// 個人申込限定（団体は専用フローで再計算）
		if ( ! empty( $registration['group_id'] ) || ! empty( $registration['is_representative'] ) ) {
			return new \WP_Error( 'group_not_supported', __( 'Fee classification of group registrations cannot be edited here. Use staff management instead.', 'conferencemanager' ) );
		}

		// 未入金のみ修正可
		if ( ! in_array( $registration['status'], [ 'pending', 'confirmed' ], true ) ) {
			return new \WP_Error( 'not_editable', __( 'Only unpaid registrations (pending or confirmed) can be edited.', 'conferencemanager' ) );
		}

		$payments = \ConfManager\Payment\PaymentManager::instance()->get_by_registration( $id );
		foreach ( $payments as $payment ) {
			if ( 'completed' === $payment['status'] ) {
				return new \WP_Error( 'payment_completed', __( 'This registration already has a completed payment and cannot be edited.', 'conferencemanager' ) );
			}
		}

		// 参加形態の検証
		if ( ! in_array( $participation_type, self::VALID_PARTICIPATION_TYPES, true ) ) {
			return new \WP_Error( 'invalid_participation_type', __( 'Invalid participation type.', 'conferencemanager' ) );
		}

		$event  = EventManager::instance()->get( (int) $registration['event_id'] );
		$member = MemberManager::instance()->get( (int) $registration['member_id'] );
		if ( null === $event || null === $member ) {
			return new \WP_Error( 'not_found', __( 'Event or member not found.', 'conferencemanager' ) );
		}

		// 料金区分の検証（イベントの料金ルールに存在すること。`_` 始まりのメタキーは除外）
		$resolved = EventManager::instance()->resolve_fee_rules( $event );
		if ( str_starts_with( $fee_type, '_' ) || ! isset( $resolved[ $fee_type ] ) ) {
			return new \WP_Error( 'invalid_fee_type', __( 'Invalid fee classification for this event.', 'conferencemanager' ) );
		}

		$old_fee_type = (string) $registration['fee_type'];
		$old_amount   = (int) $registration['amount'];

		$amount = $this->calculate_amount( $event, $member, $participation_type, $registration['session_options'], $fee_type );
		$tax    = $this->calculate_tax( $amount, (float) ( $event['tax_rate'] ?? 10.0 ) );
		$label  = self::resolve_fee_type_label( $fee_type, $event );

		// ステータス再評価（pending/confirmed の範囲のみ。¥0 は confirmed、有料は pending）
		$status = ( $amount + $tax ) <= 0 ? 'confirmed' : 'pending';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$result = $wpdb->update(
			$this->table(),
			[
				'fee_type'           => $fee_type,
				'fee_type_label'     => $label,
				'participation_type' => $participation_type,
				'amount'             => $amount,
				'tax'                => $tax,
				'status'             => $status,
			],
			[ 'id' => $id ]
		);

		if ( false === $result ) {
			return new \WP_Error( 'db_update_error', __( 'Failed to update the registration.', 'conferencemanager' ), $wpdb->last_error );
		}

		// 未消込の決済レコードを同期し、請求書 PDF があれば再生成
		\ConfManager\Payment\PaymentManager::instance()->sync_pending_amount( $id, $amount, $tax );
		foreach ( $payments as $payment ) {
			if ( 'pending' !== $payment['status'] ) {
				continue;
			}
			$invoice = \ConfManager\Payment\InvoiceGenerator::instance()->get_by_payment( (int) $payment['id'] );
			if ( null !== $invoice ) {
				try {
					\ConfManager\Payment\InvoiceGenerator::instance()->generate_pdf( (int) $invoice['id'] );
				} catch ( \Throwable $e ) {
					// PDF 再生成の失敗は致命ではない（金額同期は完了済）。ログのみ。
					error_log( 'conf-manager: invoice PDF regeneration failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				}
			}
		}

		AuditLog::instance()->log( 'update_fee_classification', 'registration', $id, [
			'old_fee_type'       => $old_fee_type,
			'new_fee_type'       => $fee_type,
			'participation_type' => $participation_type,
			'old_amount'         => $old_amount,
			'new_amount'         => $amount,
		] );

		/**
		 * 参加区分の管理者修正後に発火するフック。
		 *
		 * @param int $id     登録ID
		 * @param int $amount 新しい税抜金額
		 * @param int $tax    新しい消費税額
		 */
		do_action( 'conf_manager_registration_fee_corrected', $id, $amount, $tax );

		return [
			'amount'             => $amount,
			'tax'                => $tax,
			'fee_type'           => $fee_type,
			'fee_type_label'     => $label,
			'participation_type' => $participation_type,
			'status'             => $status,
		];
	}

	/**
	 * 申込を検索する
	 *
	 * @param array $args {
	 *     @type int    $event_id           イベントIDで絞り込み
	 *     @type int    $member_id          会員IDで絞り込み
	 *     @type string $status             ステータスで絞り込み
	 *     @type string $participation_type 参加形態で絞り込み
	 *     @type string $payment_method     決済手段で絞り込み
	 *     @type string $group_id           団体IDで絞り込み
	 *     @type string $keyword            登録番号・団体IDで部分一致
	 *     @type string $orderby            ソートカラム
	 *     @type string $order              ASC|DESC
	 *     @type int    $per_page           件数
	 *     @type int    $page               ページ番号
	 * }
	 * @return array{items: array, total: int, page: int, per_page: int}
	 */
	public function search( array $args = [] ): array {
		global $wpdb;

		$table    = $this->table();
		$where    = [];
		$values   = [];
		$per_page = min( 100, max( 1, absint( $args['per_page'] ?? 20 ) ) );
		$page     = max( 1, absint( $args['page'] ?? 1 ) );

		if ( ! empty( $args['event_id'] ) ) {
			$where[]  = 'event_id = %d';
			$values[] = absint( $args['event_id'] );
		}

		if ( ! empty( $args['member_id'] ) ) {
			$where[]  = 'member_id = %d';
			$values[] = absint( $args['member_id'] );
		}

		if ( ! empty( $args['status'] ) && in_array( $args['status'], self::VALID_STATUSES, true ) ) {
			$where[]  = 'status = %s';
			$values[] = $args['status'];
		}

		if ( ! empty( $args['participation_type'] ) && in_array( $args['participation_type'], self::VALID_PARTICIPATION_TYPES, true ) ) {
			$where[]  = 'participation_type = %s';
			$values[] = $args['participation_type'];
		}

		if ( ! empty( $args['payment_method'] ) && in_array( $args['payment_method'], self::VALID_PAYMENT_METHODS, true ) ) {
			$where[]  = 'payment_method = %s';
			$values[] = $args['payment_method'];
		}

		if ( ! empty( $args['group_id'] ) ) {
			$where[]  = 'group_id = %s';
			$values[] = sanitize_text_field( $args['group_id'] );
		}

		if ( ! empty( $args['keyword'] ) ) {
			$keyword  = '%' . $wpdb->esc_like( sanitize_text_field( $args['keyword'] ) ) . '%';
			$where[]  = '(registration_no LIKE %s OR group_id LIKE %s)';
			$values[] = $keyword;
			$values[] = $keyword;
		}

		$where_sql = ! empty( $where ) ? 'WHERE ' . implode( ' AND ', $where ) : '';

		$allowed_orderby = [ 'id', 'registration_no', 'event_id', 'amount', 'status', 'registered_at', 'created_at' ];
		$orderby         = in_array( $args['orderby'] ?? 'id', $allowed_orderby, true )
			? $args['orderby'] ?? 'id'
			: 'id';
		$order           = 'ASC' === strtoupper( $args['order'] ?? 'DESC' ) ? 'ASC' : 'DESC';

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $table is a hardcoded WP prefix table; $where_sql is built from validated %s/%d placeholders; values pass through $wpdb->prepare().
		$count_sql = "SELECT COUNT(*) FROM {$table} {$where_sql}";
		$total     = ! empty( $values )
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
			? (int) $wpdb->get_var( $wpdb->prepare( $count_sql, ...$values ) )
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
			: (int) $wpdb->get_var( $count_sql );

		$offset       = ( $page - 1 ) * $per_page;
		$query_values = array_merge( $values, [ $per_page, $offset ] );
		$data_sql     = "SELECT * FROM {$table} {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
		$rows         = $wpdb->get_results(
			$wpdb->prepare( $data_sql, ...$query_values ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$items = array_map( [ $this, 'format_row' ], $rows ?: [] );

		return [
			'items'    => $items,
			'total'    => $total,
			'page'     => $page,
			'per_page' => $per_page,
		];
	}

	/**
	 * イベントの申込件数を取得
	 *
	 * @param int    $event_id イベントID
	 * @param string $status   ステータス（空=全件, cancelled除外）
	 * @return int
	 */
	public function count_by_event( int $event_id, string $status = '' ): int {
		global $wpdb;

		if ( '' !== $status ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
			return (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$this->table()} WHERE event_id = %d AND status = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
					$event_id,
					$status
				)
			);
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$this->table()} WHERE event_id = %d AND status != 'cancelled'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$event_id
			)
		);
	}

	/**
	 * 料金を計算する
	 *
	 * @param array  $event              イベントデータ
	 * @param array  $member             会員データ
	 * @param string $participation_type 参加形態
	 * @param array  $session_options    選択セッションID配列
	 * @return int 合計金額（円）
	 */
	public function calculate_amount( array $event, array $member, string $participation_type, array $session_options = [], string $fee_type = '' ): int {
		// 基本料金（fee_type が指定されていればそちらで検索、なければ会員種別で検索）
		$type_key = '' !== $fee_type ? $fee_type : $member['member_type'];
		$resolved = \ConfManager\Events\EventManager::instance()->resolve_fee_rules( $event );
		$base_fee = (int) ( $resolved[ $type_key ][ $participation_type ] ?? 0 );

		// セッション追加料金
		$session_fee = 0;
		if ( ! empty( $session_options ) ) {
			$session_mgr = \ConfManager\Events\SessionManager::instance();
			foreach ( $session_options as $session_id ) {
				$session = $session_mgr->get( absint( $session_id ) );
				if ( null !== $session && (int) $session['event_id'] === (int) $event['id'] ) {
					$session_fee += (int) $session['fee'];
				}
			}
		}

		return $base_fee + $session_fee;
	}

	/**
	 * 消費税を計算する
	 *
	 * @param int   $amount   税抜金額
	 * @param float $tax_rate 税率（%）。0.0 で非課税。デフォルト 10.0
	 * @return int 消費税額
	 */
	public function calculate_tax( int $amount, float $tax_rate = 10.0 ): int {
		if ( $tax_rate <= 0.0 ) {
			return 0;
		}
		return (int) floor( $amount * $tax_rate / 100 );
	}

	/**
	 * 料金区分キーからラベルを解決する
	 *
	 * 標準会員種別 → 定義済みラベル、カスタム種別 → fee_rules._labels から取得。
	 */
	public static function resolve_fee_type_label( string $fee_type, array $event ): string {
		$standard = [
			'regular'   => __( 'Regular Member', 'conferencemanager' ),
			'student'   => __( 'Student Member', 'conferencemanager' ),
			'associate' => __( 'Associate Member', 'conferencemanager' ),
			'honorary'  => __( 'Honorary Member', 'conferencemanager' ),
			'general'   => __( 'Non-member', 'conferencemanager' ),
		];

		if ( isset( $standard[ $fee_type ] ) ) {
			return $standard[ $fee_type ];
		}

		// カスタム種別: イベントの fee_rules._labels から取得
		$rules = $event['fee_rules'] ?? [];
		if ( is_string( $rules ) ) {
			$rules = json_decode( $rules, true ) ?: [];
		}

		return $rules['_labels'][ $fee_type ] ?? $fee_type;
	}

	/**
	 * 同一イベント・同一会員のアクティブな申込があるか
	 */
	public function has_active_registration( int $event_id, int $member_id ): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		return (bool) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$this->table()} WHERE event_id = %d AND member_id = %d AND status != 'cancelled' FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$event_id,
				$member_id
			)
		);
	}

	/**
	 * 登録番号を生成する
	 *
	 * フォーマット: REG-{YYYYMMDD}-{連番5桁}
	 */
	private function generate_registration_no(): string {
		global $wpdb;

		$date_prefix = 'REG-' . gmdate( 'Ymd' ) . '-';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$last = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT registration_no FROM {$this->table()} WHERE registration_no LIKE %s ORDER BY id DESC LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$date_prefix . '%'
			)
		);

		if ( $last ) {
			$seq = (int) substr( $last, -5 ) + 1;
		} else {
			$seq = 1;
		}

		return $date_prefix . str_pad( (string) $seq, 5, '0', STR_PAD_LEFT );
	}

	/**
	 * ステータス遷移の検証
	 *
	 * 許可される遷移:
	 * pending   → confirmed, paid, cancelled
	 * confirmed → paid, cancelled
	 * paid      → attended, cancelled
	 * cancelled → pending（誤キャンセルからの復元）
	 * attended  → (遷移不可)
	 */
	private function is_valid_transition( string $from, string $to ): bool {
		$transitions = [
			'pending'   => [ 'confirmed', 'paid', 'cancelled' ],
			'confirmed' => [ 'paid', 'cancelled' ],
			'paid'      => [ 'attended', 'cancelled' ],
			'cancelled' => [ 'pending' ],
			'attended'  => [],
		];

		return in_array( $to, $transitions[ $from ] ?? [], true );
	}

	/**
	 * DB行を整形する
	 */
	private function format_row( array $row ): array {
		if ( ! empty( $row['session_options_json'] ) ) {
			$row['session_options'] = json_decode( $row['session_options_json'], true ) ?: [];
		} else {
			$row['session_options'] = [];
		}

		return $row;
	}

	/**
	 * バリデーション
	 */
	private function validate( array $data ): true|\WP_Error {
		if ( empty( $data['event_id'] ) ) {
			return new \WP_Error( 'missing_event_id', __( 'Event ID is required.', 'conferencemanager' ) );
		}

		if ( empty( $data['member_id'] ) ) {
			return new \WP_Error( 'missing_member_id', __( 'Member ID is required.', 'conferencemanager' ) );
		}

		if ( isset( $data['participation_type'] ) && ! in_array( $data['participation_type'], self::VALID_PARTICIPATION_TYPES, true ) ) {
			return new \WP_Error( 'invalid_participation_type', __( 'Invalid participation type.', 'conferencemanager' ) );
		}

		if ( isset( $data['payment_method'] ) && ! in_array( $data['payment_method'], self::VALID_PAYMENT_METHODS, true ) ) {
			return new \WP_Error( 'invalid_payment_method', __( 'Invalid payment method.', 'conferencemanager' ) );
		}

		return true;
	}

	/**
	 * 有効なステータス一覧
	 */
	public static function get_valid_statuses(): array {
		return self::VALID_STATUSES;
	}
}
