<?php

use Acms\Services\Facades\Common;
use Acms\Services\Login\Exceptions\BadRequestException;
use Acms\Services\Login\Exceptions\ExpiredException;
use Acms\Services\Login\LockKeyPolicy;

class ACMS_POST_Member_SigninWithVerifyCode extends ACMS_POST_Member_SigninWithEmail
{
    /**
     * 正常なルートからのPOSTかどうかをチェック
     *
     * @inheritDoc
     */
    protected function isValidPostRoute(): bool
    {
        return Login::canLoginPage(BID, SIGNIN_SEGMENT);
    }

    /**
     * Run
     *
     * @inheritDoc
     */
    public function post()
    {
        $loginField = $this->extract('login');
        $email = preg_replace("/(\s|　)/", "", $loginField->get('mail'));
        $code = $loginField->get('code');
        $lockKey = LockKeyPolicy::forInputId('SigninWithEmail', (string) $email);
        $data = [];

        $loginField->deleteField('sent');

        // ユーザー決定前のバリデート
        if ($this->passwordAuth()) {
            $loginField->setMethod('mailAuthSignin', 'enable', false);
        }
        $this->preValidate($loginField, $email, $lockKey);
        if (!$this->Post->isValidAll()) {
            return $this->Post;
        }

        // Why not: user_mail の一致判定は DB の照合順序（大文字小文字・アクセント記号・
        // 全角半角などを同一視）で行われる一方、$lockKey は入力文字列の表記に依存するため、
        // 表記を変えるだけで別バケットを得られ続ける（確認コードは6桁の16進数のため
        // ワンタイムパスワードと同程度の総当たり耐性しかない）。確認コードを検証する前に
        // 入力メールアドレスが一意なユーザーへ解決できるかどうかを別途調べ、解決できた場合は
        // uid ベースの lockKey でも判定・記録する。uid は表記に依存しないため回避できない。
        $resolvedUserId = $this->resolveUserIdForLock((string) $email);
        $uidLockKey = $resolvedUserId !== null ? LockKeyPolicy::forUserId('SigninWithEmail', $resolvedUserId) : null;
        if ($uidLockKey !== null) {
            $trialTime = intval(config('login_trial_time', 5));
            $trialNumber = intval(config('login_trial_number', 5));
            $lockTime = intval(config('login_lock_time', 5));
            if (RateLimiter::validateLockPost($uidLockKey, $trialTime, $trialNumber, $lockTime) === false) {
                $loginField->setMethod('mail', 'lock', false);
                AcmsLogger::notice('ログイン試行回数を超えているため、ログインに失敗しました', [
                    'email' => $email,
                    'lockKey' => $uidLockKey,
                    'trialTime' => $trialTime,
                    'trialNumber' => $trialNumber,
                    'lockTime' => $lockTime,
                ]);
                $loginField->validate(new ACMS_Validator());
                return $this->Post;
            }
        }

        if (!$this->varifyCode($email, $code)) {
            $loginField->setMethod('code', 'auth', false);
            $loginField->validate(new ACMS_Validator());
            RateLimiter::logLockPost($lockKey);
            if ($uidLockKey !== null) {
                RateLimiter::logLockPost($uidLockKey);
            }
            return $this->Post;
        }

        try {
            $all = $this->find($email, '');

            // ユーザーが見つからない or 複数見つかった
            if (empty($all) || 1 < count($all)) {
                RateLimiter::logLockPost($lockKey);
                if ($uidLockKey !== null) {
                    RateLimiter::logLockPost($uidLockKey);
                }
                throw new BadRequestException('アカウントが存在しないため、不正なリクエストと判断しました');
            }

            // 一意のユーザー
            $user = $all[0];
            $uid = intval($user['user_id']);

            // ログイン確定（ログイン状態の記録＋セッション生成）
            Login::authenticateUser($uid);
            $this->removeVerifyCode($email);

            AcmsLogger::info('ユーザー「' . ACMS_RAM::userName($uid) . '」がサインインしました', [
                'id' => $uid,
            ]);

            Webhook::call(BID, 'user', ['user:login'], $uid);

            // リダイレクト処理
            Login::loginRedirect(ACMS_RAM::user($uid), '');
        } catch (BadRequestException $e) {
            $loginField->setMethod('code', 'request', false);
            AcmsLogger::notice('不正な確認コードのため、メール認証サインインに失敗しました', Common::exceptionArray($e, $data));
        } catch (ExpiredException $e) {
            $loginField->setMethod('code', 'expired', false);
            AcmsLogger::notice('有効期限切れの確認コードのため、メール認証サインインに失敗しました', Common::exceptionArray($e, $data));
        }
        $loginField->validate(new ACMS_Validator());

        return $this->Post;
    }

    /**
     * ロックのバケット判定用に、入力メールアドレスから一意に解決できるユーザーIDを取得する
     *
     * Why not: 継承元 Signin::resolveUserIdForLock() は user_code / user_mail の両方を見る
     * buildUserLookupSql() を使うが、このクラスの認証経路（find()）は user_mail のみで
     * 一致判定するオーバーライド済みのメソッドのため、そのまま使うと lockKey 判定時の一致条件と
     * 実際の認証時の一致条件がずれる（片方だけ一意に解決できるケースが生まれうる）。
     * find() 自身が元からパスワード条件を持たないため、Signin::resolveUserIdForLock() のように
     * 別の絞り込みを新たに組み立てる必要もない。
     *
     * @param string $id
     * @return int|null 一意に解決できない場合（0件・複数件の曖昧一致）は null
     */
    protected function resolveUserIdForLock(string $id): ?int
    {
        if ($id === '') {
            return null;
        }
        $all = $this->find($id, '');
        if (count($all) !== 1) {
            return null;
        }
        return intval($all[0]['user_id']);
    }
}
