<?php

namespace Acms\Services\Login\Traits;

use Acms\Services\Facades\Application;
use DB;
use SQL;

/**
 * 確認コードを発行し、本人確認するための機能
 */
trait VeryfyCode
{
    /**
     * 確認コードのタイプを取得
     *
     * @return string
     */
    abstract protected function getVerifyCodeType(): string;

    /**
     * ランダムなコードを生成
     *
     * @param string $key
     * @return string
     */
    protected function createVerifyCode(string $key, int $lifetime): string
    {
        $randomGenerator = Application::make('crypto.random');
        assert($randomGenerator instanceof \Acms\Services\Crypto\RandomGenerator);
        $code = strtoupper($randomGenerator->genPass(6));
        $this->saveVerifyCode($key, $code, $lifetime);

        return $code;
    }

    /**
     * あとで比較用にトークンを保存
     *
     * @param string $key
     * @param string $code
     * @param int $lifetime
     * @return void
     */
    protected function saveVerifyCode(string $key, string $code, int $lifetime): void
    {
        $type = $this->getVerifyCodeType();
        if (empty($key) || empty($type)) {
            return;
        }
        // 同じ key + type のトークンを先に消す。コード送信は認証不要で繰り返せるため、
        // 消さずに INSERT すると有効なコードが並行して増え、総当たりの当選確率がその分上がる。
        // removeVerifyCode() が key + type 単位でまとめて削除しているのと同じく、この組を
        // 「常に1つだけ有効な枠」として扱う。
        //
        // Why not: 削除に失敗してもコードの発行自体は続行する。発行を止めるとログインできなく
        // なり、影響が並行有効コードの残存より大きい（DB::query の exec は本番では例外ではなく
        // false を返すため、失敗は戻り値でしか判別できない）。
        $sql = SQL::newDelete('token');
        $sql->addWhereOpr('token_key', $key);
        $sql->addWhereOpr('token_type', $type);
        DB::query($sql->get(dsn()), 'exec');

        $sql = SQL::newInsert('token');
        $sql->addInsert('token_key', $key);
        $sql->addInsert('token_type', $type);
        $sql->addInsert('token_value', hash('sha256', $code));
        $sql->addInsert('token_expire', date('Y-m-d H:i:s', REQUEST_TIME + $lifetime));
        DB::query($sql->get(dsn()), 'exec');
    }

    /**
     * コードを確認
     *
     * @param string $key
     * @param string $code
     * @return bool
     */
    protected function varifyCode(string $key, string $code): bool
    {
        $type = $this->getVerifyCodeType();
        if (empty($key) || empty($type) || empty($code)) {
            return false;
        }
        $sql = SQL::newSelect('token');
        $sql->addWhereOpr('token_key', $key);
        $sql->addWhereOpr('token_type', $type);
        $sql->addWhereOpr('token_value', hash('sha256', $code));
        $sql->addWhereOpr('token_expire', date('Y-m-d H:i:s', REQUEST_TIME), '>');
        $t = DB::query($sql->get(dsn()), 'row');
        if (!isset($t['token_value'])) {
            return false;
        }
        if (hash_equals((string) $t['token_value'], hash('sha256', $code))) {
            return true;
        }
        return false;
    }

    /**
     * 使用したトークンを削除
     *
     * @param string $key
     * @return void
     */
    protected function removeVerifyCode(string $key): void
    {
        $type = $this->getVerifyCodeType();
        if (empty($key) || empty($type)) {
            return;
        }
        // 使用済みのトークンを削除
        $sql = SQL::newDelete('token');
        $sql->addWhereOpr('token_key', $key);
        $sql->addWhereOpr('token_type', $type);
        DB::query($sql->get(dsn()), 'exec');

        // 有効期限切れのトークンを削除
        $sql = SQL::newDelete('token');
        $sql->addWhereOpr('token_expire', date('Y-m-d H:i:s', REQUEST_TIME), '<');
        DB::query($sql->get(dsn()), 'exec');
    }
}
