<?php

namespace Acms\Services\Login\Traits;

use ACMS_Validator;
use Acms\Services\Facades\Logger as AcmsLogger;
use Acms\Services\Facades\RateLimiter;
use Acms\Services\Facades\Tfa;
use Acms\Services\Login\Enums\TfaAuthResult;
use Acms\Services\Login\LockKeyPolicy;
use Field_Validation;

/**
 * 2段階認証コード入力後の判定・総当たり対策
 *
 * 会員側（ACMS_POST_Member_Tfa_Auth）・管理側（ACMS_POST_Member_Admin_Tfa_Auth）は
 * ロック scope 文字列（tfaLockScope()）以外のロジックが完全に同一のため、ここに集約する。
 */
trait TfaLockAttempt
{
    /**
     * 2段階認証の試行ロックの scope 文字列
     *
     * Why not: パスワード側ロック（Signin.php）は 'Member_Signin' を会員/管理ログインで
     * 共有しているが、これは既存仕様の踏襲にすぎず新規カウンタでそれを踏襲する理由がない。
     * 会員側の2段階認証ロックが管理側の2段階認証ロックに影響しないよう scope を分ける。
     *
     * @return string
     */
    abstract protected function tfaLockScope(): string;

    /**
     * 2段階認証のアクション
     * 戻り値が true だと、そこで処理をやめる
     *
     * @param Field_Validation $loginField
     * @param int $uid
     * @return bool
     */
    protected function checkTowFactorAuthAction(Field_Validation $loginField, int $uid): bool
    {
        // Why not: ログインの lockKey は入力ID（表記）に依存し、パスワードは既に一致済みの
        // ためこの時点でパスワード側の予算を消費させても総当たりを止められない。
        // 2段階認証の時点では uid が確定しているので uid で別バケットのキーを作り、
        // パスワード側の予算と分離したうえでここでロックを判定する。
        $lockKey = LockKeyPolicy::forUserId($this->tfaLockScope(), $uid);
        $trialTime = intval(config('login_trial_time', 5));
        $trialNumber = intval(config('login_trial_number', 5));
        $lockTime = intval(config('login_lock_time', 5));
        // Why not: $remoteAddr を false にして接続元IPに依存しないカウンタにする。ここに
        // 到達するには既にパスワード照合済み（Signin::post() がパスワード不一致なら
        // checkTowFactorAuthAction() を呼ばない）のため、IPを分散されても total で
        // カウントし続けたい（分散されると各IPが個別の5回予算を得てしまい、6桁コードの
        // 総当たりが継続できてしまう）。パスワード側ロック（Signin.php）は未認証の第三者が
        // ID を知るだけでトリガーできるため IP 依存を維持するが、ここはパスワードを
        // 知っている前提がある分、悪用のハードルが高くトレードオフが異なる。
        if (RateLimiter::validateLockPost($lockKey, $trialTime, $trialNumber, $lockTime, false) === false) {
            // Why not: ロック中に表示するテキストは「2段階認証コードの失敗」向けに書き分けているため
            // 誤解の余地はない。'mail' フィールドに紐づけているのは、パスワード側ロック
            // （Signin.php）と同じ Field / validator メソッド名にしておくことで、カスタムテーマが
            // 独自に {code:validator#lock} のような新しいトークンを実装し直す必要をなくすため
            // （既存テーマ互換優先）。
            $loginField->setMethod('mail', 'lock', false);
            AcmsLogger::notice('2段階認証の試行回数を超えているため、2段階認証に失敗しました', [
                'uid' => $uid,
                'lockKey' => $lockKey,
                'trialTime' => $trialTime,
                'trialNumber' => $trialNumber,
                'lockTime' => $lockTime,
            ]);
            $loginField->validate(new ACMS_Validator());
            return true;
        }

        $inputCode = preg_replace("/(\s|　)/", "", $loginField->get('code'));
        $result = Tfa::authenticate($uid, $inputCode);
        if ($result === TfaAuthResult::SUCCESS) {
            AcmsLogger::info('2段階認証に成功しました', [
                'uid' => $uid,
            ]);
            return false;
        }
        if ($result === TfaAuthResult::DECRYPT_FAILED) {
            $loginField->setMethod('code', 'secretKey', false);
            AcmsLogger::warning('秘密鍵の復号に失敗したため2段階認証を中断しました', [
                'uid' => $uid,
            ]);
        } else {
            $loginField->setMethod('code', 'auth', false);
            AcmsLogger::info('2段階認証に失敗しました', [
                'uid' => $uid,
            ]);
        }
        // Why not: 「DECRYPT_FAILED だけ if で除外し、それ以外は else でまとめて数える」という
        // 書き方だと、TfaAuthResult に case を追加したときに黙って数え忘れる／数えすぎる回帰を
        // 検知できない。判定を TfaAuthResult::shouldCountAsLockAttempt() 側に持たせ、
        // ここでは呼ぶだけにすることで、そちらのテストで固定できるようにする。
        if ($result->shouldCountAsLockAttempt()) {
            RateLimiter::logLockPost($lockKey);
        }
        $loginField->validate(new ACMS_Validator());
        return true;
    }
}
