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

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


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

/**
 * 団体申込管理
 *
 * 代表者+スタッフの団体申込を管理する。
 * スタッフの事後追加・差額計算にも対応。
 *
 * @since 1.0.0
 */
class GroupRegistration {

	use Singleton;

	private const STAFF_TABLE = 'conf_group_members';

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

	/**
	 * スタッフテーブル名
	 */
	private function staff_table(): string {
		global $wpdb;
		return $wpdb->prefix . self::STAFF_TABLE;
	}

	/**
	 * 団体申込を作成する（代表者＋スタッフ）
	 *
	 * @param array $representative_data 代表者の申込データ（RegistrationManager::create() と同じ）
	 * @param array $staff_list          スタッフ一覧 [{name, email, participation_type, session_options, member_id?}, ...]
	 * @return array{registration_id: int, group_id: string, staff_ids: int[]}|\WP_Error
	 */
	public function create_group( array $representative_data, array $staff_list = [] ): array|\WP_Error {
		$reg_mgr = RegistrationManager::instance();

		// 代表者を登録（group_id は後で設定）
		$representative_data['is_representative'] = true;
		$reg_id = $reg_mgr->create( $representative_data );

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

		// 代表者の登録番号を団体IDとして使用
		$registration = $reg_mgr->get( $reg_id );
		$group_id     = $registration['registration_no'];

		// 代表者に group_id を設定
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$wpdb->update(
			$wpdb->prefix . 'conf_registrations',
			[ 'group_id' => $group_id ],
			[ 'id' => $reg_id ]
		);

		// スタッフを登録
		$staff_ids = [];
		foreach ( $staff_list as $staff ) {
			$staff_id = $this->add_staff( $reg_id, $staff );
			if ( is_wp_error( $staff_id ) ) {
				continue; // エラーのスタッフはスキップ
			}
			$staff_ids[] = $staff_id;
		}

		// 団体全体の料金を再計算して代表者に設定
		$this->recalculate_group_amount( $reg_id );

		AuditLog::instance()->log( 'create_group_registration', 'registration', $reg_id, [
			'group_id'    => $group_id,
			'staff_count' => count( $staff_ids ),
		] );

		return [
			'registration_id' => $reg_id,
			'group_id'        => $group_id,
			'staff_ids'       => $staff_ids,
		];
	}

	/**
	 * スタッフを追加する
	 *
	 * @param int   $registration_id 代表者の登録ID
	 * @param array $staff_data {
	 *     @type string $name               スタッフ氏名（必須）
	 *     @type string $email              メールアドレス（必須）
	 *     @type string $participation_type  参加形態 onsite|online
	 *     @type array  $session_options    選択セッションID配列
	 *     @type int    $member_id          会員IDがある場合
	 * }
	 * @return int|\WP_Error スタッフID
	 */
	public function add_staff( int $registration_id, array $staff_data ): int|\WP_Error {
		global $wpdb;

		// 代表者申込の存在確認
		$registration = RegistrationManager::instance()->get( $registration_id );
		if ( null === $registration ) {
			return new \WP_Error( 'not_found', __( 'Registration not found.', 'conferencemanager' ) );
		}

		if ( empty( $staff_data['name'] ) ) {
			return new \WP_Error( 'missing_name', __( 'Staff name is required.', 'conferencemanager' ) );
		}

		if ( empty( $staff_data['email'] ) || ! is_email( $staff_data['email'] ) ) {
			return new \WP_Error( 'invalid_email', __( 'Please enter a valid email address.', 'conferencemanager' ) );
		}

		$enc = Encryption::instance();

		$normalized_email = strtolower( trim( $staff_data['email'] ) );
		$email_hash       = Encryption::hash_email( $normalized_email );

		// 同一団体内のメール重複チェック
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from safe internal method; values bound via prepare().
		$existing = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$this->staff_table()} WHERE registration_id = %d AND email_hash = %s AND status = 'active'",
				$registration_id,
				$email_hash
			)
		);
		// phpcs:enable
		if ( $existing > 0 ) {
			return new \WP_Error(
				'duplicate_email',
				sprintf(
					/* translators: %s: email address */
					__( 'Email address %s is already registered in this group.', 'conferencemanager' ),
					$staff_data['email']
				)
			);
		}

		// 代表者と同じメールアドレスかチェック
		$rep_member = \ConfManager\Members\MemberManager::instance()->get( (int) $registration['member_id'] );
		if ( null !== $rep_member && $rep_member['email_hash'] === $email_hash ) {
			return new \WP_Error(
				'duplicate_representative',
				__( 'Cannot use the same email address as the representative.', 'conferencemanager' )
			);
		}

		// 🔴 料金が設定されていない参加形態のスタッフは受け付けない。
		//    recalculate_group_amount() の `?? 0` に落ちると、そのスタッフぶんだけ
		//    黙って無料になり、団体全体の請求額が実態とずれる。
		$staff_participation = sanitize_text_field( $staff_data['participation_type'] ?? 'onsite' );
		$staff_type          = sanitize_text_field( $staff_data['fee_type'] ?? '' );
		if ( '' === $staff_type ) {
			$staff_member = ! empty( $staff_data['member_id'] )
				? \ConfManager\Members\MemberManager::instance()->get( absint( $staff_data['member_id'] ) )
				: null;
			$staff_type   = $staff_member['member_type'] ?? ( $rep_member['member_type'] ?? 'regular' );
		}

		$event = \ConfManager\Events\EventManager::instance()->get( (int) $registration['event_id'] );
		if ( null !== $event
			&& ! RegistrationManager::instance()->fee_is_configured( $event, $staff_type, $staff_participation ) ) {
			return new \WP_Error(
				'participation_type_unavailable',
				__( 'The selected participation type is not available for this event. Please choose another one or contact the organizer.', 'conferencemanager' ),
				[ 'status' => 400 ]
			);
		}

		$insert_data = [
			'registration_id'    => $registration_id,
			'name'               => sanitize_text_field( $staff_data['name'] ),
			'email'              => $enc->encrypt( $normalized_email ),
			'email_hash'         => $email_hash,
			'fee_type'           => sanitize_text_field( $staff_data['fee_type'] ?? '' ),
			'participation_type' => $staff_participation,
			'status'             => 'active',
		];

		if ( ! empty( $staff_data['member_id'] ) ) {
			$insert_data['member_id'] = absint( $staff_data['member_id'] );
		}

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

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

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

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

		AuditLog::instance()->log( 'add_group_staff', 'group_member', $staff_id, [
			'registration_id' => $registration_id,
			'name'            => $staff_data['name'],
		] );

		return $staff_id;
	}

	/**
	 * スタッフを取得する
	 *
	 * @param int $id スタッフID
	 * @return array|null 復号済みデータ
	 */
	public function get_staff( int $id ): ?array {
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from safe internal method; values bound via prepare().
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->staff_table()} WHERE id = %d",
				$id
			),
			ARRAY_A
		);
		// phpcs:enable

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

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

	/**
	 * 代表者申込に紐づくスタッフ一覧を取得する
	 *
	 * @param int $registration_id 代表者の登録ID
	 * @return array
	 */
	public function get_staff_list( int $registration_id ): array {
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from safe internal method; values bound via prepare().
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$this->staff_table()} WHERE registration_id = %d AND status = 'active' ORDER BY id ASC",
				$registration_id
			),
			ARRAY_A
		);
		// phpcs:enable

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

	/**
	 * メールアドレスでスタッフを検索する
	 *
	 * @param string $email メールアドレス（平文）
	 * @return array|null 復号済みデータ（最新のアクティブなスタッフ）
	 */
	public function get_staff_by_email( string $email ): ?array {
		global $wpdb;

		$email_hash = Encryption::hash_email( $email );

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from safe internal method; values bound via prepare().
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->staff_table()} WHERE email_hash = %s AND status = 'active' ORDER BY id DESC LIMIT 1",
				$email_hash
			),
			ARRAY_A
		);
		// phpcs:enable

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

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

	/**
	 * スタッフをキャンセルする
	 *
	 * @param int $staff_id スタッフID
	 * @return true|\WP_Error
	 */
	public function cancel_staff( int $staff_id ): true|\WP_Error {
		global $wpdb;

		$existing = $this->get_staff( $staff_id );
		if ( null === $existing ) {
			return new \WP_Error( 'not_found', __( 'Staff 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->staff_table(),
			[ 'status' => 'cancelled' ],
			[ 'id' => $staff_id ]
		);

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

		AuditLog::instance()->log( 'cancel_group_staff', 'group_member', $staff_id, [
			'registration_id' => $existing['registration_id'],
			'name'            => $existing['name'],
		] );

		return true;
	}

	/**
	 * 団体の合計金額を再計算し、代表者の申込に反映する
	 *
	 * @param int $registration_id 代表者の登録ID
	 * @return array{amount: int, tax: int, diff: int} 再計算結果と差額
	 */
	public function recalculate_group_amount( int $registration_id ): array {
		global $wpdb;

		$reg_mgr      = RegistrationManager::instance();
		$registration = $reg_mgr->get( $registration_id );

		if ( null === $registration ) {
			return [ 'amount' => 0, 'tax' => 0, 'diff' => 0 ];
		}

		$event  = EventManager::instance()->get( (int) $registration['event_id'] );
		$member = MemberManager::instance()->get( (int) $registration['member_id'] );

		if ( null === $event || null === $member ) {
			return [ 'amount' => 0, 'tax' => 0, 'diff' => 0 ];
		}

		// 代表者の料金
		$total = $reg_mgr->calculate_amount(
			$event,
			$member,
			$registration['participation_type'],
			$registration['session_options']
		);

		// スタッフの料金を加算
		$staff_list = $this->get_staff_list( $registration_id );
		foreach ( $staff_list as $staff ) {
			// fee_type が設定されていればそちらを優先（カスタム種別対応）
			$staff_type = ! empty( $staff['fee_type'] ) ? $staff['fee_type'] : $member['member_type'];

			// fee_type 未設定かつスタッフが会員の場合はその種別を使用
			if ( empty( $staff['fee_type'] ) && ! empty( $staff['member_id'] ) ) {
				$staff_member = MemberManager::instance()->get( (int) $staff['member_id'] );
				if ( null !== $staff_member ) {
					$staff_type = $staff_member['member_type'];
				}
			}

			$staff_session_options = [];
			if ( ! empty( $staff['session_options_json'] ) ) {
				$staff_session_options = json_decode( $staff['session_options_json'], true ) ?: [];
			}

			$resolved = \ConfManager\Events\EventManager::instance()->resolve_fee_rules( $event );
			$base_fee = (int) ( $resolved[ $staff_type ][ $staff['participation_type'] ] ?? 0 );

			// セッション追加料金
			$session_fee = 0;
			$session_mgr = \ConfManager\Events\SessionManager::instance();
			foreach ( $staff_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'];
				}
			}

			$total += $base_fee + $session_fee;
		}

		$old_amount = (int) $registration['amount'];
		$tax_rate   = (float) ( $event['tax_rate'] ?? 10.0 );
		$tax        = $reg_mgr->calculate_tax( $total, $tax_rate );
		$diff       = $total - $old_amount;

		$update_fields = [
			'amount' => $total,
			'tax'    => $tax,
		];

		// Re-evaluate status only while pending/confirmed (keep paid/attended/cancelled).
		// A group whose recalculated total becomes ¥0 needs no payment so it is confirmed;
		// if it rises above ¥0 again it returns to pending.
		$current_status = (string) ( $registration['status'] ?? 'pending' );
		if ( in_array( $current_status, [ 'pending', 'confirmed' ], true ) ) {
			$update_fields['status'] = ( $total + $tax ) <= 0 ? 'confirmed' : 'pending';
		}

		// 代表者の申込金額を更新
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$wpdb->update(
			$wpdb->prefix . 'conf_registrations',
			$update_fields,
			[ 'id' => $registration_id ]
		);

		if ( 0 !== $diff ) {
			AuditLog::instance()->log( 'recalculate_group_amount', 'registration', $registration_id, [
				'old_amount' => $old_amount,
				'new_amount' => $total,
				'diff'       => $diff,
			] );
		}

		return [
			'amount' => $total,
			'tax'    => $tax,
			'diff'   => $diff,
		];
	}

	/**
	 * 団体のスタッフ数を取得する
	 *
	 * @param int $registration_id 代表者の登録ID
	 * @return int
	 */
	public function count_staff( int $registration_id ): int {
		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from safe internal method; values bound via prepare().
		$count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$this->staff_table()} WHERE registration_id = %d AND status = 'active'",
				$registration_id
			)
		);
		// phpcs:enable
		return $count;
	}

	/**
	 * スタッフ行の暗号化カラムを復号する
	 */
	private function decrypt_staff_row( array $row ): array {
		if ( ! empty( $row['email'] ) ) {
			try {
				$row['email'] = Encryption::instance()->decrypt( $row['email'] );
			} catch ( \RuntimeException $e ) {
				$row['email'] = '';
			}
		}

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

		return $row;
	}
}
