<?php

use PragmaRX\Google2FA\Google2FA;

class TwoFaHelper
{
    public const SECRET_KEY_LENGTH = 32;
    public const TWO_FA_TOKEN_HEADER = 'Cf-2fa-Token';
    public const AUTHORIZATION_HEADER = 'Authorization';
    public const BEARER_PREFIX = 'Bearer ';

    public static function getInstance(): Google2FA
    {
        return new Google2FA();
    }

    /**
     * @param $key
     * @param $secret
     * @param $accessToken. If access token is provided, then access token will be marked as verified in case of valid 2FA code
     * @return bool
     * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
     * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
     * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
     */
    public static function verify($key, $secret, $accessToken = null): bool
    {
        $twoFaModel = self::getInstance();
        $isValid = $twoFaModel->verify($key, $secret);
        if ($accessToken && $isValid) {
            self::updateAccessTokeVerificationTime($accessToken);
        }
        return $isValid;
    }

    /**
     * @param string $username
     * @param array $headers
     * @return bool
     * @throws ResponseException
     */
    public static function verifyRequest(CfUsers $cfUser, array $headers = []): bool
    {
        $accessToken = self::getAccessTokenFromHeaders($headers);
        if (
            $cfUser->get2FaEnabled() &&
            self::isAccessTokenShouldBeVerified($accessToken) &&
            (
                !isset($headers[self::TWO_FA_TOKEN_HEADER]) ||
                !self::verify($headers[self::TWO_FA_TOKEN_HEADER], $cfUser->get2FaTotpSecret(), $accessToken)
            )
        ) {
            throw new ResponseException('Invalid two-factor authentication code.', Response::UNAUTHORIZED);
        }

        return true;
    }

    public static function isAccessTokenShouldBeVerified($accessToken): bool
    {
        $shouldBeVerified = true;

        if ($accessToken != null) {
            $server = Utils::getOauthServer();
            $accessTokenStorage = $server->getStorage('access_token');
            $lastTimeVerified = $accessTokenStorage->getToken2FaVerified($accessToken);
            if ($lastTimeVerified) {
                $lastTimeVerifiedDateTime = new DateTime($lastTimeVerified);
                $lastTimeVerifiedDateTime->modify('+'. TWO_FA_VERIFICATION_EXPIRES_MINUTES .' minutes');

                $now = new DateTime();
                $shouldBeVerified = $now >= $lastTimeVerifiedDateTime;
            }

        }

        return $shouldBeVerified;
    }

    public static function updateAccessTokeVerificationTime($accessToken): void
    {
        if ($accessToken != null) {
            $server = Utils::getOauthServer();
            $accessTokenStorage = $server->getStorage('access_token');
            $accessTokenStorage->setToken2FaVerified($accessToken);
        }
    }

    public static function getAccessTokenFromHeaders($headers = []): ?string
    {
        $accessToken = null;
        if (isset($headers[self::AUTHORIZATION_HEADER]) && str_starts_with($headers[self::AUTHORIZATION_HEADER], self::BEARER_PREFIX)) {
            $accessToken = substr($headers[self::AUTHORIZATION_HEADER], strlen(self::BEARER_PREFIX));
        }
        return $accessToken;
    }
}
