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

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


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

/**
 * フロントエンドからの自己登録（会員/非会員）
 *
 * OTPメール認証 → プロフィール入力 → 会員レコード作成 のフローを管理。
 *
 * @since 1.1.0
 */
class SelfRegistration {

	use Singleton;

	/**
	 * OTP有効期限（秒）
	 */
	private const OTP_TTL = 600;

	/**
	 * OTP最大試行回数
	 *
	 * ⚠️ **実態はアドレス単位。** キーは `conf_selfreg_attempts_{email_hash}` で
	 * IP を含まない（以前このコメントは「IP単位」と書いていたが誤り）。
	 * IP を変えても回避できないので防御としてはそのほうが強いが、
	 * 「IP 単位の防御もある」と読むと総当たり耐性を過大評価する。
	 */
	private const OTP_MAX_ATTEMPTS = 5;

	/**
	 * OTPグローバル最大試行回数（メール単位、IP非依存）
	 * 分散ブルートフォース攻撃対策
	 *
	 * ⚠️ 上の OTP_MAX_ATTEMPTS と**同じキー体系・同じ増減・同じリセット**なので、
	 * 実際に先に発動するのは常に厳しい側（5 回）で、この 10 回には到達しない。
	 * 分散攻撃への上乗せにはなっていない。挙動を変えると総当たり耐性が下がる向きに
	 * なりうるため据え置き、事実だけ記録する。
	 */
	private const OTP_GLOBAL_MAX_ATTEMPTS = 10;

	/**
	 * 登録トークン有効期限（秒）
	 */
	private const TOKEN_TTL = 900;

	/**
	 * 有効な registered_via 値
	 */
	private const VALID_VIA = [ 'admin', 'csv', 'self_registration' ];

	protected function init(): void {}

	/**
	 * 認証メール共通フッターを生成する
	 *
	 * 差出人組織の identity と連絡先を本文に明示することで、
	 * 素っ気ないコード単体メールがフィッシングとして誤判定され
	 * 迷惑メール振り分け・受信拒否されるリスクを低減する。
	 */
	private function build_mail_footer(): string {
		$site_name  = (string) get_option( 'blogname', '' );
		$admin_mail = (string) get_option( 'admin_email', '' );
		$home_url   = home_url( '/' );

		$footer = "\n" . str_repeat( '━', 26 ) . "\n";
		if ( '' !== $site_name ) {
			$footer .= $site_name . "\n";
		}
		$footer .= sprintf(
			/* translators: %s: site name */
			__( 'This email was sent automatically by the registration system of %s.', 'conferencemanager' ),
			$site_name
		) . "\n";
		if ( '' !== $admin_mail && is_email( $admin_mail ) ) {
			/* translators: %s: administrator email address */
			$footer .= sprintf( __( 'Contact: %s', 'conferencemanager' ), $admin_mail ) . "\n";
		}
		if ( '' !== $home_url ) {
			/* translators: %s: site URL */
			$footer .= sprintf( __( 'Website: %s', 'conferencemanager' ), $home_url ) . "\n";
		}
		return $footer;
	}

	/**
	 * OTPを生成してメール送信する
	 *
	 * メール列挙攻撃を防ぐため、既存会員の場合も同一レスポンスを返す。
	 *
	 * @param string $email メールアドレス
	 * @return array{sent: bool}|\WP_Error
	 */
	public function send_otp( string $email ): array|\WP_Error {
		$email = sanitize_email( $email );
		if ( empty( $email ) || ! is_email( $email ) ) {
			return new \WP_Error( 'invalid_email', __( 'Please enter a valid email address.', 'conferencemanager' ), [ 'status' => 400 ] );
		}

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

		// IP + メール複合レート制限
		$rate_check = RateLimiter::instance()->check_composite( 'self_register_otp', 'otp_per_email', $email_hash );
		if ( is_wp_error( $rate_check ) ) {
			return $rate_check;
		}

		$site_name = (string) get_option( 'blogname', '' );

		// 既存会員チェック（列挙攻撃防止: 同一レスポンスを返す）
		$existing = MemberManager::instance()->get_by_email( $email );
		if ( null !== $existing ) {
			// 既存会員にはログイン案内メールを送信（OTPは発行しない）
			$subject = sprintf(
				/* translators: %s: site name */
				__( '[%s] Registration Information', 'conferencemanager' ),
				$site_name
			);
			$body  = sprintf(
				/* translators: %s: site name */
				__( 'Thank you for using the registration service of %s.', 'conferencemanager' ),
				$site_name
			) . "\n\n";
			$body .= __( 'This email address is already registered as a member.', 'conferencemanager' ) . "\n\n";
			$body .= __( 'Please log in from My Page below to register for the event.', 'conferencemanager' ) . "\n\n";
			// ⚠️ URL は TransactionalMailer に寄せること（案内先の食い違いを防ぐ）。
			$body .= \ConfManager\Notification\TransactionalMailer::instance()->get_mypage_url() . "\n\n";
			$body .= __( 'If you do not recognize this request, please discard this email.', 'conferencemanager' );
			$body .= $this->build_mail_footer();
			$mail_sent = wp_mail( $email, $subject, $body );

			AuditLog::instance()->log( 'self_register_existing_member', 'member', (int) $existing['id'], [
				'email_hash' => $email_hash,
			] );

			// 🔴 列挙攻撃の対称性。既存会員あてと新規あてで**返り値が完全に一致**すること。
			//    片方だけ失敗を伝えると、受信を拒否するドメインのアドレスを投げるだけで
			//    「そのアドレスが会員かどうか」が読める。
			if ( ! $mail_sent ) {
				return $this->mail_failed_error();
			}

			return [ 'sent' => true ];
		}

		// 6桁OTP生成
		$otp = str_pad( (string) random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );

		// ハッシュしてTransientに保存
		$otp_key = 'conf_selfreg_otp_' . $email_hash;
		set_transient( $otp_key, wp_hash( $otp ), self::OTP_TTL );

		// 試行回数リセット（どちらもアドレス単位。上の定数コメント参照）
		delete_transient( 'conf_selfreg_attempts_' . $email_hash );
		delete_transient( 'conf_selfreg_gattempts_' . $email_hash );

		// メール送信
		$subject = sprintf(
			/* translators: %s: site name */
			__( '[%s] Registration Email Verification Code', 'conferencemanager' ),
			$site_name
		);
		$body  = sprintf(
			/* translators: %s: site name */
			__( 'Thank you for using the registration service of %s.', 'conferencemanager' ),
			$site_name
		) . "\n\n";
		$body .= __( 'Here is the email verification code to enter on the registration page.', 'conferencemanager' ) . "\n\n";
		$body .= sprintf(
			/* translators: %s: OTP code */
			__( 'Verification code: %s', 'conferencemanager' ),
			$otp
		) . "\n\n";
		$body .= __( 'This code is valid for 10 minutes. Please enter it in the field on the registration page.', 'conferencemanager' ) . "\n";
		$body .= __( 'Never share this verification code with anyone. Our staff will never ask you for the code by email or phone.', 'conferencemanager' ) . "\n\n";
		$body .= __( 'If you do not recognize this email, please discard it.', 'conferencemanager' ) . "\n";
		$body .= $this->build_mail_footer();

		$mail_sent = wp_mail( $email, $subject, $body );

		AuditLog::instance()->log( 'self_register_otp_sent', 'member', null, [
			'email_hash' => $email_hash,
		] );

		// 🔴 既存会員あての分岐と**同じもの**を返すこと（列挙攻撃の対称性）。
		if ( ! $mail_sent ) {
			return $this->mail_failed_error();
		}

		return [ 'sent' => true ];
	}

	/**
	 * メール送信に失敗したときのエラー
	 *
	 * 🔴 **既存会員あて・新規あての両方の分岐から、これ 1 つだけを返す。**
	 * 片方だけ別のものを返すと、受信を拒否するドメインのアドレスを投げるだけで
	 * 「そのアドレスが会員かどうか」が読めてしまう。
	 *
	 * ⚠️ マイページ側（PublicApi::send_otp()）は**あえて直していない**。
	 * あちらは「会員が存在しないときは送信せずに sent: true を返す」分岐があるため、
	 * 失敗を伝えると **存在しないアドレス＝必ず成功／存在するアドレス＝失敗し得る**
	 * という差が生まれ、同じ列挙が成立してしまう。
	 */
	private function mail_failed_error(): \WP_Error {
		return new \WP_Error(
			'mail_failed',
			__( 'Failed to send the email. Please check the address and try again, or contact the organizer.', 'conferencemanager' ),
			[ 'status' => 500 ]
		);
	}

	/**
	 * OTPを検証して登録トークンを発行する
	 *
	 * @param string $email メールアドレス
	 * @param string $otp   OTPコード
	 * @return array{verified: bool, registration_token: string}|\WP_Error
	 */
	public function verify_otp( string $email, string $otp ): array|\WP_Error {
		$rate_check = RateLimiter::instance()->check( 'self_register_verify' );
		if ( is_wp_error( $rate_check ) ) {
			return $rate_check;
		}

		$email = sanitize_email( $email );
		$otp   = sanitize_text_field( $otp );

		if ( empty( $email ) || ! is_email( $email ) || empty( $otp ) ) {
			return new \WP_Error( 'invalid_params', __( 'Email address and verification code are required.', 'conferencemanager' ), [ 'status' => 400 ] );
		}

		$email_hash          = Encryption::hash_email( $email );
		$otp_key             = 'conf_selfreg_otp_' . $email_hash;
		$attempts_key        = 'conf_selfreg_attempts_' . $email_hash;
		$global_attempts_key = 'conf_selfreg_gattempts_' . $email_hash;

		// グローバル試行回数チェック（IP非依存、分散攻撃対策）
		$global_attempts = (int) get_transient( $global_attempts_key );
		if ( $global_attempts >= self::OTP_GLOBAL_MAX_ATTEMPTS ) {
			delete_transient( $otp_key );
			return new \WP_Error( 'too_many_attempts', __( 'Maximum attempts reached. Please resend the verification code.', 'conferencemanager' ), [ 'status' => 429 ] );
		}

		// アドレス単位の試行回数チェック（キーに IP は入らない）
		$attempts = (int) get_transient( $attempts_key );
		if ( $attempts >= self::OTP_MAX_ATTEMPTS ) {
			delete_transient( $otp_key );
			return new \WP_Error( 'too_many_attempts', __( 'Maximum attempts reached. Please resend the verification code.', 'conferencemanager' ), [ 'status' => 429 ] );
		}

		$stored_hash = get_transient( $otp_key );
		if ( false === $stored_hash ) {
			return new \WP_Error( 'otp_expired', __( 'Verification code has expired. Please resend.', 'conferencemanager' ), [ 'status' => 401 ] );
		}

		if ( ! hash_equals( $stored_hash, wp_hash( $otp ) ) ) {
			set_transient( $attempts_key, $attempts + 1, self::OTP_TTL );
			set_transient( $global_attempts_key, $global_attempts + 1, self::OTP_TTL );
			return new \WP_Error( 'otp_invalid', __( 'Incorrect verification code.', 'conferencemanager' ), [ 'status' => 401 ] );
		}

		// OTP消費
		delete_transient( $otp_key );
		delete_transient( $attempts_key );
		delete_transient( $global_attempts_key );

		// 登録トークンを発行（メールアドレスを暗号化して紐付け）
		$token     = bin2hex( random_bytes( 32 ) );
		$token_key = 'conf_selfreg_token_' . $token;
		set_transient( $token_key, Encryption::instance()->encrypt( $email ), self::TOKEN_TTL );

		AuditLog::instance()->log( 'self_register_otp_verified', 'member', null, [
			'email_hash' => $email_hash,
		] );

		return [
			'verified'           => true,
			'registration_token' => $token,
		];
	}

	/**
	 * 登録トークンを検証しプロフィールで会員レコードを作成する
	 *
	 * @param string $token 登録トークン
	 * @param array  $data  {
	 *     @type string $name_sei      姓（必須）
	 *     @type string $name_mei      名（必須）
	 *     @type string $name_sei_kana 姓カナ
	 *     @type string $name_mei_kana 名カナ
	 *     @type string $org_name      所属
	 *     @type string $org_dept      部署
	 *     @type bool   $is_member     会員として登録するか
	 *     @type string $member_type   会員種別（会員の場合のみ）
	 * }
	 * @return array{member_id: int, is_member: bool, auth_token: string}|\WP_Error
	 */
	public function complete( string $token, array $data ): array|\WP_Error {
		$rate_check = RateLimiter::instance()->check( 'registration' );
		if ( is_wp_error( $rate_check ) ) {
			return $rate_check;
		}

		$token         = sanitize_text_field( $token );
		$token_key     = 'conf_selfreg_token_' . $token;
		$encrypted_email = get_transient( $token_key );

		if ( false === $encrypted_email ) {
			return new \WP_Error( 'invalid_token', __( 'Registration token is invalid or expired. Please start over.', 'conferencemanager' ), [ 'status' => 401 ] );
		}

		// トークン消費
		delete_transient( $token_key );

		// 暗号化されたメールを復号（旧形式の平文にも対応）
		$email = Encryption::instance()->decrypt( $encrypted_email );
		if ( false === $email ) {
			// 旧形式（平文）のTransientの場合はそのまま使用
			$email = is_email( $encrypted_email ) ? $encrypted_email : null;
		}

		if ( empty( $email ) ) {
			return new \WP_Error( 'invalid_token', __( 'Registration token is invalid or expired. Please start over.', 'conferencemanager' ), [ 'status' => 401 ] );
		}

		// 念のため重複チェック
		$existing = MemberManager::instance()->get_by_email( $email );
		if ( null !== $existing ) {
			return new \WP_Error( 'already_registered', __( 'This email address is already registered.', 'conferencemanager' ), [ 'status' => 409 ] );
		}

		$is_member   = ! empty( $data['is_member'] );
		$member_type = $is_member
			? sanitize_text_field( $data['member_type'] ?? 'regular' )
			: 'general';

		$require_approval = (bool) get_option( 'conf_manager_require_approval', false );

		$create_data = [
			'email'           => $email,
			'name_sei'        => $data['name_sei'] ?? '',
			'name_mei'        => $data['name_mei'] ?? '',
			'name_sei_kana'   => $data['name_sei_kana'] ?? '',
			'name_mei_kana'   => $data['name_mei_kana'] ?? '',
			'org_name'        => $data['org_name'] ?? '',
			'org_dept'        => $data['org_dept'] ?? '',
			'is_member'       => $is_member,
			'member_type'     => $member_type,
			'email_verified'  => true,
			'registered_via'  => 'self_registration',
			'approval_status' => $require_approval ? 'pending' : 'auto_approved',
			'status'          => $require_approval ? 'inactive' : 'active',
		];

		$member_id = MemberManager::instance()->create( $create_data );
		if ( is_wp_error( $member_id ) ) {
			return $member_id;
		}

		AuditLog::instance()->log( 'self_register_complete', 'member', $member_id, [
			'is_member'        => $is_member,
			'member_type'      => $member_type,
			'require_approval' => $require_approval,
		] );

		// 承認待ちの場合はセッショントークンを発行しない（申込不可）
		if ( $require_approval ) {
			return [
				'member_id'        => $member_id,
				'is_member'        => $is_member,
				'pending_approval' => true,
			];
		}

		// セッショントークンを発行（申込フローで使用）
		$auth_token = bin2hex( random_bytes( 32 ) );
		set_transient( 'conf_otp_session_' . $auth_token, $member_id, self::TOKEN_TTL );

		$member = MemberManager::instance()->get( $member_id );

		return [
			'member_id'   => $member_id,
			'is_member'   => $is_member,
			'auth_token'  => $auth_token,
			'email_hash'  => $member['email_hash'] ?? Encryption::hash_email( $email ),
			'name_sei'    => $member['name_sei'] ?? '',
			'name_mei'    => $member['name_mei'] ?? '',
			'member_type' => $member['member_type'] ?? $member_type,
			'org_name'    => $member['org_name'] ?? '',
		];
	}
}
