<?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  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 */

namespace Sso\BackendBundle\Worker;

use Doctrine\ORM\EntityManagerInterface;
use Sso\BackendBundle\Exception\InvalidConfigurationException;
use Sso\BackendBundle\Model\Form\UserCsvConfiguration;
use Sso\BackendBundle\Model\Form\UserCsvRow;
use Sso\WebserviceBundle\Api\Error\Type\Base as UserTypeError;
use Sso\WebserviceBundle\Database\Webservice\User as UserDatabase;
use Sso\WebserviceBundle\Database\Webservice\Application as ApplicationDatabase;
use Sso\WebserviceBundle\Database\Webservice\Attribute as AttributeDatabase;
use Sso\WebserviceBundle\Database\Webservice\Role as RoleDatabase;
use Sso\WebserviceBundle\Entity\Webservice\Type\Application;
use Sso\WebserviceBundle\Entity\Webservice\Type\Attribute;
use Sso\WebserviceBundle\Entity\Webservice\Type\Role;
use Sso\WebserviceBundle\Entity\Webservice\Type\User;
use Sso\WebserviceBundle\Entity\Webservice\Type\UserApplication;
use Sso\WebserviceBundle\Entity\Webservice\Type\UserApplicationAttribute;
use Sso\WebserviceBundle\Entity\Webservice\Type\UserApplicationRole;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Class UserCsvUpdate
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 * @package Sso\BackendBundle\Worker
 */
class UserCsvUpdate
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var UserDatabase
     */
    private $userDatabase;

    /**
     * @var ApplicationDatabase
     */
    private $applicationDatabase;

    /**
     * @var AttributeDatabase
     */
    private $attributeDatabase;

    /**
     * @var RoleDatabase
     */
    private $roleDatabase;

    /**
     * @var ValidatorInterface
     */
    private $validator;

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

    /**
     * @var bool
     */
    private $isDebug;

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

    /**
     * UserCsvUpdate constructor.
     * @param EntityManagerInterface $entityManager
     * @param UserDatabase $userDatabase
     * @param ApplicationDatabase $applicationDatabase
     * @param AttributeDatabase $attributeDatabase
     * @param RoleDatabase $roleDatabase
     * @param ValidatorInterface $validator
     * @param string $cacheDir
     * @param bool $isDebug
     */
    public function __construct(
        EntityManagerInterface $entityManager,
        UserDatabase $userDatabase,
        ApplicationDatabase $applicationDatabase,
        AttributeDatabase $attributeDatabase,
        RoleDatabase $roleDatabase,
        ValidatorInterface $validator,
        string $cacheDir,
        bool $isDebug
    ) {
        $this->entityManager = $entityManager;
        $this->userDatabase = $userDatabase;
        $this->applicationDatabase = $applicationDatabase;
        $this->attributeDatabase = $attributeDatabase;
        $this->roleDatabase = $roleDatabase;
        $this->validator = $validator;
        $this->cacheDir = $cacheDir;
        $this->isDebug = $isDebug;
    }

    /**
     * Cleanup
     */
    public function __destruct()
    {
        if (null !== $this->resultFile) {
            unlink($this->resultFile);
        }
    }

    /**
     * @param UploadedFile $file
     * @param UserCsvConfiguration $csvConfiguration
     * @return string
     */
    public function import(UploadedFile $file, UserCsvConfiguration $csvConfiguration): string
    {
        // Disable SQL logger to fight against memory leaks
        if (!$this->isDebug) {
            $this->entityManager
                ->getConnection()
                ->getConfiguration()
                ->setSQLLogger(null);
        }

        $this->resultFile = $this->cacheDir . '/user-csv-import-' . date('Ymd-His') . '-' . hash('crc32', mt_rand()) . '.csv';

        $rowCount = 0;
        $colCount = count($csvConfiguration->getRows());
        $columnDelimiter = $csvConfiguration->getColumnDelimiter();
        $textEnclosure = $csvConfiguration->getTextEnclosure();

        $fw = fopen($this->resultFile, 'w');
        $fh = fopen($file->getPathname(), 'r');

        while (($line = fgetcsv($fh, 0, $columnDelimiter, $textEnclosure)) !== false) {
            ++$rowCount;
            if (count($line) < $colCount) {
                fputcsv($fw, [$rowCount, 'invalid data'], $columnDelimiter, $textEnclosure);
                continue;
            }

            $rowStatus = 'updated';
            $user = $this->findUser($line, $csvConfiguration);
            if (null === $user) {
                if ($csvConfiguration->getAutoCreateUser()) {
                    $user = $this->userDatabase->getUserEntity();
                    $user->generateId();
                    $user->generateGuid();
                    $user->setActive($csvConfiguration->getActivateCreatedUser());
                    $rowStatus = 'created';
                } else {
                    fputcsv($fw, [$rowCount, 'not found'], $columnDelimiter, $textEnclosure);
                    continue;
                }
            }

            foreach ($csvConfiguration->getRows() as $colConfiguration) {
                switch ($colConfiguration->getTargetEntity()) {
                    case 'user':
                        $user = $this->updateUserProperty($user, $line, $colConfiguration, $csvConfiguration);
                        break;
                    case 'attribute':
                        $user = $this->updateApplicationAttribute($user, $line, $colConfiguration);
                        break;
                    case 'role':
                        $user = $this->updateApplicationRole($user, $line, $colConfiguration);
                        break;
                }
            }

            if ($this->userDatabase->saveUser($user)) {
                // Flush all other objects
                $this->entityManager->flush();
            } else {
                $errorMessages = array_map(function(UserTypeError $error) {
                    return $error->shortMessage;
                }, $user->errors()->getErrors());
                $rowStatus = 'user not valid: ' . implode(', ', $errorMessages);
            }

            fputcsv($fw, [$rowCount, $rowStatus, $user->getGuid(), $user->getEmail()], $columnDelimiter, $textEnclosure);

            // Cleanup
            $this->entityManager->clear();
            unset($user);
            $user = null;
        }

        fclose($fh);
        fclose($fw);

        return $this->resultFile;
    }

    /**
     * @param UserCsvConfiguration $csvConfiguration
     * @return UserCsvRow
     * @throws InvalidConfigurationException
     */
    private function getPrimaryColConfiguration(UserCsvConfiguration $csvConfiguration): UserCsvRow
    {
        foreach ($csvConfiguration->getRows() as $colConfiguration) {
            if ('user' === $colConfiguration->getTargetEntity() && $colConfiguration->getUser()->getPrimaryKey()) {
                return $colConfiguration;
            }
        }
        throw new InvalidConfigurationException('Primary key not set!');
    }

    /**
     * @param array $row
     * @param UserCsvConfiguration $csvConfiguration
     * @return User|null
     * @throws InvalidConfigurationException
     */
    private function findUser(array $row, UserCsvConfiguration $csvConfiguration): ?User
    {
        $primaryColConfiguration = $this->getPrimaryColConfiguration($csvConfiguration);
        $value = $row[$primaryColConfiguration->getRowId()];
        $key = $primaryColConfiguration->getUser()->getUserProperty();
        $method = 'getUserBy' . ucfirst($key);
        return $this->userDatabase->$method($value);
    }

    /**
     * @param User $user
     * @param array $row
     * @param UserCsvRow $colConfiguration
     * @param UserCsvConfiguration $csvConfiguration
     * @return User
     */
    private function updateUserProperty(
        User $user,
        array $row,
        UserCsvRow $colConfiguration,
        UserCsvConfiguration $csvConfiguration
    ): User {
        $value = $row[$colConfiguration->getRowId()];
        $key = $colConfiguration->getUser()->getUserProperty();
        $method = 'set' . ucfirst($key);
        if ('setEncryptedPassword' === $method) {
            $user->setEncryptedPassword($value, $csvConfiguration->getEncryptType());
        } else {
            $user->$method($value);
        }

        return $user;
    }

    /**
     * @param User $user
     * @param array $line
     * @param UserCsvRow $colConfiguration
     * @return User
     */
    private function updateApplicationAttribute(User $user, array $line, UserCsvRow $colConfiguration): User
    {
        $application = $this->applicationDatabase->getApplicationById(
            $colConfiguration->getApplicationAttribute()->getApplication()->getId()
        );
        $applicationName = $application->getName();

        $attribute = $this->attributeDatabase->getAttributeById(
            $colConfiguration->getApplicationAttribute()->getAttribute()->getId()
        );
        $attributeName = $attribute->getName();

        $value = trim($line[$colConfiguration->getRowId()]);
        $addAttribute = 0 < strlen($value);

        foreach ($user->getUserApplications() as $userApplication) {
            if (0 !== strcasecmp($applicationName, $userApplication->getApplication()->getName())) {
                continue;
            }

            foreach ($userApplication->getAttributes() as $userApplicationAttribute) {
                if (0 !== strcasecmp($attributeName, $userApplicationAttribute->getName())) {
                    continue;
                }

                if ($attribute->getType() === 'one') {
                    if ($addAttribute) {
                        $userApplicationAttribute->setValue($value);
                    } else {
                        $userApplication->removeAttribute($userApplicationAttribute);
                        $this->entityManager->remove($userApplicationAttribute);
                    }
                    return $user;
                }
            }

            if ($addAttribute) {
                // User application exists - add user application attribute
                $this->addApplicationAttribute($attribute, $userApplication, $value);
            }

            return $user;
        }

        if ($addAttribute) {
            // User application does not exist - add user application and attribute
            $userApplication = $this->addUserApplication($user, $application);
            $this->addApplicationAttribute($attribute, $userApplication, $value);
        }

        return $user;
    }

    /**
     * @param User $user
     * @param array $line
     * @param UserCsvRow $colConfiguration
     * @return User
     */
    private function updateApplicationRole(User $user, array $line, UserCsvRow $colConfiguration): User
    {
        $application = $this->applicationDatabase->getApplicationById(
            $colConfiguration->getApplicationRole()->getApplication()->getId()
        );
        $applicationName = $application->getName();

        $role = $this->roleDatabase->getRoleById(
            $colConfiguration->getApplicationRole()->getRole()->getId()
        );
        $roleName = $role->getName();

        // Add or remove role depending on value
        $addRole = (bool)$line[$colConfiguration->getRowId()];

        foreach ($user->getUserApplications() as $userApplication) {
            if (0 !== strcasecmp($applicationName, $userApplication->getApplication()->getName())) {
                continue;
            }

            foreach ($userApplication->getRoles() as $userApplicationRole) {
                if (0 !== strcasecmp($roleName, $userApplicationRole->getName())) {
                    continue;
                }

                if ($addRole) {
                    $userApplicationRole->setActive(true);
                } else {
                    $userApplication->removeRole($userApplicationRole);
                    $this->entityManager->remove($userApplicationRole);
                }

                return $user;
            }

            if ($addRole) {
                // User application exists - add user application role
                $this->addApplicationRole($role, $userApplication);
            }

            return $user;
        }

        if ($addRole) {
            // User application does not exist - add user application and role
            $userApplication = $this->addUserApplication($user, $application);
            $this->addApplicationRole($role, $userApplication);
        }

        return $user;
    }

    /**
     * @param User $user
     * @param Application $application
     * @return UserApplication
     */
    private function addUserApplication(
        User $user,
        Application $application
    ): UserApplication {
        $userApplication = new UserApplication($this->validator);
        $userApplication->generateId();
        $userApplication->setApplication($application);
        $userApplication->setUser($user);
        $userApplication->setActive(true);

        $user->addUserApplication($userApplication);
        $this->entityManager->persist($userApplication);

        return $userApplication;
    }

    /**
     * @param Attribute $attribute
     * @param UserApplication $userApplication
     * @param string $value
     */
    private function addApplicationAttribute(
        Attribute $attribute,
        UserApplication $userApplication,
        string $value
    ): void {
        $userApplicationAttribute = new UserApplicationAttribute($this->validator);
        $userApplicationAttribute->generateId();
        $userApplicationAttribute->setAttribute($attribute);
        $userApplicationAttribute->setUserApplication($userApplication);
        $userApplicationAttribute->setValue($value);

        $userApplication->addAttribute($userApplicationAttribute);
        $this->entityManager->persist($userApplicationAttribute);
    }

    /**
     * @param Role $role
     * @param UserApplication $userApplication
     */
    private function addApplicationRole(
        Role $role,
        UserApplication $userApplication
    ): void {
        $userApplicationRole = new UserApplicationRole($this->validator);
        $userApplicationRole->generateId();
        $userApplicationRole->setActive(true);
        $userApplicationRole->setRole($role);
        $userApplicationRole->setUserApplication($userApplication);

        $userApplication->addRole($userApplicationRole);
        $this->entityManager->persist($userApplicationRole);
    }
}
