<?php

/**
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is protected 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
 * @version    $Id:$
 * @link       http://www.life-style.de
 */

namespace Sso\ApiBundle\Api\Controller;

use Sso\ApiBundle\Api;
use Sso\ApiBundle\Api\Exception\Type\Api as ApiException;
use Sso\ApiBundle\Api\Error\Type\External as ExternalError;
use Sso\ApiBundle\Api\Error\Type\Internal as InternalError;
use Sso\ApiBundle\Api\Controller as Controller;
use Sso\ApiBundle\Api\Response\Builder as XmlResponse;

/**
 * Class IndexController
 * @package Sso\ApiBundle\Api\Controller
 */
class IndexController
{

    /**
     * List of available controllers
     *
     * @var array
     */
    protected $_controllerList = array(
        'Language',
        'Location',
    );

    /**
     * Current controller name
     *
     * @var string
     */
    private $_controllerName = 'Unknown';

    /**
     * Current action name
     *
     * @var string
     */
    private $_actionName = 'Unknown';

    /**
     * Controller node in xml document
     *
     * @var DOMElement
     */
    private $_xmlController;

    /**
     * Credentials node in xml document
     *
     * @var DOMElement
     */
    private $_xmlCredentials;

    /**
     * Request xml
     *
     * @var DOMDocument
     */
    public $simpleXML = false;

    /**
     * Api-Manager
     *
     * @var Api\Manager
     */
    public $apiM;

    /**
     *
     * @var Api\Error\Storage
     */
    public $apiErrors;

    /**
     * Constructor
     *
     * @param Api\Manager $apiM
     */
    public function __construct(Api\Manager $apiM)
    {
        $this->apiM = $apiM;
        $this->apiErrors = new Api\Error\Storage();
    }

    /**
     * Exit with error
     *
     * @param Api\Error\Type\ $error
     * @throws ApiException
     */
    private function _exception(Api\Error\Type\Base $error = null)
    {
        if (null !== $error) {
            $this->apiErrors->addError($error);
        }
        $exception = new ApiException();
        $exception->setErrors($this->apiErrors->getErrors());
        throw $exception;
    }

    /**
     * Get crontroler object
     *
     * @param string $controllerName
     * @param string $actionName
     * @param string $xmlRequest
     * @param XmlResponse $xmlResponse
     * @param string $remoteAddress
     * @return Controller\ControllerAbstract
     */
    public function getController($controllerName, $actionName, $xmlRequest, XmlResponse $xmlResponse, $remoteAddress)
    {

        // Check controller
        $this->_validateController($controllerName);

        // Validate and load xml
        $this->_validateXML($xmlRequest);

        // Check xml structure
        $this->_validateXmlElements();

        // Validate controller in xml against controller in header
        if ($this->_xmlController->getName() != $controllerName) {
            $this->_exception(new ExternalError('xh20', 'ControllerInvalid',
                'Controller in header does not match controller in xml'));
        }

        // Retrieve action name
        if (!($this->_actionName = $this->_getAction())) {
            $this->_exception(new ExternalError('xh21', 'ActionNotFound'));
        }

        // Validate action in xml against action in header
        if ($this->_actionName != $actionName) {
            $this->_exception(new ExternalError('xh22', 'ActionInvalid',
                'Action in header does not match action in xml'));
        }

        // Log usage
        ApiException::setAction($this->_actionName);

        $this->apiM->database()->serviceProvider()->tokenUsage()->setAction($this->_actionName);

        // Validate Credentials
        $this->_validateCredentials($controllerName, $this->_actionName, $remoteAddress);

        // Throw exception, if any error has occured
        if ($this->apiErrors->hasErrors()) {
            $this->_exception();
        }

        $className = __NAMESPACE__ . '\\' . $controllerName . 'Controller';
        return new $className($this->apiM, $this->_xmlController, $xmlResponse);
    }

    /**
     * Validate controller name
     *
     * @param string $controllerName
     */
    private function _validateController($controllerName)
    {
        // Set controller name for exit handler
        $this->_controllerName = (string)$controllerName;

        if (!in_array($controllerName, $this->_controllerList)) {
            $this->_exception(new ExternalError('xh30', 'ControllerInvalid',
                'Controller "' . $controllerName . '" is no valid controller.'));
        }

        // Set controller in token usage logger
        ApiException::setController($controllerName);
        $this->apiM->database()->serviceProvider()->tokenUsage()->setController($controllerName);
    }

    /**
     * Validate xml
     *
     * @param string $xml
     */
    private function _validateXML($xml)
    {
        libxml_clear_errors();
        libxml_disable_entity_loader(true);
        libxml_use_internal_errors(true);

        $this->simpleXML = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_COMPACT);

        if (!$this->simpleXML) {
            $errors = libxml_get_errors();
            foreach ($errors as $error) {
                $this->apiErrors->addError(new ExternalError('xh10', 'XmlInvalid', $error->message));
            }
            libxml_clear_errors();
            libxml_disable_entity_loader(false);
            $this->_exception();
        } else {
            libxml_clear_errors();
            libxml_disable_entity_loader(false);
        }
    }

    /**
     * Validate xml structure and read controller and credentials from xml
     *
     * @param DOMElement $xml
     */
    private function _validateXmlElements()
    {
        $xmlElementCnt = 0;
        foreach ($this->simpleXML as $root) {
            switch ($xmlElementCnt) {
                case 0:
                    $this->_xmlCredentials = $root;
                    break;
                case 1:
                    $this->_xmlController = $root;
                    break;
                default:
                    $this->_exception(new ExternalError('xh01', 'XmlInvalid', 'Too many nodes in xml'));
            }
            ++$xmlElementCnt;
        }

        // Check credential node
        if (null === $this->_xmlCredentials) {
            $this->_exception(new ExternalError('xh02', 'CredentialsNotFound'));
        }

        // Check controller node
        if (null === $this->_xmlController) {
            $this->_exception(new ExternalError('xh03', 'ControllerNotFound'));
        }
    }

    /**
     * Validate credentials
     *
     * @param string $controllerName
     * @param string $actionName
     * @param string $remoteAddress
     */
    private function _validateCredentials($controllerName, $actionName, $remoteAddress)
    {
        try {
            $this->apiM->credentials()->validateCredentials($this->_xmlCredentials, $controllerName, $actionName,
                $remoteAddress);
        } catch (\Exception $exc) {
            if ($exc instanceof Api\Exception\Type\Base) {
                $this->apiErrors->addErrors($exc->getErrors());
            }
            if (!($exc instanceof Api\Credentials\Exception)) {
                $this->_exception(new InternalError('api1', 'ApiError', '', '', $exc));
            }
        }
    }

    /**
     * Read action from xml
     *
     * @return string
     */
    private function _getAction()
    {
        $actionCount = 0;
        foreach ($this->_xmlController as $xmlNodeAction) {
            if ($actionCount == 0) {
                $actionXml = $xmlNodeAction;
                break;
            }
        }
        return (string)$actionXml->getName();
    }
}
