<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2018 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace Sso\RestBundle\Api\Validate\Credentials;

use Sso\RestBundle\Api\Exception\CredentialsException;
use Sso\RestBundle\Api\Manager as ApiManager;
use Sso\RestBundle\Api\Database\Factory as DatabaseManager;
use Sso\RestBundle\Model\Response\Idp\PostLogin\Response as PostLoginResponse;
use Sso\WebserviceBundle\Entity\Webservice\Type\User as SsoUser;
use Sso\WebserviceBundle\PasswordCrypt\PasswordCryptRepositoryInterface;

/**
 * Class Index
 * @package Sso\RestBundle\Api\Validate\Credentials
 */
class Index
{
    /**
     * @var ApiManager
     */
    protected $apiM;

    /**
     * @var \LifeStyle\Tools\RestErrorBundle\Api\Manager
     */
    protected $errorApi;

    /**
     * @var \LifeStyle\Tools\RestErrorBundle\Api\Error\Errors\Index
     */
    protected $errors;

    /**
     * @var DatabaseManager
     */
    protected $dbM;

    /**
     * @var PostLoginResponse
     */
    protected $responseModel;

    /**
     * @var string
     */
    private $userIdentifier;

    /**
     * @var string
     */
    private $password;

    /**
     * @var SsoUser
     */
    private $ssoUser;

    /**
     * @var string
     */
    private $loginStatus;

    /**
     * @var string
     */
    private $userAuthId;

    /**
     * @var string
     */
    private $userLdapSearchAttributes;

    /**
     * @var string
     */
    private $userLdapSearchValue;

    /**
     * @var PasswordCryptRepositoryInterface
     */
    private $passwordCryptRepository;

    /**
     * Index constructor.
     * @param ApiManager $apiM
     */
    public function __construct(ApiManager $apiM)
    {
        $this->apiM = $apiM;
        $this->dbM = $this->apiM->database();
        $this->errorApi = $this->apiM->errorManager();
        $this->errors = $this->errorApi->error()->errors();
        $this->responseModel = $this->apiM->model()->response()->idp()->postLogin();
        $this->passwordCryptRepository = $this->apiM->container->get('password_crypt_repository');
    }

    /**
     * @param string $userIdentifier
     * @param string $password
     * @throws \Exception
     * @return PostLoginResponse
     */
    public function loginUser($userIdentifier, $password)
    {
        $this->userIdentifier = $userIdentifier;
        $this->password = $password;

        // first: validate userIdentifier and get encrypted password
        $this->validateUserIdentifier();
        if ($this->errors->hasErrors()) {
            return $this->errors->getErrors();
        }

        //is user blocking enabled
        if ($this->apiM->configuration()->isUserBlockingEnabled()) {
            //check if the user is blocked because of e.g. bruteforce
            $this->isUserBlocked();
        }

        if ($this->errors->hasErrors()) {
            //login error
            return $this->errors->getErrors();
        }

        // second: check password
        $this->validatePassword();
        if ($this->errors->hasErrors()) {
            //login error
            return $this->errors->getErrors();
        }

        //check if the password was verified
        if ('verified' == $this->loginStatus) {

            //ok login success
            $this->setResponseModelVerified();

        } elseif ('unverified' == $this->loginStatus) {
            //ok unverified
            $this->setResponseModelUnverified();
        } else {
            throw new CredentialsException('LoginStatus unknown');
        }
        return $this->responseModel;
    }

    /**
     * @throws \Exception
     */
    private function validateUserIdentifier()
    {

        $this->ssoUser = $this->dbM->webservice()->data()->user()->findUserByIdentifier($this->userIdentifier);
        if (!$this->ssoUser) {
            $this->addError('user not found in database');
            return;
        }
        // now check if the user is active
        $active = $this->ssoUser->getActive();
        if ($active == 0) {
            $this->addError('user is inactive', 401);
            $this->dbM->webservice()->data()->user()->save($this->ssoUser);
            return;
        }
        //set some data
        $this->userAuthId = $this->ssoUser->getAuthId();
        $this->userLdapSearchAttributes = $this->ssoUser->getLdapSearchAttributes();
        $this->userLdapSearchValue = $this->ssoUser->getLdapSearchValue();
    }

    /**
     * @return bool
     * @throws \Exception
     */
    private function validatePassword()
    {
        if (!$this->ssoUser->verifyPassword(
            $this->password,
            $this->passwordCryptRepository->getByUser($this->ssoUser)
        )) {

            //ok wrong credentials or differnent authentication source check that
            if ((strlen($this->userAuthId) > 1) && (strlen($this->userLdapSearchAttributes) > 1) && (strlen($this->userLdapSearchValue) > 1)) {
                $this->loginStatus = "unverified";
            } else {
                $this->addError('wrong password', 401);
                $this->loginStatus = "error";
            }
            return false;
        }
        $this->loginStatus = "verified";
        return true;
    }

    /**
     * @throws \Exception
     * @return boolean
     */
    private function isUserBlocked()
    {
        //get required vars
        $maxLoginFails = $this->apiM->configuration()->getMaxLoginFails();
        $userLoginFails = $this->ssoUser->getLoginFails();

        //first check if userLoginFails are greater than maxLoginFails
        if ($userLoginFails >= $maxLoginFails) {
            //set clearing date
            $clearTimeSec = $this->apiM->configuration()->getClearTimeSec();
            try {
                $clearingDate = new \DateTime('now -' . (int)$clearTimeSec . ' seconds');
            } catch (\Exception $exception) {
                $clearingDate = new \DateTime();
            }
            $userLoginLastFailed = $this->ssoUser->getLoginFailedLastAt();

            //if not set yet set it and add login error
            if (null === $userLoginLastFailed) {
                $this->addError('user login blocked', 403);
                $this->loginStatus = "blocked";
                return false;
            }

            //is he allowed to try again?
            if (($userLoginLastFailed > $clearingDate)) {
                $this->addError('user login blocked', 403);
                $this->loginStatus = "blocked";
                return false;
            }
        }
        return true;
    }

    /**
     * @param $error
     * @param int $status
     * @return void
     */
    private function addError($error, $status = 400)
    {
        // before killing the process set a failed login if the user exists
        $this->errors->addError($status, $error, 'external', 'InvalidRequest')->setStatus($status);
    }

    /**
     * @return void
     */
    private function setResponseModelVerified()
    {
        $this->responseModel
            ->setStatus(200)
            ->setMessage('OK')
            ->setLoginStatus($this->loginStatus)
            ->setUserGuid($this->ssoUser->getGuid())
            ->setUsername($this->ssoUser->getUsername())
            ->setUserEmail($this->ssoUser->getEmail())
            ->setUserFirstname($this->ssoUser->getFirstname())
            ->setUserLastname($this->ssoUser->getLastname())
            ->setUserIdentifier($this->ssoUser->getUsername())
            ->setUserAuthId($this->ssoUser->getAuthId())
            ->setUserLdapSearchAttributes($this->ssoUser->getLdapSearchAttributes())
            ->setUserLdapSearchValue($this->ssoUser->getLdapSearchValue())
            ->setMfaEnabled($this->ssoUser->isMfaEnabled())
            ->setLastLogin($this->ssoUser->getLastLoginAt())
            ->setLoginFails($this->ssoUser->getLoginFails());

        // Mfa secret as qr-code should only be added if user has not received the secret yet
        if ($this->ssoUser->isMfaEnabled() && !$this->ssoUser->hasReceivedMfaSecret()) {
            $this->responseModel->setMfaQrCode($this->mfaApiM()->mfa()->image()->generateQr($this->ssoUser->getEmail(),
                $this->ssoUser->getMfaSecret()));
        }
    }

    /**
     * @return void
     */
    private function setResponseModelUnverified()
    {
        $this->responseModel
            ->setStatus(406)
            ->setMessage('Not Acceptable')
            ->setLoginStatus($this->loginStatus)
            ->setUserGuid($this->ssoUser->getGuid())
            ->setUsername($this->ssoUser->getUsername())
            ->setUserEmail($this->ssoUser->getEmail())
            ->setUserFirstname($this->ssoUser->getFirstname())
            ->setUserLastname($this->ssoUser->getLastname())
            ->setUserIdentifier($this->ssoUser->getUsername())
            ->setUserAuthId($this->ssoUser->getAuthId())
            ->setUserLdapSearchAttributes($this->ssoUser->getLdapSearchAttributes())
            ->setUserLdapSearchValue($this->ssoUser->getLdapSearchValue())
            ->setMfaEnabled($this->ssoUser->isMfaEnabled())
            ->setLastLogin($this->ssoUser->getLastLoginAt())
            ->setLoginFails($this->ssoUser->getLoginFails());

        // Mfa secret as qr-code should only be added if user has not received the secret yet
        if ($this->ssoUser->isMfaEnabled() && !$this->ssoUser->hasReceivedMfaSecret()) {
            $this->responseModel->setMfaQrCode($this->mfaApiM()->mfa()->image()->generateQr($this->ssoUser->getEmail(),
                $this->ssoUser->getMfaSecret()));
        }
    }

    /**
     * @return \LifeStyle\Tools\MfaBundle\Api\Manager
     */
    private function mfaApiM()
    {
        return $this->apiM->getContainer()->get('mfa_api_manager');
    }
}
