<?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\WebserviceBundle\Database\Webservice;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Sso\WebserviceBundle\Entity\Webservice\Type\User as ModelUser;
use Sso\WebserviceBundle\Entity\Webservice\Type\Application as ModelApplication;
use Sso\WebserviceBundle\Entity\Webservice\Type\UserApplicationRole as ModelUserApplicationRole;
use Sso\WebserviceBundle\Entity\Webservice\Type\Role as ModelApplicationRole;
use Sso\WebserviceBundle\PasswordCrypt\PasswordCryptRepositoryInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Class User
 * @package Sso\WebserviceBundle\Database\Webservice
 */
class User extends Base
{
    /**
     * @var PasswordCryptRepositoryInterface
     */
    private $passwordCryptRepository;

    /**
     * User constructor.
     * @param EntityManagerInterface $entityManager
     * @param ValidatorInterface $validator
     * @param $readonly
     * @param PasswordCryptRepositoryInterface $passwordCryptRepository
     */
    public function __construct(
        EntityManagerInterface $entityManager,
        ValidatorInterface $validator,
        $readonly,
        PasswordCryptRepositoryInterface $passwordCryptRepository
    ) {
        $this->passwordCryptRepository = $passwordCryptRepository;
        parent::__construct($entityManager, $validator, $readonly);
    }

    /**
     * Save user
     * @param ModelUser $typeUser
     * @return bool Returns true on success, false on validation failure
     */
    public function saveUser(ModelUser $typeUser)
    {
        $passwordCrypt = $this->passwordCryptRepository->getByUser($typeUser);
        $typeUser->encryptPassword($passwordCrypt);

        // Do default validation
        $typeUser->setValidator($this->validator);
        if (!$typeUser->isValid()) {
            return false;
        }

        if ($this->readonly) {
            return false;
        }

        $this->entityManager->persist($typeUser);
        $this->entityManager->flush($typeUser);

        return true;
    }

    /**
     * Delete user
     * @param ModelUser $typeUser
     * @return bool True on success
     */
    public function deleteUser(ModelUser $typeUser)
    {
        $typeUser->setValidator($this->validator);
        if (!$typeUser->validate(array('User_Delete'))) {
            return false;
        }

        if ($this->readonly) {
            return false;
        }

        $this->entityManager->remove($typeUser);
        $this->entityManager->flush($typeUser);

        return true;
    }

    /**
     * Find user by user-id
     * @param string $userId
     * @return ModelUser|null
     */
    public function getUserById($userId)
    {
        return $this->getUserRepository()->find($userId);
    }

    /**
     * Find user by username
     * @param string $username
     * @return ModelUser|null
     */
    public function getUserByUsername($username)
    {
        return $this->getUserRepository()->findOneBy(array('LowerUsername' => strtolower($username)));
    }

    /**
     * Find user by GUID, username or email
     * @param string $identifier
     * @return ModelUser|null
     * @throws \Exception
     */
    public function getUserByIdentifier($identifier)
    {
        // It may be faster, to send up to 3 queries "using const", than doing a full table scan once
        $repository = $this->getUserRepository();
        if (($userGuid = $repository->findOneBy(array('Guid' => $identifier)))) {
            return $userGuid;
        }

        if (($userUsername = $repository->findOneBy(array('LowerUsername' => strtolower($identifier))))) {
            return $userUsername;
        }

        return $repository->findOneBy(array('LowerEmail' => strtolower($identifier)));
    }

    /**
     * Find user by GUID, username or email
     * @param ModelUser $user
     * @return ModelUser|null
     */
    public function getUserByUser(ModelUser $user)
    {
        $repository = $this->getUserRepository();
        if (($identifier = $user->getIdentifier())) {
            return $this->getUserByIdentifier($identifier);
        }

        if (($guid = $user->getGuid())) {
            return $repository->findOneBy(array('Guid' => $guid));
        }

        if (($username = $user->getLowerUsername())) {
            return $repository->findOneBy(array('LowerUsername' => strtolower($username)));
        }

        if (($email = $user->getLowerEmail())) {
            return $repository->findOneBy(array('LowerEmail' => strtolower($email)));
        }

        return null;
    }

    /**
     * Find user by email
     * @param string $email
     * @return ModelUser|null
     */
    public function getUserByEmail($email)
    {
        return $this->getUserRepository()->findOneBy(array('LowerEmail' => strtolower($email)));
    }

    /**
     * Find user by guid
     * @param string $guid
     * @return ModelUser|null
     */
    public function getUserByGuid($guid)
    {
        return $this->getUserRepository()->findOneBy(array('Guid' => $guid));
    }

    /**
     * Find user by email and password request token
     * @param string $email
     * @param string $passwordToken
     * @return ModelUser|null
     */
    public function getUserByEmailAndPasswordToken($email, $passwordToken)
    {
        return $this->getUserRepository()->findOneBy(
            array(
                'LowerEmail' => strtolower($email),
                'PasswordToken' => strtolower($passwordToken),
            )
        );
    }

    /**
     * Find user by email and activate request token
     * @param string $email
     * @param string $activateToken
     * @return ModelUser|null
     */
    public function getUserByEmailAndActivateToken($email, $activateToken)
    {
        return $this->getUserRepository()->findOneBy(
            array(
                'LowerEmail' => strtolower($email),
                'ActivateToken' => strtolower($activateToken),
            )
        );
    }

    /**
     * Find user by username and email
     * @param string $username
     * @param string $email
     * @return ModelUser|null
     */
    public function getUserByUsernameAndEmail($username, $email)
    {
        return $this->getUserRepository()->findOneBy(
            array(
                'LowerUsername' => strtolower($username),
                'LowerEmail' => strtolower($email),
            )
        );
    }

    /**
     * Find a list of users by username
     * @param string $username
     * @param int $limit
     * @param int $offset
     * @return \Sso\WebserviceBundle\Entity\Webservice\Type\User[]|null
     */
    public function getUsersByUsername($username, $limit = 1, $offset = 0)
    {
        $username = str_replace('%', '\%', strtolower($username));

        return $this->getUserRepository()
            ->createQueryBuilder('u')
            ->where('u.LowerUsername LIKE :username')
            ->setParameter('username', $username.'%')
            ->orderBy('u.LowerUsername')
            ->setMaxResults($limit)
            ->setFirstResult($offset)
            ->getQuery()
            ->getResult();
    }

    /**
     * Get application by name
     * @param ModelUser $user
     * @param string $applicationName
     * @return ModelApplication|null
     */
    public function getUserApplicationByName(ModelUser $user, $applicationName)
    {
        $applicationName = strtolower($applicationName);
        foreach ($user->getUserApplications() as $application) {
            if ($application->getLowername() == $applicationName) {
                return $application;
            }
        }

        return null;
    }

    /**
     * Get users application attribute values
     * @param ModelUser $user
     * @param ModelApplication $application
     * @return array List of AttributeValue
     */
    public function getUserApplicationAttributeValues(ModelUser $user, ModelApplication $application)
    {
        return $this->entityManager->getRepository('Sso\WebserviceBundle\Entity\Webservice\Type\AttributeValue')->findBy(
            array('ApplicationId' => $application->getId(), 'UserId' => $user->getId())
        );
    }

    /**
     * Returns list of GUIDs filtered by application-name
     * @param \Sso\WebserviceBundle\Entity\Webservice\Type\Application $application
     * @param bool|null $applicationActive
     * @param \DateTime|null $modifiedAfter
     * @return array
     */
    public function getGuidsByApplication(ModelApplication $application, $applicationActive, $modifiedAfter)
    {
        $dql = 'SELECT u.Guid '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Application a '.'WHERE a.Id=:ApplicationId';

        // Optional filter
        if (null !== $applicationActive) {
            $dql .= ' AND ua.Active=:Active';
        }

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameter('ApplicationId', $application->getId());

        // Optional filter
        if (null !== $applicationActive) {
            $query->setParameter('Active', $applicationActive);
        }

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        return $query->getScalarResult();
    }

    /**
     * Returns list of GUIDs filtered by application-attribute-value
     * @param \Sso\WebserviceBundle\Entity\Webservice\Type\UserApplicationAttribute $attribute
     * @param null|\DateTime $modifiedAfter
     * @param null|boolean $userAppActive
     * @return array
     */
    public function getGuidsByApplicationAttribute($attribute, $modifiedAfter, $userAppActive)
    {
        $dql = 'SELECT u.Guid '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Attributes uaa '.'JOIN uaa.Attribute a '.'WHERE a.Id=:AttributeId '.'AND uaa.Value=:Value';

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        if (null !== $userAppActive) {
            $dql .= ' AND ua.Active = :Active';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameters(['AttributeId' => $attribute->getAttribute()->getId(), 'Value' => $attribute->getValue()]);

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        // Optional filter
        if (null !== $userAppActive) {
            $query->setParameter('Active', $userAppActive);
        }

        return $query->getScalarResult();
    }

    /**
     * Returns list of GUIDs filtered by application-attribute-value
     * @param string $attributeId
     * @param null|\DateTime $modifiedAfter
     * @param null|boolean $userAppActive
     * @return array
     */
    public function getGuidNameEmailByApplicationAttribute(
        $attributeId,
        $attributeValue,
        $modifiedAfter,
        $userAppActive
    ) {
        $dql = 'SELECT u.Guid, u.Email, u.Username, u.Active '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Attributes uaa '.'JOIN uaa.Attribute a '.'WHERE a.Id=:AttributeId '.'AND uaa.Value=:Value';

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        if (null !== $userAppActive) {
            $dql .= ' AND ua.Active = :Active';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameters(
            array(
                'AttributeId' => $attributeId,
                'Value' => $attributeValue,
            )
        );

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        // Optional filter
        if (null !== $userAppActive) {
            $query->setParameter('Active', $userAppActive ? 1 : 0);
        }

        return $query->getScalarResult();
    }

    /**
     * @param ModelApplicationRole $role
     * @param null|\DateTime $modifiedAfter
     * @param null|boolean $roleActive
     * @param null|boolean $userApplicationActive
     *
     * @return array
     */
    public function getGuidNameEmailByApplicationRole(
        ModelApplicationRole $role,
        $modifiedAfter,
        $roleActive,
        $userApplicationActive
    ) {
        $dql = 'SELECT u.Guid, u.Email, u.Username, u.Active '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Roles uar '.'WHERE uar.RoleId=:RoleId';

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        if (null !== $roleActive) {
            $dql .= ' AND uar.Active = :RoleActive';
        }

        if (null !== $userApplicationActive) {
            $dql .= ' AND ua.Active = :UserApplicationActive';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameters(
            array(
                'RoleId' => $role->getId(),
            )
        );

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        if (null !== $roleActive) {
            $query->setParameter('RoleActive', $roleActive ? 1 : 0);
        }

        if (null !== $userApplicationActive) {
            $query->setParameter('UserApplicationActive', $userApplicationActive ? 1 : 0);
        }

        return $query->getScalarResult();
    }

    /**
     * @param ModelUserApplicationRole $role
     * @param null|\DateTime $modifiedAfter
     * @param null|boolean $roleActive
     * @param null|boolean $userApplicationActive
     * @return array
     */
    public function getGuidsByApplicationRole(
        ModelUserApplicationRole $role,
        $modifiedAfter,
        $roleActive,
        $userApplicationActive
    ) {
        $dql = 'SELECT u.Guid '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Roles uar '.'WHERE uar.RoleId=:RoleId';

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        if (null !== $roleActive) {
            $dql .= ' AND uar.Active = :RoleActive';
        }

        if (null !== $userApplicationActive) {
            $dql .= ' AND ua.Active = :UserApplicationActive';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameters(
            array(
                'RoleId' => $role->getRole()->getId(),
            )
        );

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        if (null !== $roleActive) {
            $query->setParameter('RoleActive', $roleActive);
        }

        if (null !== $userApplicationActive) {
            $query->setParameter('UserApplicationActive', $userApplicationActive);
        }

        return $query->getScalarResult();
    }

    /**
     *
     * @return EntityRepository
     */
    public function getUserRepository()
    {
        return $this->entityManager->getRepository('Sso\WebserviceBundle\Entity\Webservice\Type\User');
    }


    /**
     * @return ModelUser
     */
    public function getUserEntity()
    {
        $user = new ModelUser($this->validator);

        // For backwards compatibility because password-encrypt-type is not supported by every version
        $user->setDefaultPasswordEncryptType($this->passwordCryptRepository->getDefault()->getEncryptType());

        return $user;
    }

    /**
     * Returns list of GUIDs filtered by application-name
     * @param \Sso\WebserviceBundle\Entity\Webservice\Type\Application $application
     * @param bool|null $applicationActive
     * @param \DateTime|null $modifiedAfter
     * @return array
     */
    public function getUserDataByApplication(ModelApplication $application, $applicationActive, $modifiedAfter)
    {
        $dql = 'SELECT u.Guid,u.Email,u.Active,u.Username '.'FROM Sso\WebserviceBundle\Entity\Webservice\Type\User u '.'JOIN u.UserApplications ua '.'JOIN ua.Application a '.'WHERE a.Id=:ApplicationId';

        // Optional filter
        if (null !== $applicationActive) {
            $dql .= ' AND ua.Active=:Active';
        }

        if (null !== $modifiedAfter) {
            $dql .= ' AND u.UpdatedAt >= :UpdatedAt';
        }

        $query = $this->entityManager->createQuery($dql);
        $query->setParameter('ApplicationId', $application->getId());

        // Optional filter
        if (null !== $applicationActive) {
            $query->setParameter('Active', $applicationActive ? 1 : 0);
        }

        // Optional filter
        if (null !== $modifiedAfter) {
            $query->setParameter('UpdatedAt', $modifiedAfter);
        }

        return $query->getScalarResult();
    }

    /**
     * @param ModelUser $userEntity
     * @return \Sso\WebserviceBundle\Entity\Webservice\Type\PasswordPolicy|null
     */
    public function getUsersPasswordPolicy(ModelUser $userEntity){
        $policyEntity = null;

        $policyId = $userEntity->getPasswordPolicy();
        if (0 < strlen($policyId)) {
            $policyEntity = $this->entityManager->getRepository('Sso\WebserviceBundle\Entity\Webservice\Type\PasswordPolicy')->find($policyId);
        }

        // User does not have any policy set - use default policy
        if (null === $policyEntity) {
            $policyEntity = $this->entityManager->getRepository('Sso\WebserviceBundle\Entity\Webservice\Type\PasswordPolicy')->findOneBy(['isDefault' => true]);
        }
        return $policyEntity;
    }
}
