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

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


use ConfManager\Traits\Singleton;
use ConfManager\Security\AuditLog;

/**
 * イベントCRUD管理
 *
 * イベントの作成・取得・更新・削除・複製・検索を行う。
 * 料金ルール（会員種別×参加形態マトリックス）は fee_rules_json で管理。
 *
 * @since 1.0.0
 */
class EventManager {

	use Singleton;

	private const TABLE = 'conf_events';

	/**
	 * 有効なステータス
	 */
	private const VALID_STATUSES = [ 'draft', 'published', 'closed', 'cancelled' ];

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

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

	/**
	 * イベントを作成する
	 *
	 * @param array $data {
	 *     イベントデータ
	 *     @type string $title          タイトル（必須）
	 *     @type string $description    説明
	 *     @type string $date_start     開始日時（必須、Y-m-d H:i:s）
	 *     @type string $date_end       終了日時（必須）
	 *     @type string $venue          会場名
	 *     @type string $venue_address  会場住所
	 *     @type int    $capacity       定員
	 *     @type string $reg_start      申込開始日時
	 *     @type string $reg_end        申込締切日時
	 *     @type array  $fee_rules      料金ルール配列
	 *     @type string $status         ステータス（デフォルト: draft）
	 * }
	 * @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;
		}

		$insert_data = $this->prepare_data( $data );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table query.
		$result = $wpdb->insert( $this->table(), $insert_data );

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

		$event_id = (int) $wpdb->insert_id;

		AuditLog::instance()->log( 'create_event', 'event', $event_id, [
			'title' => $data['title'],
		] );

		delete_transient( 'conf_manager_published_events' );

		return $event_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 int   $id   イベントID
	 * @param array $data 更新データ
	 * @return true|\WP_Error
	 */
	public function update( int $id, array $data ): true|\WP_Error {
		global $wpdb;

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

		// ステータスのバリデーション
		if ( isset( $data['status'] ) && ! in_array( $data['status'], self::VALID_STATUSES, true ) ) {
			return new \WP_Error( 'invalid_status', __( 'Invalid status.', 'conferencemanager' ) );
		}

		// 日付のバリデーション（両方指定された場合のみ整合性チェック）
		$start = $data['date_start'] ?? $existing['date_start'];
		$end   = $data['date_end'] ?? $existing['date_end'];
		if ( $start && $end && strtotime( $start ) >= strtotime( $end ) ) {
			return new \WP_Error( 'invalid_dates', __( 'End date must be after start date.', 'conferencemanager' ) );
		}

		$update_data = [];

		// テキストフィールド
		$text_fields = [ 'title', 'venue', 'venue_address', 'status', 'payment_account_name' ];
		foreach ( $text_fields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$update_data[ $field ] = sanitize_text_field( $data[ $field ] );
			}
		}

		// テキストエリアフィールド（改行保持）
		$textarea_fields = [ 'payment_account_info', 'organizer_info', 'advance_fee_notice' ];
		foreach ( $textarea_fields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$update_data[ $field ] = sanitize_textarea_field( $data[ $field ] );
			}
		}

		// description は wp_kses_post
		if ( isset( $data['description'] ) ) {
			$update_data['description'] = wp_kses_post( $data['description'] );
		}

		// 日時フィールド
		$datetime_fields = [ 'date_start', 'date_end', 'reg_start', 'reg_end', 'advance_fee_deadline', 'submission_start', 'submission_end' ];
		foreach ( $datetime_fields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$update_data[ $field ] = sanitize_text_field( $data[ $field ] ) ?: null;
			}
		}

		// 数値フィールド
		if ( isset( $data['capacity'] ) ) {
			$update_data['capacity'] = absint( $data['capacity'] ) ?: null;
		}

		// 税率（0.0〜100.0 にクランプ）
		if ( isset( $data['tax_rate'] ) ) {
			$update_data['tax_rate'] = max( 0.0, min( 100.0, (float) $data['tax_rate'] ) );
		}

		// 料金ルール
		if ( isset( $data['fee_rules'] ) ) {
			$update_data['fee_rules_json'] = wp_json_encode( $data['fee_rules'], JSON_UNESCAPED_UNICODE );
		}

		// 当日料金ルール
		if ( isset( $data['fee_rules_day'] ) ) {
			$update_data['fee_rules_day_json'] = wp_json_encode( $data['fee_rules_day'], JSON_UNESCAPED_UNICODE );
		}

		// 演題投稿設定
		if ( isset( $data['submission_settings'] ) ) {
			$update_data['submission_settings_json'] = wp_json_encode( $data['submission_settings'], JSON_UNESCAPED_UNICODE );
		}

		if ( empty( $update_data ) ) {
			return 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 event.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		AuditLog::instance()->log( 'update_event', 'event', $id, [
			'fields' => array_keys( $update_data ),
		] );

		delete_transient( 'conf_manager_published_events' );

		return true;
	}

	/**
	 * 催しをキャンセルしたときに巻き込める申込を数える
	 *
	 * 🔴 **数える口はここ 1 つ。** 画面に見せる件数と、実際に動く件数が
	 * 同じリゾルバから出ることを保証するため、`delete()` も件数ではなく
	 * **このメソッドが返した ID の配列**を回す。別々に数えると
	 * 「3 件と言われたのに 2 件しか動かない」が起きる。
	 *
	 * ⚠️ **出席済（`attended`）は巻き込めない。** 遷移表が `attended => []` なので
	 * cancelled にできない。数に混ぜると上のずれが起きるため**別枠で返し、
	 * 画面にもそう出す**。
	 *
	 * 🔴 **入金済の申込は数には入れるが、決済には触れない。** 催しが中止でも
	 * 受け取った金は自動では戻らない（返金扱いにすると「起きていない返金が
	 * 台帳に載る」）。運用者が判断できるよう件数だけ知らせる。
	 *
	 * ⚠️ **SELECT に ORDER BY が要る。** 無いと監査ログに残す ID の並びが
	 * 実行ごとに変わり、後から照合できない。
	 *
	 * @param int $id イベントID
	 * @return array{cancellable_ids:int[],cancellable:int,attended:int,with_received_payment:int}
	 */
	public function get_cancel_impact( int $id ): array {
		global $wpdb;

		$registrations = $wpdb->prefix . 'conf_registrations';
		$payments      = $wpdb->prefix . 'conf_payments';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names come from the plugin-defined prefix; the only user value is bound with prepare().
		$cancellable_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$registrations}
				 WHERE event_id = %d AND status IN ('pending', 'confirmed', 'paid')
				 ORDER BY id ASC",
				$id
			)
		);

		$attended = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$registrations} WHERE event_id = %d AND status = 'attended'",
				$id
			)
		);

		$with_received_payment = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(DISTINCT r.id) FROM {$registrations} r
				 INNER JOIN {$payments} p ON p.registration_id = r.id
				 WHERE r.event_id = %d
				   AND r.status IN ('pending', 'confirmed', 'paid')
				   AND p.status IN ('completed', 'refunded', 'partial_refund')",
				$id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

		$cancellable_ids = array_map( 'intval', $cancellable_ids ?: [] );

		return [
			'cancellable_ids'       => $cancellable_ids,
			'cancellable'           => count( $cancellable_ids ),
			'attended'              => $attended,
			'with_received_payment' => $with_received_payment,
		];
	}

	/**
	 * イベントを削除する（論理削除: cancelled に変更）
	 *
	 * 🔴 `$cancel_registrations` の既定は **false**。催しをキャンセルしても
	 * 申込は自動では巻き込まない。実データには申込が 100 件以上ぶら下がった
	 * 催しがあり、連鎖させると**押しただけで全部が動き、戻すのは 1 件ずつ**に
	 * なる。かといって何もしなければ誤った未収金が残るので、
	 * **押す前に件数を見せて運用者に選ばせる**（画面側は未選択で開く）。
	 *
	 * @param int  $id                   イベントID
	 * @param bool $cancel_registrations 申込も巻き込んでキャンセルするか
	 * @return true|\WP_Error
	 */
	public function delete( int $id, bool $cancel_registrations = false ): true|\WP_Error {
		global $wpdb;

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

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

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

		// 🔴 見せた件数と動く件数を一致させるため、**同じリゾルバが返した ID** を回す。
		// 別に数え直すと、この 2 つのあいだに申込が増減したときに食い違う。
		$cancelled_ids = [];
		if ( $cancel_registrations ) {
			$impact  = $this->get_cancel_impact( $id );
			$reg_mgr = \ConfManager\Registration\RegistrationManager::instance();
			foreach ( $impact['cancellable_ids'] as $registration_id ) {
				$result = $reg_mgr->update_status( $registration_id, 'cancelled' );
				if ( ! is_wp_error( $result ) ) {
					$cancelled_ids[] = $registration_id;
				}
			}
		}

		AuditLog::instance()->log( 'delete_event', 'event', $id, [
			'title' => $existing['title'],
			// ⚠️ 巻き込んだ申込は 1 件ずつしか戻せないので、これが唯一の手がかりになる。
			'cancelled_registration_ids' => $cancelled_ids,
		] );

		delete_transient( 'conf_manager_published_events' );
		delete_option( "conf_manager_program_pdfs_{$id}" );

		return true;
	}

	/**
	 * イベントを複製する
	 *
	 * セッションも含めて複製。ステータスは draft にリセット。
	 *
	 * @param int $id 複製元イベントID
	 * @return int|\WP_Error 新しいイベントID
	 */
	public function duplicate( int $id ): int|\WP_Error {
		$source = $this->get( $id );
		if ( null === $source ) {
			return new \WP_Error( 'not_found', __( 'Event not found.', 'conferencemanager' ) );
		}

		$new_data = [
			'title'         => sprintf(
				/* translators: %s: original event title */
				__( '%s (Copy)', 'conferencemanager' ),
				$source['title']
			),
			'description'   => $source['description'] ?? '',
			'date_start'    => $source['date_start'],
			'date_end'      => $source['date_end'],
			'venue'         => $source['venue'] ?? '',
			'venue_address' => $source['venue_address'] ?? '',
			'capacity'      => $source['capacity'] ? (int) $source['capacity'] : 0,
			'reg_start'     => $source['reg_start'] ?? '',
			'reg_end'       => $source['reg_end'] ?? '',
			'fee_rules'              => $source['fee_rules'] ?? [],
			'advance_fee_deadline'   => $source['advance_fee_deadline'] ?? '',
			'fee_rules_day'          => $source['fee_rules_day'] ?? [],
			'advance_fee_notice'     => $source['advance_fee_notice'] ?? '',
			'status'                 => 'draft',
		];

		$new_id = $this->create( $new_data );

		if ( is_wp_error( $new_id ) ) {
			return $new_id;
		}

		// セッションも複製
		$session_mgr = SessionManager::instance();
		$sessions    = $session_mgr->get_by_event( $id );

		foreach ( $sessions as $session ) {
			$session_mgr->create( [
				'event_id'     => $new_id,
				'title'        => $session['title'],
				'session_type' => $session['session_type'],
				'date_start'   => $session['date_start'],
				'date_end'     => $session['date_end'],
				'capacity'     => $session['capacity'] ? (int) $session['capacity'] : 0,
				'fee'          => (int) $session['fee'],
				'sort_order'   => (int) $session['sort_order'],
			] );
		}

		AuditLog::instance()->log( 'duplicate_event', 'event', $new_id, [
			'source_id' => $id,
		] );

		return $new_id;
	}

	/**
	 * イベントを検索する
	 *
	 * @param array $args {
	 *     @type string $keyword   タイトル・会場で部分一致
	 *     @type string $status    ステータスで絞り込み
	 *     @type string $from      開始日以降（Y-m-d）
	 *     @type string $to        終了日以前（Y-m-d）
	 *     @type string $orderby   ソートカラム（デフォルト: date_start）
	 *     @type string $order     ASC|DESC（デフォルト: DESC）
	 *     @type int    $per_page  件数（デフォルト: 20）
	 *     @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['status'] ) && in_array( $args['status'], self::VALID_STATUSES, true ) ) {
			$where[]  = 'status = %s';
			$values[] = $args['status'];
		}

		// キーワード
		if ( ! empty( $args['keyword'] ) ) {
			$keyword  = '%' . $wpdb->esc_like( sanitize_text_field( $args['keyword'] ) ) . '%';
			$where[]  = '(title LIKE %s OR venue LIKE %s)';
			$values[] = $keyword;
			$values[] = $keyword;
		}

		// 日付範囲
		if ( ! empty( $args['from'] ) ) {
			$where[]  = 'date_start >= %s';
			$values[] = sanitize_text_field( $args['from'] ) . ' 00:00:00';
		}

		if ( ! empty( $args['to'] ) ) {
			$where[]  = 'date_start <= %s';
			$values[] = sanitize_text_field( $args['to'] ) . ' 23:59:59';
		}

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

		// ソート
		$allowed_orderby = [ 'id', 'title', 'date_start', 'date_end', 'reg_start', 'status', 'created_at' ];
		$orderby         = in_array( $args['orderby'] ?? 'date_start', $allowed_orderby, true )
			? $args['orderby'] ?? 'date_start'
			: 'date_start';
		$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 -- Hardcoded WP prefix table; $where_sql uses validated 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 ] );
		// ORDER BY の末尾に id を足して並びを一意にする。
		// 一意でないまま LIMIT/OFFSET で頁を送ると、頁をまたぐ順序が保証されず
		// **同じ行が 2 度来たり 1 行落ちたりする**。「100 件で切れる」より質が悪く、
		// 「たまに 1 件足りない」は誰も気づけない。しかも InnoDB はたいてい安定して
		// 返すため**テストでは守れない**ので、構造として固定する（M-12 が検査する）。
		$data_sql     = "SELECT * FROM {$table} {$where_sql} ORDER BY {$orderby} {$order}, id {$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,
		];
	}

	/**
	 * 公開中のイベント一覧を取得する
	 *
	 * @return array
	 */
	public function get_published(): array {
		$cache_key = 'conf_manager_published_events';
		$cached    = get_transient( $cache_key );

		if ( false !== $cached ) {
			return $cached;
		}

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$rows = $wpdb->get_results(
			"SELECT * FROM {$this->table()} WHERE status = 'published' ORDER BY date_start ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
			ARRAY_A
		);

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

		set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS );

		return $result;
	}

	/**
	 * イベントの料金を取得する
	 *
	 * @param int    $event_id          イベントID
	 * @param string $member_type       会員種別
	 * @param string $participation_type 参加形態（onsite|online）
	 * @return int 料金（円）。ルール未設定時は 0
	 */
	public function get_fee( int $event_id, string $member_type, string $participation_type = 'onsite' ): int {
		$event = $this->get( $event_id );
		if ( null === $event ) {
			return 0;
		}

		$rules = $this->resolve_fee_rules( $event );
		return (int) ( $rules[ $member_type ][ $participation_type ] ?? 0 );
	}

	/**
	 * 入力データのバリデーション
	 */
	private function validate( array $data ): true|\WP_Error {
		if ( empty( $data['title'] ) ) {
			return new \WP_Error( 'missing_title', __( 'Title is required.', 'conferencemanager' ) );
		}

		if ( empty( $data['date_start'] ) || empty( $data['date_end'] ) ) {
			return new \WP_Error( 'missing_dates', __( 'Start date and end date are required.', 'conferencemanager' ) );
		}

		if ( strtotime( $data['date_start'] ) >= strtotime( $data['date_end'] ) ) {
			return new \WP_Error( 'invalid_dates', __( 'End date must be after start date.', 'conferencemanager' ) );
		}

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

		if ( ! empty( $data['advance_fee_deadline'] ) && ! empty( $data['date_start'] ) ) {
			if ( strtotime( $data['advance_fee_deadline'] ) >= strtotime( $data['date_start'] ) ) {
				return new \WP_Error( 'invalid_advance_deadline', __( 'Advance ticket deadline must be before the event start date.', 'conferencemanager' ) );
			}
		}

		return true;
	}

	/**
	 * 保存用データを準備する
	 */
	private function prepare_data( array $data ): array {
		$prepared = [
			'title'      => sanitize_text_field( $data['title'] ),
			'date_start' => sanitize_text_field( $data['date_start'] ),
			'date_end'   => sanitize_text_field( $data['date_end'] ),
			'status'     => sanitize_text_field( $data['status'] ?? 'draft' ),
		];

		if ( isset( $data['description'] ) ) {
			$prepared['description'] = wp_kses_post( $data['description'] );
		}

		$optional_text = [ 'venue', 'venue_address' ];
		foreach ( $optional_text as $field ) {
			if ( ! empty( $data[ $field ] ) ) {
				$prepared[ $field ] = sanitize_text_field( $data[ $field ] );
			}
		}

		if ( ! empty( $data['capacity'] ) ) {
			$prepared['capacity'] = absint( $data['capacity'] );
		}

		$optional_datetime = [ 'reg_start', 'reg_end', 'advance_fee_deadline', 'submission_start', 'submission_end' ];
		foreach ( $optional_datetime as $field ) {
			if ( ! empty( $data[ $field ] ) ) {
				$prepared[ $field ] = sanitize_text_field( $data[ $field ] );
			}
		}

		if ( isset( $data['fee_rules'] ) && is_array( $data['fee_rules'] ) ) {
			$prepared['fee_rules_json'] = wp_json_encode( $data['fee_rules'], JSON_UNESCAPED_UNICODE );
		}

		if ( isset( $data['fee_rules_day'] ) && is_array( $data['fee_rules_day'] ) ) {
			$prepared['fee_rules_day_json'] = wp_json_encode( $data['fee_rules_day'], JSON_UNESCAPED_UNICODE );
		}

		if ( isset( $data['submission_settings'] ) && is_array( $data['submission_settings'] ) ) {
			$prepared['submission_settings_json'] = wp_json_encode( $data['submission_settings'], JSON_UNESCAPED_UNICODE );
		}

		if ( isset( $data['tax_rate'] ) ) {
			$prepared['tax_rate'] = max( 0.0, min( 100.0, (float) $data['tax_rate'] ) );
		}

		// 振込先名義（単行テキスト）
		if ( ! empty( $data['payment_account_name'] ) ) {
			$prepared['payment_account_name'] = sanitize_text_field( $data['payment_account_name'] );
		}

		// 振込先情報・主催者情報・前売注意事項（改行あり）
		$textarea_fields = [ 'payment_account_info', 'organizer_info', 'advance_fee_notice' ];
		foreach ( $textarea_fields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$prepared[ $field ] = sanitize_textarea_field( $data[ $field ] );
			}
		}

		return $prepared;
	}

	/**
	 * DB行を整形する（fee_rules_json をデコード）
	 */
	private function format_row( array $row ): array {
		if ( ! empty( $row['fee_rules_json'] ) ) {
			$row['fee_rules'] = json_decode( $row['fee_rules_json'], true ) ?: [];
		} else {
			$row['fee_rules'] = [];
		}

		if ( ! empty( $row['fee_rules_day_json'] ) ) {
			$row['fee_rules_day'] = json_decode( $row['fee_rules_day_json'], true ) ?: [];
		} else {
			$row['fee_rules_day'] = [];
		}

		$row['tax_rate'] = (float) ( $row['tax_rate'] ?? 10.0 );

		if ( ! empty( $row['submission_settings_json'] ) ) {
			$row['submission_settings'] = json_decode( $row['submission_settings_json'], true ) ?: [];
		} else {
			$row['submission_settings'] = [];
		}

		return $row;
	}

	/**
	 * 現在時刻に基づいて適用する料金ルールを返す
	 *
	 * advance_fee_deadline が設定されている場合、締切前は前売料金（fee_rules）、
	 * 締切後は当日料金（fee_rules_day）を返す。
	 * 未設定の場合は従来通り fee_rules を返す（後方互換）。
	 *
	 * @param array $event イベントデータ
	 * @return array 料金ルール配列
	 */
	public function resolve_fee_rules( array $event ): array {
		$deadline = $event['advance_fee_deadline'] ?? null;
		if ( empty( $deadline ) || empty( $event['fee_rules_day'] ) ) {
			return $event['fee_rules'] ?? [];
		}
		return time() <= strtotime( $deadline )
			? ( $event['fee_rules'] ?? [] )
			: $event['fee_rules_day'];
	}

	/**
	 * 現在が前売期間かどうかを判定する
	 *
	 * @param array $event イベントデータ
	 * @return bool|null 前売期間中なら true、当日料金期間なら false、二段階価格未設定なら null
	 */
	public function is_advance_period( array $event ): ?bool {
		$deadline = $event['advance_fee_deadline'] ?? null;
		if ( empty( $deadline ) || empty( $event['fee_rules_day'] ) ) {
			return null;
		}
		return time() <= strtotime( $deadline );
	}

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