<?php

declare(strict_types=1);

namespace pictpostpersonal\model;

use PDO;
use Exception;
use pictpostpersonal\SQLiteCRUD;
use pictpostpersonal\SQL;
use pictpostpersonal\Utility;

/**
 * 認証トークンを管理するクラス
 */
class AuthToken
{
    // チェック状況をキャッシュする
    private static ?bool $check_token_cache = null;

    private PDO $db;
    private SQLiteCRUD $sql_crud;

    /**
     * コンストラクタ
     *
     * @param PDO $db データベース接続オブジェクト
     */
    public function __construct(PDO $db)
    {
        $this->db = $db;
        $this->sql_crud = new SQLiteCRUD($db);
        $this->initialize();
    }

    /**
     * 認証トークン格納用のテーブルを生成する
     *
     * @return void
     */
    private function initialize(): void
    {
        $this->sql_crud->table('authTokens')->createTable(
            [
                SQL::Id('id'),
                SQL::Text('token'),
                SQL::DateTime('expiration'),
                SQL::Text('uid'),
            ],
            [
                'token',
            ]
        );
    }

    /**
     * すべての認証トークンを取得する
     *
     * @return array 認証トークンの一覧
     */
    public function findAll(): array
    {
        return $this->sql_crud->table('authTokens')->fetch();
    }

    /**
     * トークンで認証トークンを検索する
     *
     * @param string $token トークン文字列
     * @return array 認証トークンの一覧
     */
    public function findByToken(string $token): array
    {
        return $this->sql_crud->table('authTokens')
            ->where('token = :token', ['token' => $token])
            ->fetch();
    }

    /**
     * 新しい認証トークンを作成する
     *
     * @param string $token トークン文字列
     * @param string $expiration 有効期限（Y-m-d H:i:s形式）
     * @param string $uid ユーザーID
     * @return int 作成された認証トークンのID
     */
    public function create(string $token, string $expiration, string $uid): int
    {
        $data = [
            'token'      => $token,
            'expiration' => $expiration,
            'uid'        => $uid,
        ];

        $result_id = $this->sql_crud->table('authTokens')->create($data);
        return (int)$result_id;
    }

    /**
     * 既存の認証トークンを更新する
     *
     * @param string $oldToken 古いトークン文字列
     * @param string $newToken 新しいトークン文字列
     * @param string $expiration 新しい有効期限（Y-m-d H:i:s形式）
     * @return void
     */
    public function update(string $old_token, string $new_token, string $expiration): void
    {
        $data = [
            'token'      => $new_token,
            'expiration' => $expiration,
        ];

        $this->sql_crud->table('authTokens')
            ->where('token = :token', ['token' => $old_token])
            ->update($data);
    }

    /**
     * 指定されたIDの認証トークンを削除する
     *
     * @param int $id 認証トークンのID
     * @return void
     */
    public function delete($id): void
    {
        $this->sql_crud->table('authTokens')->delete($id);
    }

    /**
     * 指定されたトークン文字列の認証トークンを削除する
     *
     * @param string $token トークン文字列
     * @return void
     */
    public function deleteByToken(string $token): void
    {
        $this->sql_crud->table('authTokens')
            ->where('token = :token', ['token' => $token])
            ->delete();
    }

    /**
     * 有効期限が過ぎた認証トークンを削除する
     *
     * @return void
     */
    public function deleteByExpiration(): void
    {
        $current_date_time = Utility::GetCurrentTime();
        $this->sql_crud->table('authTokens')
            ->where('expiration < :currentTime', ['currentTime' => $current_date_time])
            ->delete();
    }

    /**
     * ユーザーIDとパスワードを検証し、ログイン処理を行う
     *
     * @param string $user ユーザーID
     * @param string $password パスワード（平文）
     * @return bool 検証結果
     *
     * @throws Exception ログイントークンの発行に失敗した場合
     */
    public function login(string $user, string $password): bool
    {
        // /env/usersetting.phpを読み込み
        require_once(\pictpostpersonal\USER_PATH);
        //$sys_usernameおよび$sys_passwordはusersetting.phpにて定義されている

        if ($user === SYS_USERNAME && password_verify($password, SYS_PASSWORD)) {
            $this->publishToken($user);
            return true;
        }

        return false;
    }

    /**
     * ログアウト処理を行い、認証トークンを削除する
     *
     * @return void
     */
    public function logout(): void
    {
        $this->removeToken();
    }

    /**
     * ユーザーがログインしているかどうかをチェックする
     *
     * @return bool ログイン済みか否か
     */
    public function isLogin(): bool
    {
        return $this->checkToken();
    }

    /**
     * ログイントークンを発行し、DBに記録する
     * ログイントークンはCookieに記録される
     *
     * @param string $uid ユーザーID
     * @return void
     *
     * @throws Exception トークンの生成または保存に失敗した場合
     */
    private function publishToken(string $uid): void
    {
        // トークンを生成する
        $token = bin2hex(random_bytes(20));
        $enc_token = hash('sha256', $token);

        if (empty($_SERVER['HTTPS'])) {
            // HTTPの場合、短期間トークンにする
            $expiration_timestamp = time() + (3 * 60 * 60); // 有効期限は3時間
            $expiration = Utility::convertTimestampToDateTime($expiration_timestamp);
            $this->create($enc_token, $expiration, $uid);
            Utility::setCookie('n-token', $token, 0, false);
        } else {
            // HTTPSの場合、長期間トークンにする
            $expiration_timestamp = time() + (30 * 24 * 60 * 60); // 有効期限は30日後
            $expiration = Utility::convertTimestampToDateTime($expiration_timestamp);
            $this->create($enc_token, $expiration, $uid);
            Utility::setCookie('token', $token, $expiration_timestamp, true);
        }
    }

    /**
     * 現在使用しているトークンを削除する
     *
     * @return void
     */
    private function removeToken(): void
    {
        if (empty($_SERVER['HTTPS'])) {
            // HTTPの場合、短期間トークンにする
            $user_token = filter_input(INPUT_COOKIE, 'n-token');
            if ($user_token) {
                $enc_token = hash('sha256', $user_token);
                $this->deleteByToken($enc_token);
                Utility::setCookie('n-token', '--', time() + 1, false);
            }
        } else {
            // HTTPSの場合、長期間トークンにする
            $user_token = filter_input(INPUT_COOKIE, 'token');
            if ($user_token) {
                $enc_token = hash('sha256', $user_token);
                $this->deleteByToken($enc_token);
                Utility::setCookie('token', '--', time() + 1, true);
            }
        }
    }

    /**
     * トークンの有効期限をリフレッシュする
     *
     * @return void
     *
     * @throws Exception トークンの更新に失敗した場合
     */
    public function refreshToken(): void
    {
        $user_token = empty($_SERVER['HTTPS']) ?
            filter_input(INPUT_COOKIE, 'n-token') :
            filter_input(INPUT_COOKIE, 'token');

        if (empty($user_token)) {
            return;
        }

        $enc_token = hash('sha256', $user_token);
        $new_token = bin2hex(random_bytes(20));
        $enc_new_token = hash('sha256', $new_token);
        $new_expiration_timestamp = time() + (30 * 24 * 60 * 60); // 有効期限は30日後
        $new_expiration = Utility::convertTimestampToDateTime($new_expiration_timestamp);

        try {
            $this->sql_crud->begin();
            $this->update($enc_token, $enc_new_token, $new_expiration);
            $this->sql_crud->commit();

            if (empty($_SERVER['HTTPS'])) {
                Utility::setCookie('n-token', $new_token, 0, false);
            } else {
                Utility::setCookie('token', $new_token, $new_expiration_timestamp, true);
            }
        } catch (Exception $e) {
            $this->sql_crud->rollback();
            throw $e;
        }
    }

    /**
     * 現在使用しているトークンが有効かチェックする
     *
     * @return bool トークンが有効か否か
     */
    public function checkToken(): bool
    {
        if (self::$check_token_cache !== null) {
            return self::$check_token_cache;
        }

        $user_token = empty($_SERVER['HTTPS']) ?
            filter_input(INPUT_COOKIE, 'n-token') :
            filter_input(INPUT_COOKIE, 'token');

        if (empty($user_token)) {
            self::$check_token_cache = false;
            return false;
        }

        $enc_token = hash('sha256', $user_token);
        $tokens = $this->findByToken($enc_token);

        if (!empty($tokens) && strtotime($tokens[0]['expiration']) > time()) {
            self::$check_token_cache = true;
            return true;
        }

        self::$check_token_cache = false;
        return false;
    }
}
