<?php

namespace Sso\WebserviceBundle\Security\Api\Firewall\Level;

use Doctrine\Common\Collections\ArrayCollection;
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 Sso\WebserviceBundle\FeatureToggle\FeatureToggle;
use Sso\WebserviceBundle\Security\Api\Manager as ApiManager;
use Sso\WebserviceBundle\Security\Configuration\AccessConfiguration;
use Sso\WebserviceBundle\Security\Exception\FirewallException;
use Sso\WebserviceBundle\Security\Model\Request\Request as XmlRequest;
use Sso\WebserviceBundle\Database\Manager as DatabaseManager;

/**
 * Class Three
 * @package Sso\WebserviceBundle\Security\Api\Firewall\Level
 */
class Four
{
    /**
     * The application the webservice consumer must be connected to
     */
    const SERVICE_CONSUMER_APPLICATION = 'WebserviceConsumer';
    const SERVICE_CONSUMER_APPLICATION_LEN = 18;

    /**
     * The application attribute where the webservice consumers service token must be stored
     */
    const SERVICE_CONSUMER_ATTRIBUTE = 'UserWS-ServiceToken';

    /**
     * @var ApiManager
     */
    private $apiM;

    /**
     * @var \Sso\WebserviceBundle\ErrorHandler\ErrorStorage\Factory
     */
    private $errorStorage;

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

    /**
     * @var FeatureToggle
     */
    private $featureToggle;

    /**
     * @var AccessConfiguration
     */
    private $accessConfiguration;

    /**
     * Factory constructor.
     * @param ApiManager $apiM
     */
    public function __construct(ApiManager $apiM, FeatureToggle $featureToggle)
    {
        $this->apiM = $apiM;
        $this->errorStorage = $this->apiM->getErrorHandler()->errorStorage();
        $this->dbM = $this->apiM->database();
        $this->featureToggle = $featureToggle;
        $this->accessConfiguration = $this->apiM->accessConfiguration();
    }

    /**
     * @param XmlRequest $xmlRequest
     * @return array
     * @throws FirewallException
     */
    public function userHasApplicationWebserviceConsumer(XmlRequest $xmlRequest)
    {
        // TODO Remove feature flag enable_application_access_control
        if (!$this->featureToggle->isActive('enable_application_access_control')) {
            return $this->disabledFirewall($xmlRequest);
        }

        $consumerUsername = $xmlRequest->getCredentials()->getServiceTrigger()->getUserName();
        $consumer = $this->dbM->webservice()->firewall()->userHasApplication($consumerUsername, static::SERVICE_CONSUMER_APPLICATION);

        if (null === $consumer) {
            $message = "The User is not allowed to consume webservices.";
            $this->errorStorage->setErrorFromValues(400, 'fw004', 'hd008', 'UserIsNotAllowed', $message);
        }

        if ($this->errorStorage->hasErrors()) {
            throw new FirewallException('Firewall Level Four Exception');
        }

        $userActiveRoles = [];
        $userServiceTokens = [];

        $tokenId = $xmlRequest->getCredentials()->getServiceProvider()->getServiceToken();
        $token = $this->apiM->database()->serviceProvider()->token()->getOneById($tokenId);
        $isAccessRestricted = null !== $token && $token->isAccessRestricted();

        // If the token has restricted access, the application access level is based on
        // the roles of an configurable user application connected to the current
        // application user (not the service consumer user).
        if ($isAccessRestricted) {
            $username = $xmlRequest->getCredentials()->getApplicationUser()->getUserName();
            $user = $this->dbM->webservice()->user()->getUserByUsername($username);
            if (
                null !== $user &&
                null !== ($application = $this->findApplication($user, $this->accessConfiguration->getUserAccessApplication()))
            ) {
                $userActiveRoles = $this->getAccessRoles($application->getRoles());
            }
        }

        // If not the application access level is read from the WebserviceConsumer application.
        if (null !== ($application = $this->findApplication($consumer, static::SERVICE_CONSUMER_APPLICATION))) {

            if (!$isAccessRestricted) {
                $userActiveRoles = $this->getAccessRoles($application->getRoles());
            }
            $userServiceTokens = $this->getAccessTokens($application->getAttributes());
        }

        return [$userActiveRoles, $userServiceTokens];
    }

    /**
     * @param UserApplicationRole[]|ArrayCollection $roles
     * @return array
     */
    private function getAccessRoles($roles): array
    {
        $userActiveRoles = [];
        foreach ($roles as $role) {
            if (!$role->getActive()) {
                continue;
            }

            // Role should be something like MyApplication:read or MyApplication:write
            $accessArray = explode(':', $role->getName());
            if (2 > count($accessArray)) {
                continue;
            }

            list($application, $accessType) = $accessArray;
            $userActiveRoles[$accessType][] = $application;

            // Write access includes read access too
            if ('write' === $accessType) {
                $userActiveRoles['read'][] = $application;
            }
        }

        return $userActiveRoles;
    }

    /**
     * @param UserApplicationAttribute[]|ArrayCollection $attributes
     * @return array
     */
    private function getAccessTokens($attributes): array
    {
        //now we check which serviceTokens the user is allowed to use
        $userServiceTokens = [];
        foreach ($attributes as $attribute) {
            if ($attribute->getName() === static::SERVICE_CONSUMER_ATTRIBUTE) {
                $userServiceTokens[] = $attribute->getValue();
            }
        }

        return $userServiceTokens;
    }

    /**
     * @param User $user
     * @param string $applicationName
     * @return UserApplication|null
     */
    private function findApplication(User $user, string $applicationName):? UserApplication
    {
        foreach ($user->getUserApplications() as $userApplication) {
            if ($userApplication->getActive() && $userApplication->getName() === $applicationName) {
                return $userApplication;
            }
        }
        return null;
    }

    /**
     * @param XmlRequest $xmlRequest
     * @return array
     */
    private function disabledFirewall(XmlRequest $xmlRequest)
    {
        $this->apiM->logger()->info('Application access control currently disabled.');

        $serviceToken = $xmlRequest->getCredentials()->getServiceProvider()->getServiceToken();
        $applications = $this->dbM->webservice()->application()->getAllApplications();

        $userActiveRoles = [];
        $userServiceTokens = [$serviceToken];
        foreach ($applications as $application) {
            $userActiveRoles['read'][] = $application->getName();
            $userActiveRoles['write'][] = $application->getName();
        }

        return [$userActiveRoles, $userServiceTokens];
    }
}
