<?php

use events\BaseEvents;
use events\dto\UserEventsDto;
use events\EventService;
use templates\TemplateLoader;

/**
 * @uri /auth/password/forgot/:username
 */
class ForgotPassword extends CfBaseResource
{
    /**
     * @throws ResponseException
     * @throws Exception
     */
    public function post($request, $username)
    {
        $response = new Response($request);
        $response->code = Response::ACCEPTED;
        $response->body = 'Check your email for the link to reset your password.';

        $cfResetPassword = new CfPasswordReset();

        if (empty($username)) {
            throw new ResponseException('Username is required.', Response::UNPROCESSABLE_ENTITY);
        }

        if (!preg_match('/^[a-zA-Z0-9._-]+$/', $username)) {
            throw new ResponseException(
                'Username can only contain letters, dots, dashes, numbers, and underscores.',
                Response::UNPROCESSABLE_ENTITY
            );
        }

        $cfUser = new CfUsers($username);

        /**
         * If a user is not found instead of providing this information and disclosing account presence,
         * it emulates normal script work time and returns the `check your email for the link` response
         */
        if (!($user = $cfUser->getUser())) {
            // random microseconds pause duration (1.6-2.5 seconds)
            $pauseDurationMicroseconds = rand((1.6 * 1000 * 1000), (2.5 * 1000 * 1000));
            usleep($pauseDurationMicroseconds);
            return $response;
        }

        if (!isset($user['email']) && empty($user['email'])) {
            throw new ResponseException(
                'Email is missing, reset password cannot be proceeded.',
                Response::UNPROCESSABLE_ENTITY
            );
        }

        if ($cfUser->isExternal()) {
            throw new ResponseException('Sorry, we are unable to reset the password at this time. Please contact your administrator for more information.', Response::UNPROCESSABLE_ENTITY);
        }

        $expireAfterHours = SettingsHelper::DEFAULTS[SettingsHelper::PASSWORD_EXPIRATION_AFTER_RESET_HOURS];
        $token = $cfResetPassword->generateToken($username, $expireAfterHours);

        $settings = MailSettingsDTO::fromArray(CfMailSettings::getSettings());
        $mailService = new CfMail($settings);

        try {
            $mailService->send(new MailContentDTO(
                fromAddress: "{$settings->defaultFromEmail}",
                to: $user['email'],
                subject: 'CFEngine - Password reset request',
                body: TemplateLoader::load(
                    'mail/password-forgot',
                    [
                        'token' => $token,
                        'domain' => rtrim(!empty($settings->linksDomain) ? $settings->linksDomain : NetworkHelper::getServerUrl(verifyNameBelongsToServer: true), '/'),
                        'expire' => $expireAfterHours
                    ]
                )
            ));
        } catch (\PHPMailer\PHPMailer\Exception $exception) {
            $cfResetPassword->revokeToken($token);
            throw new ResponseException(
                'Sorry, we are unable to reset the password at this time. Please contact your administrator for more information.',
                Response::UNPROCESSABLE_ENTITY
            );
        }

        return $response;
    }

}

/**
 * @uri /auth/password/reset/:token
 */
class ResetPassword extends CfBaseResource
{
    private CfPasswordReset $cfResetPassword;

    private MailSettingsDTO $mailSettings;
    private CfMail $mailService;

    public function __construct($parameters)
    {
        $this->cfResetPassword = new CfPasswordReset();
        $this->mailSettings = MailSettingsDTO::fromArray(CfMailSettings::getSettings());
        $this->mailService = new CfMail($this->mailSettings);
        parent::__construct($parameters);
    }

    public function post($request, string $token)
    {
        $response = new Response($request);
        $data = Utils::getValidJsonData($request->data);
        $CfFailedAttempts = new CfFailedAttempts(ipAddress: $_SERVER['REMOTE_ADDR'], action: CfFailedAttempts::PASSWORD_RESET_ACTION, delayInSeconds: [10, 30, 60]);
        $timeout = $CfFailedAttempts->timeOutBeforeNextAttempt();

        if ($timeout !== null) {
            throw new ResponseException(
                "We have detected multiple unsuccessful reset password attempts. Please try again after $timeout seconds.",
                Response::TOO_MANY_REQUESTS
            );
        }

        if (!($username = $this->cfResetPassword->verifyToken($token))) {
            $CfFailedAttempts->logFailedAttempt();
            throw new ResponseException('Unable to process request.', Response::UNPROCESSABLE_ENTITY);
        }

        $cfUser = new CfUsers($username);

        if (!$cfUser->getUser()) {
            throw new ResponseException('Unable to process request.', Response::UNPROCESSABLE_ENTITY);
        }

        Validator::password($data->password, $username);

        if (
            cfapi_user_post(
                $username, // under this user update operation will be performed, same as update itself
                $username,
                $data->password,
                '',
                '',
                '',
                '',
                false
            )
        ) {
            EventService::dispatchEvent(
                BaseEvents::PASSWORD_CHANGED,
                new UserEventsDto(
                    username: $username,
                    cfUser: new CfUsers($username)
                )
            );
        }

        $response->code = Response::ACCEPTED;
        $response->body = 'Password successfully changed.';
        return $response;
    }

    public function delete($request, $token): Response
    {
        $response = new Response($request);
        $removedRows = $this->cfResetPassword->revokeToken($token);
        if ($removedRows > 0) {
            $response->code = Response::ACCEPTED;
            $response->body = 'Reset password token successfully invalidated.';
        } else {
            $response->code = Response::UNPROCESSABLE_ENTITY;
            $response->body = 'Unable to process request.';
        }

        return $response;
    }
}
