<?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 LifeStyle\DH\AccessControlBundle\EventListener;

use Doctrine\DBAL\Connection;
use LifeStyle\DH\AccessControlBundle\Authorization\UserScopes;
use LifeStyle\DH\AccessControlBundle\Authorization\UserScopesBuilder;
use LifeStyle\DH\AccessControlBundle\Exception\InvalidConfigurationException;
use LifeStyle\DH\AccessControlBundle\Exception\InvalidRequestException;
use LifeStyle\DH\AccessControlBundle\Repository\ObjectServiceRepository;
use Psr\Log\LoggerInterface;
use Sso\AccessBundle\Authorization\TemporaryTokenAuthorizationInterface;
use Sso\WebserviceBundle\Event\UserAdvancedSearchQueryEvent;
use Sso\WebserviceBundle\Security\Authentication\Token\WsFirewallToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Class UserAdvancedSearchFilterEventListener
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 * @package LifeStyle\DH\AccessControlBundle\EventListener
 */
class UserAdvancedSearchFilterEventListener
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorageInterface;

    /**
     * @var TemporaryTokenAuthorizationInterface
     */
    private $temporaryTokenAuthorization;

    /**
     * @var ObjectServiceRepository
     */
    private $objectServiceRepository;

    /**
     * @var Connection
     */
    private $databaseConnection;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var UserScopesBuilder
     */
    private $userScopesBuilder;

    /**
     * UserAdvancedSearchFilterEventListener constructor.
     * @param TokenStorageInterface $tokenStorageInterface
     * @param TemporaryTokenAuthorizationInterface $temporaryTokenAuthorization
     * @param ObjectServiceRepository $objectServiceRepository
     * @param Connection $databaseConnection
     * @param LoggerInterface $logger
     * @param UserScopesBuilder $userScopesBuilder
     */
    public function __construct(
        TokenStorageInterface $tokenStorageInterface,
        TemporaryTokenAuthorizationInterface $temporaryTokenAuthorization,
        ObjectServiceRepository $objectServiceRepository,
        Connection $databaseConnection,
        LoggerInterface $logger,
        UserScopesBuilder $userScopesBuilder
    ) {
        $this->tokenStorageInterface = $tokenStorageInterface;
        $this->temporaryTokenAuthorization = $temporaryTokenAuthorization;
        $this->objectServiceRepository = $objectServiceRepository;
        $this->databaseConnection = $databaseConnection;
        $this->logger = $logger;
        $this->userScopesBuilder = $userScopesBuilder;
    }

    /**
     * @param UserAdvancedSearchQueryEvent $event
     * @throws InvalidConfigurationException
     */
    public function onUserAdvancedSearchQueryEvent(UserAdvancedSearchQueryEvent $event): void
    {
        if (!$this->hasRestrictedAccess()) {
            return;
        }

        $applicationUserScopes = $this->userScopesBuilder->build();

        // Global HR manager has no restrictions
        if ($applicationUserScopes->isGlobalHrManager()) {
            return;
        }

        $previousSql = $event->buildSql();

        $userId = $this->getQuotedUserId($applicationUserScopes);
        $applicationId = $this->getQuotedApplicationId($applicationUserScopes);
        $objectIds = $this->getQuotedObjectIds($applicationUserScopes);
        $attributeIds = $this->getQuotedAttributeIds($applicationUserScopes);
        $grantAccessUserIds = $this->getQuotedGrantAccessUserIds($applicationUserScopes);

        // If the user has no permissions, the result should always be empty
        if (empty($objectIds) || empty($attributeIds)) {
            if (empty($grantAccessUserIds)) {
                $where = ' WHERE 0 ';
            } else {
                $where = ' ' .
                    'WHERE (' . $this->normalizeWhere($event->getWhere()) . ') ' .
                    'AND ( `' . $event->getAlias() . '`.`user_id` IN (' . $grantAccessUserIds . ') ';
            }
            $event->setWhere($where);
            $sql = $event->buildSql();
            $this->logger->debug(
                'Added access restrictions (no scopes found)! Changed sql FROM: "' . $previousSql . '" TO: "' . $sql
            );
            $event->setSql($sql);
            return;
        }

        $previousWhere = $event->getWhere();
        $where = implode(' ', [
            'WHERE',
            '(',
            $this->normalizeWhere($previousWhere),
            ')',
            'AND',
            '(',
            // User is not allowed to view/edit/delete himself
            '`' . $event->getAlias() . '`.`user_id` <> ' . $userId,
            // Limit access to application users scopes
            'AND `u2a`.`application_id` = ' . $applicationId,
            'AND `uaa`.`attribute_id` IN (' . $attributeIds . ')',
            'AND `uaa`.`attribute_value` IN (' . $objectIds . ')',
            ')',
        ]);
        $event->setWhere($where);

        $previousJoin = $event->getJoin();
        $join = implode(' ', [
            $previousJoin,
            'JOIN `user_application` AS `u2a` USING (`user_id`)',
            'JOIN `user_application_attribute` AS `uaa` ON (`u2a`.`id` = `uaa`.`userapplication_id`)',
        ]);
        $event->setJoin($join);

        // If no shortly added users available, keep it simple!
        if (empty($grantAccessUserIds)) {
            $sql = $event->buildSql();
            $this->logger->debug(
                'Added access restrictions (scopes)! Changed sql from "' . $previousSql . '" to "' . $sql
            );
            $event->setSql($sql);
            return;
        }

        $sqlByScopeAccess = implode(' ', [
            $event->getSelect(),
            $event->getFrom(),
            $event->getJoin(),
            $event->getWhere(),
            $event->getGroupBy(),
            $event->getOrderBy(),
        ]);

        $sqlBySessionAccess = implode(' ', [
            $event->getSelect(),
            $event->getFrom(),
            $previousJoin,
            'WHERE',
            '(' .
            $this->normalizeWhere($previousWhere),
            ')' .
            'AND',
            '(' .
            // User is allowed to view/edit/delete shortly added users
            '`' . $event->getAlias() . '`.`user_id` IN (' . $grantAccessUserIds . ')',
            ')',
            $event->getGroupBy(),
            $event->getOrderBy(),
        ]);


        $sql = implode(' ', [
            'SELECT `' . $event->getAlias() . '`.*',
            'FROM',
            '(',
            '  (',
            $this->replaceAlias($event->getAlias(), 'u4711', $sqlByScopeAccess),
            '  )',
            '  UNION',
            '  (',
            $this->replaceAlias($event->getAlias(), 'u4712', $sqlBySessionAccess),
            '  )',
            ')',
            'AS `' . $event->getAlias() . '`',
            $event->getOrderBy(),
            $event->getLimit(),
        ]);

        $this->logger->debug(
            'Added access restrictions (scopes and added users)! Changed sql from "' . $previousSql . '" to "' . $sql
        );
        $event->setSql($sql);
    }

    /**
     * @return bool
     */
    private function hasRestrictedAccess(): bool
    {
        return
            ($token = $this->tokenStorageInterface->getToken()) &&
            $token instanceof WsFirewallToken &&
            $token->accessRestricted;
    }

    /**
     * @param UserScopes $userScopes
     * @return string
     */
    private function getQuotedUserId(UserScopes $userScopes): string
    {
        return $this->databaseConnection->quote($userScopes->getUserId());
    }

    /**
     * @param UserScopes $userScopes
     * @return string
     */
    private function getQuotedApplicationId(UserScopes $userScopes): string
    {
        return $this->databaseConnection->quote($userScopes->getApplicationId());
    }

    /**
     * @param UserScopes $userScopes
     * @return string
     */
    private function getQuotedAttributeIds(UserScopes $userScopes): string
    {
        $attributeIds = array_map(
            function (string $attributeId): string {
                return $this->databaseConnection->quote($attributeId);
            },
            $userScopes->getApplicationAttributeIds()
        );

        return join(',', $attributeIds);
    }

    /**
     * @param UserScopes $userScopes
     * @return string
     */
    private function getQuotedObjectIds(UserScopes $userScopes): string
    {
        try {
            $objectIds = $this->objectServiceRepository->findObjectIdsByScopeIds($userScopes->getScopeIds());
        } catch (InvalidRequestException $exception) {
            // We don't want errors in object-ws to bubble up
            $this->logger->debug(sprintf(
                    'Error while receiving authorization details from object-ws! %s',
                    $exception->getMessage())
            );
            $objectIds = [];
        }

        // Manually quote groups here, because we cannot set query parameters
        $objectIds = array_map(
            function (string $objectId): string {
                return $this->databaseConnection->quote($objectId);
            },
            $objectIds
        );

        return implode(',', $objectIds);
    }

    /**
     * @param UserScopes $userScopes
     * @return string
     */
    private function getQuotedGrantAccessUserIds(UserScopes $userScopes): string
    {
        $userId = $userScopes->getUserId();
        $grantAccessUserIds = array_map(
            function (string $grantAccessUserId): string {
                return $this->databaseConnection->quote($grantAccessUserId);
            },
            array_filter(
                $this->temporaryTokenAuthorization->getAuthorizedUserIds(),
                function (string $grantAccessUserId) use ($userId) {
                    return $grantAccessUserId !== $userId;
                }
            )
        );

        return join(',', $grantAccessUserIds);
    }

    /**
     * @param string $search
     * @param string $replace
     * @param string $sql
     * @return string
     */
    private function replaceAlias(string $search, string $replace, string $sql): string
    {
        return str_replace(
            [' ' . $search . '.', '`' . $search . '`.', ' ' . $search . ' ', '`' . $search . '` '],
            [' `' . $replace . '`.', '`' . $replace . '`.', ' `' . $replace . '` ', '`' . $replace . '` '],
            $sql
        );
    }

    /**
     * @param string $where
     * @return string
     */
    private function normalizeWhere(string $where): string
    {
        $where = preg_replace('/^[\s]*WHERE[\s]*/i', '', $where);
        return empty($where) ? '1' : $where;
    }
}
