<?php

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

use Sso\WebserviceBundle\Entity\ServiceProvider\Type\Token;
use Sso\WebserviceBundle\ErrorHandler\ErrorStorage\Factory as ErrorFactory;
use Sso\WebserviceBundle\Logger\ProcessDetails;
use Sso\WebserviceBundle\Security\Api\Manager as ApiManager;
use Sso\WebserviceBundle\Security\Exception\FirewallException;
use Sso\WebserviceBundle\Security\Model\Request\Request as XmlRequest;

/**
 * Class Three
 * @package Sso\WebserviceBundle\Security\Api\Firewall\Level
 */
class Three
{
    /**
     * @var ApiManager
     */
    private $apiM;

    /**
     * @var ProcessDetails
     */
    private $processDetails;

    /**
     * @var ErrorFactory
     */
    private $errorStorage;

    /**
     * Factory constructor.
     * @param ApiManager $apiM
     * @param ProcessDetails $processDetails
     */
    public function __construct(ApiManager $apiM, ProcessDetails $processDetails)
    {
        $this->apiM = $apiM;
        $this->processDetails = $processDetails;
        $this->errorStorage = $this->apiM->getErrorHandler()->errorStorage();
    }

    /**
     * @param string $xmlString
     * @return array
     * @throws FirewallException
     */
    public function validateRequest($xmlString)
    {
        $requestModel = $this->parseRequest($xmlString);
        $this->storeUsername($requestModel);
        $this->validateCredentials($requestModel);
        $accessRestricted = $this->validateToken($requestModel);

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

        return [$requestModel, $accessRestricted];
    }

    /**
     * @param $xmlString
     * @return XmlRequest
     * @throws FirewallException
     */
    private function parseRequest($xmlString): XmlRequest
    {
        try {
            $requestModel = $this->apiM->serializer()->deserialize($xmlString, XmlRequest::class, 'xml');
        } catch (\Exception $exc) {
            $this->errorStorage->setErrorFromValues(400, 'fw003', 'hd006', 'InvalidXml', $exc->getMessage());
            throw new FirewallException('Firewall Level Three Exception');
        }

        if (!$requestModel instanceof XmlRequest) {
            $this->errorStorage->setErrorFromValues(400, 'fw003', 'hd009', 'InvalidXml', 'Unable to parse xml.');
            throw new FirewallException('Firewall Level Three Exception');
        }

        return $requestModel;
    }

    /**
     * Try to get username from xml even if xml is not valid
     *
     * @param XmlRequest $requestModel
     */
    private function storeUsername(XmlRequest $requestModel): void
    {
        if (
            $requestModel->getCredentials() &&
            $requestModel->getCredentials()->getServiceTrigger() &&
            ($username = $requestModel->getCredentials()->getServiceTrigger()->getUserName())
        ) {
            $this->processDetails->setUsername($username);
        }
    }

    /**
     * @param XmlRequest $requestModel
     * @throws FirewallException
     */
    private function validateCredentials(XmlRequest $requestModel): void
    {
        $violations = $this->apiM->validator()->validate($requestModel);
        if (count($violations) > 0) {
            foreach ($violations as $error) {
                $message = $error->getPropertyPath() . ": " . $error->getMessage();
                $this->errorStorage->setErrorFromValues(400, 'fw003', 'hd007', 'InvalidXml', $message);
            }
        }
        if ($this->errorStorage->hasErrors()) {
            throw new FirewallException('Firewall Level Three Exception');
        }
    }

    /**
     * Checks if token exists and validate extended request credentials
     *
     * @param XmlRequest $requestModel
     * @return bool returns true, if token has restricted access
     * @throws FirewallException
     */
    private function validateToken(XmlRequest $requestModel): bool
    {
        $tokenId = $requestModel->getCredentials()->getServiceProvider()->getServiceToken();
        $token = $this->apiM->database()->serviceProvider()->token()->getOneById($tokenId);
        if (!$token instanceof Token) {
            $this->errorStorage->setErrorFromValues(401, 'fw004', 'hd011', 'InvalidToken', 'Service token not found in database');
            throw new FirewallException('Firewall Level Three Exception');
        }

        // Additional validation of xml if token has restricted access
        if ($token->isAccessRestricted()) {
            $violations = $this->apiM->validator()->validate($requestModel, ['accessRestricted']);
            if (count($violations) > 0) {
                foreach ($violations as $error) {
                    $message = $error->getPropertyPath() . ": " . $error->getMessage();
                    $this->errorStorage->setErrorFromValues(400, 'fw003', 'hd010', 'InvalidXml', $message);
                }
                throw new FirewallException('Firewall Level Three Exception');
            }

            return true;
        }

        return false;
    }
}
