<?php

use events\BaseEvents;
use events\dto\UserEventsDto;
use events\EventService;
use PragmaRX\Google2FA\Google2FA;

class BaseTotpController extends CfProtectedResource
{
    /**
     * @var Google2FA
     */
    protected $google2fa;

    /**
     * @var CfUsers
     */
    protected $user;

    public function __construct($parameters)
    {
        parent::__construct($parameters);

        $this->google2fa = TwoFaHelper::getInstance();
        $this->user = new CfUsers($this->username);
    }
}

/**
 * @uri /2fa/totp/configure
 */
class TotpConfigure extends BaseTotpController
{
    public function get($request): Response
    {
        $user = $this->username;
        $response = new Response($request);

        $secretKey = $this->google2fa->generateSecretKey(TwoFaHelper::SECRET_KEY_LENGTH);
        $g2faUrl = $this->google2fa->getQRCodeUrl(company: APP_NAME, holder: $user, secret: $secretKey);

        if ($this->user->get2FaEnabled()) {
            $response->code = Response::CONFLICT;
            $response->body = '2FA is already enabled for this user.';
            return $response;
        }

        $this->user->set2FaTotpSecret($secretKey);

        $response->body = json_encode([
            'secret' => $secretKey,
            '2faUrl' => $g2faUrl,
            'algorithm' => $this->google2fa->getAlgorithm(),
            'digits' => $this->google2fa->getOneTimePasswordLength(),
            'period' => $this->google2fa->getKeyRegeneration(),
            'issuer' => APP_NAME,
            'holder' => $user
        ]);

        return $response;
    }

    public function post($request): Response
    {
        $response = new Response($request);
        $data = Utils::getValidJsonData($request->data);

        if ($this->user->get2FaEnabled()) {
            $response->code = Response::CONFLICT;
            $response->body = '2FA is already enabled for this user.';
            return $response;
        }

        if (!$this->user->get2FaTotpSecret()) {
            $response->code = Response::BADREQUEST;
            $response->body = '2FA is not configured for this user.';
            return $response;
        }

        if (!$this->google2fa->verifyKey($this->user->get2FaTotpSecret(), (string)$data->code)) {
            $response->code = Response::BADREQUEST;
            $response->body = '2FA verification failed.';
            return $response;
        }

        $this->user->set2FaEnabled(true);
        $this->user->resetFirstLoginAfter2faEnforced();

        AuditLogService::register([
            AuditLogFields::ACTOR => $this->username,
            AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
            AuditLogFields::OBJECT_ID => $this->username,
            AuditLogFields::OBJECT_NAME => $this->username,
            AuditLogFields::ACTION => AuditLogActions::UPDATE,
            AuditLogFields::DETAILS => ['Enabled 2FA for themself.']
        ]);

        EventService::dispatchEvent(
            BaseEvents::TWO_FA_CONFIGURED,
            new UserEventsDto(
                username: $this->username,
                oauthToken: $this->accessToken,
            )
        );

        $response->code = Response::OK;
        $response->body = '2FA successfully enabled for this user.';
        return $response;
    }
}

/**
 * @uri /2fa/totp/verify
 */
class TotpVerify extends BaseTotpController
{
    public function post($request): Response
    {
        $response = new Response($request);
        $data = Utils::getValidJsonData($request->data);

        $CfFailedAttempts = new CfFailedAttempts(
            ipAddress: $_SERVER['REMOTE_ADDR'],
            action: CfFailedAttempts::TWO_FA_ACTION,
            delayInSeconds: [60],
            attemptsThreshold: 5,
            resetAttemptsAfterSec: 60
        );

        $timeout = $CfFailedAttempts->timeOutBeforeNextAttempt();
        if ($timeout !== null) {
            throw new ResponseException(
                "We have detected multiple unsuccessful 2FA verification attempts. Please try again after $timeout seconds.",
                Response::TOO_MANY_REQUESTS
            );
        }

        if (!$this->user->get2FaEnabled() || !$this->user->get2FaTotpSecret()) {
            $response->code = Response::OK;
            $response->body = '2FA is not configured for this user.';
            return $response;
        }

        if (!$this->google2fa->verifyKey($this->user->get2FaTotpSecret(), (string)$data->code)) {
            $response->code = Response::UNPROCESSABLE_ENTITY;
            $response->body = '2FA verification failed.';
            $CfFailedAttempts->logFailedAttempt();
            return $response;
        }

        TwoFaHelper::updateAccessTokeVerificationTime(
            TwoFaHelper::getAccessTokenFromHeaders(getallheaders())
        );
        $response->code = Response::OK;
        $response->body = '2FA successfully verified.';
        return $response;
    }
}

/**
 * @uri /2fa/totp/disable
 */
class TotpDisable extends BaseTotpController
{
    public function post($request)
    {
        $response = new Response($request);

        if (!$this->user->get2FaEnabled()) {
            $response->code = Response::CONFLICT;
            $response->body = '2FA is not enabled for this user.';
            return $response;
        }

        TwoFaHelper::verifyRequest((new CfUsers($this->username)), getallheaders());

        $this->user->set2FaEnabled(false);
        $this->user->set2FaTotpSecret(null);

        $response->code = Response::OK;
        $response->body = '2FA successfully disabled for this user.';

        AuditLogService::register([
            AuditLogFields::ACTOR => $this->username,
            AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
            AuditLogFields::OBJECT_ID => $this->username,
            AuditLogFields::OBJECT_NAME => $this->username,
            AuditLogFields::ACTION => AuditLogActions::UPDATE,
            AuditLogFields::DETAILS => ['Disabled 2FA for themself.']
        ]);

        return $response;
    }
}

/**
 * @uri /2fa/totp/disable/user/:username
 */
class TotpUserDisable extends BaseTotpController
{
    /**
     * @rbacName Disable 2FA for any user
     * @rbacGroup Two-factor authentication
     * @rbacAlias 2fa.any-user.disable
     * @throws ResponseException
     */
    public function post($request, $username): Response
    {
        $response = new Response($request);
        $this->user = new CfUsers($username);

        if (!$this->user->get2FaEnabled()) {
            $response->code = Response::CONFLICT;
            $response->body = "2FA is not enabled for `$username` user.";
            return $response;
        }

        TwoFaHelper::verifyRequest((new CfUsers($this->username)), getallheaders());

        $this->user->set2FaEnabled(false);
        $this->user->set2FaTotpSecret(null);

        $response->code = Response::OK;
        $response->body = "2FA successfully disabled for `$username` user.";

        AuditLogService::register([
            AuditLogFields::ACTOR => $this->username,
            AuditLogFields::OBJECT_TYPE => AuditLogObjectTypes::USER,
            AuditLogFields::OBJECT_ID => $username,
            AuditLogFields::OBJECT_NAME => $username,
            AuditLogFields::ACTION => AuditLogActions::UPDATE,
            AuditLogFields::DETAILS => ['Disabled 2FA for another user.']
        ]);

        return $response;
    }
}

/**
 * @uri /2fa/users-status
 */
class TwoFaUsersStatus extends CfProtectedResource
{
    /**
     * Reports whether all users have 2FA enabled.
     * Gated by the same permission as the user list.
     *
     * @rbacAlias userList.get
     * @rbacAllowedByDefault
     *
     * @return Response
     */
    public function get($request): Response
    {
        $response = new Response($request);
        $response->code = Response::OK;
        $response->body = json_encode(CfUsers::getTwoFaSummary());
        return $response;
    }
}

/**
 * @uri /2fa/verification-needed
 */
class TwoFaVerificationNeeded extends BaseTotpController
{
    public function get($request): Response
    {
        $response = new Response($request);

        $isVerificationNeeded = $this->user->get2FaEnabled() && TwoFaHelper::isAccessTokenShouldBeVerified(TwoFaHelper::getAccessTokenFromHeaders(getallheaders()));

        $response->code = Response::OK;
        $response->body = json_encode(['result' => $isVerificationNeeded]);
        return $response;
    }
}
