<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

namespace oat\taoDeliveryRdf\controller;

use common_exception_RestApi;
use core_kernel_classes_Class;
use oat\generis\model\kernel\persistence\smoothsql\search\ComplexSearchService;
use oat\oatbox\event\EventManager;
use oat\search\base\exception\SearchGateWayExeption;
use oat\tao\model\taskQueue\QueueDispatcher;
use oat\tao\model\taskQueue\TaskLog\Broker\TaskLogBrokerInterface;
use oat\tao\model\taskQueue\TaskLog\Entity\EntityInterface;
use oat\tao\model\taskQueue\TaskLog\TaskLogFilter;
use oat\tao\model\taskQueue\TaskLogActionTrait;
use oat\tao\model\taskQueue\TaskLogInterface;
use oat\taoDeliveryRdf\model\Delete\DeliveryDeleteTask;
use oat\taoDeliveryRdf\model\DeliveryAssemblyService;
use oat\generis\model\OntologyRdfs;
use oat\taoDeliveryRdf\model\DeliveryFactory;
use oat\taoDeliveryRdf\model\tasks\CompileDelivery;
use oat\taoDeliveryRdf\model\tasks\UpdateDelivery;

class RestDelivery extends \tao_actions_RestController
{
    use TaskLogActionTrait;

    const REST_DELIVERY_TEST_ID        = 'test';
    const REST_DELIVERY_SEARCH_PARAMS  = 'searchParams';
    const REST_DELIVERY_ID             = 'delivery';
    const REST_DELIVERY_CLASS_URI      = 'delivery-uri';
    const REST_DELIVERY_CLASS_LABEL    = 'delivery-label';
    const REST_DELIVERY_CLASS_PARENT   = 'delivery-parent';
    const REST_DELIVERY_CLASS_COMMENT  = 'delivery-comment';
    const TASK_ID_PARAM                = 'id';

    const CLASS_LABEL_PARAM            = 'delivery-label';
    const CLASS_COMMENT_PARAM          = 'delivery-comment';
    const PARENT_CLASS_URI_PARAM       = 'delivery-parent';

    /**
     * @return EventManager
     */
    protected function getEventManager()
    {
        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
    }

    /**
     * Generate a delivery from test uri
     * Test uri has to be set and existing
     */
    public function generate()
    {
        try {
            if (!$this->hasRequestParameter(self::REST_DELIVERY_TEST_ID)) {
                throw new \common_exception_MissingParameter(self::REST_DELIVERY_TEST_ID, $this->getRequestURI());
            }

            $test = $this->getResource($this->getRequestParameter(self::REST_DELIVERY_TEST_ID));
            if (!$test->exists()) {
                throw new common_exception_RestApi('Unable to find a test associated to the given uri.');
            }

            $label = 'Delivery of ' . $test->getLabel();
            $deliveryClass = $this->getDeliveryClassByParameters();

            $deliveryFactory = $this->getServiceManager()->get(DeliveryFactory::SERVICE_ID);
            /** @var \common_report_Report $report */
            $report = $deliveryFactory->create($deliveryClass, $test, $label);

            if ($report->getType() == \common_report_Report::TYPE_ERROR) {
                $this->logInfo('Unable to generate delivery execution ' .
                    'into taoDeliveryRdf::RestDelivery for test uri ' . $test->getUri());
                throw new \common_Exception('Unable to generate delivery execution.');
            }
            $delivery = $report->getData();

            /** @var DeliveryFactory $deliveryFactoryService */
            $deliveryFactoryService = $this->getServiceManager()->get(DeliveryFactory::SERVICE_ID);
            $initialProperties = $deliveryFactoryService->getInitialPropertiesFromRequest($this->getRequest());
            $delivery = $deliveryFactoryService->setInitialProperties($initialProperties, $delivery);
            $this->returnSuccess(['delivery' => $delivery->getUri()]);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * Put task to generate a delivery from test uri to the task queue
     * Test uri has to be set and existing
     */
    public function generateDeferred()
    {
        try {
            if (! $this->hasRequestParameter(self::REST_DELIVERY_TEST_ID)) {
                throw new \common_exception_MissingParameter(self::REST_DELIVERY_TEST_ID, $this->getRequestURI());
            }

            $test = $this->getResource($this->getRequestParameter(self::REST_DELIVERY_TEST_ID));
            if (! $test->exists()) {
                throw new common_exception_RestApi('Unable to find a test associated to the given uri.');
            }

            $deliveryClass = $this->getDeliveryClassByParameters();

            /** @var DeliveryFactory $deliveryFactoryService */
            $deliveryFactoryService = $this->getServiceManager()->get(DeliveryFactory::SERVICE_ID);
            $initialProperties = $deliveryFactoryService->getInitialPropertiesFromRequest($this->getRequest());

            $task = CompileDelivery::createTask($test, $deliveryClass, $initialProperties);

            $result = [
                'reference_id' => $task->getId()
            ];

            /** @var TaskLogInterface $taskLog */
            $taskLog = $this->getServiceManager()->get(TaskLogInterface::SERVICE_ID);

            $report = $taskLog->getReport($task->getId());

            if (!empty($report)) {
                if ($report instanceof \common_report_Report) {
                    //serialize report to array
                    $report = json_decode($report);
                }
                $result['common_report_Report'] = $report;
            }

            return $this->returnSuccess($result);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * Update delivery by parameters
     */
    public function update()
    {
        try {
            if ($this->getRequestMethod() !== \Request::HTTP_POST) {
                throw new \common_exception_NotImplemented('Only post method is accepted to updating delivery');
            }

            if (! $this->hasRequestParameter(self::REST_DELIVERY_SEARCH_PARAMS)) {
                throw new \common_exception_MissingParameter(self::REST_DELIVERY_SEARCH_PARAMS, $this->getRequestURI());
            }

            $where = json_decode(html_entity_decode($this->getRequestParameter(self::REST_DELIVERY_SEARCH_PARAMS)), true);
            $propertyValues = $this->getRequestParameters();
            unset($propertyValues[self::REST_DELIVERY_SEARCH_PARAMS]);

            $deliveryModelClass = $this->getDeliveryRootClass();
            $deliveries = $deliveryModelClass->searchInstances($where, ['like' => false, 'recursive' => true]);

            $response = [];

            /** @var \core_kernel_classes_Resource $delivery */
            foreach ($deliveries as $key => $delivery) {
                foreach ($propertyValues as $rdfKey => $rdfValue) {
                    $rdfKey = \tao_helpers_Uri::decode($rdfKey);
                    $property = $this->getProperty($rdfKey);
                    $delivery->editPropertyValues($property, $rdfValue);
                }
                $response[] = ['delivery' => $delivery->getUri()];
            }
            $this->returnSuccess($response);
        } catch (\Exception $e) {
                $this->returnFailure($e);
        }
    }

    /**
     * Update delivery by parameters
     */
    public function updateDeferred()
    {
        try {
            if ($this->getRequestMethod() !== \Request::HTTP_POST) {
                throw new \common_exception_NotImplemented('Only post method is accepted to updating delivery');
            }
            if (! $this->hasRequestParameter(self::REST_DELIVERY_SEARCH_PARAMS)) {
                throw new \common_exception_MissingParameter(self::REST_DELIVERY_SEARCH_PARAMS, $this->getRequestURI());
            }
            $where = json_decode(html_entity_decode($this->getRequestParameter(self::REST_DELIVERY_SEARCH_PARAMS)), true);
            $propertyValues = $this->getRequestParameters();
            unset($propertyValues[self::REST_DELIVERY_SEARCH_PARAMS]);

            $task = UpdateDelivery::createTask($where, $propertyValues);

            $result = [
                'reference_id' => $task->getId()
            ];

            /** @var TaskLogInterface $taskLog */
            $taskLog = $this->getServiceManager()->get(TaskLogInterface::SERVICE_ID);
            $report = $taskLog->getReport($task->getId());
            if (!empty($report)) {
                if ($report instanceof \common_report_Report) {
                    //serialize report to array
                    $report = json_decode($report);
                }
                $result['common_report_Report'] = $report;
            }
            return $this->returnSuccess($result);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * Delete delivery by URI
     */
    public function deleteDeferred()
    {
        try {
            if ($this->getRequestMethod() !== \Request::HTTP_DELETE) {
                throw new \common_exception_NotImplemented('Only delete method is accepted to deleting delivery');
            }

            if (!$this->hasRequestParameter('uri')) {
                throw new \common_exception_MissingParameter('uri', $this->getRequestURI());
            }

            $uri = $this->getRequestParameter('uri');
            $delivery  = $this->getResource($uri);

            if (!$delivery->exists()) {
                $this->returnFailure(new \common_exception_NotFound('Delivery has not been found'));
            }

            /** @var QueueDispatcher $queueDispatcher */
            $queueDispatcher = $this->getServiceManager()->get(QueueDispatcher::SERVICE_ID);

            $task = new DeliveryDeleteTask();
            $task->setServiceLocator($this->getServiceLocator());
            $taskParameters = ['deliveryId' => $uri];

            $task = $queueDispatcher->createTask($task, $taskParameters, __('Deleting of "%s"', $delivery->getLabel()), null, true);

            $data = $this->getTaskLogReturnData(
                $task->getId(),
                DeliveryDeleteTask::class
            );
            $this->returnSuccess($data);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * List all deliveries or paginated range
     */
    public function get()
    {
        try {
            if ($this->getRequestMethod() !== \Request::HTTP_GET) {
                throw new \common_exception_NotImplemented('Only get method is accepted to getting deliveries');
            }

            $limit = 0;
            if ($this->hasRequestParameter('limit')) {
                $limit = $this->getRequestParameter('limit');
                if (!is_numeric($limit) || (int)$limit != $limit || $limit < 0) {
                    throw new \common_exception_ValidationFailed('limit', '\'Limit\' should be a positive integer');
                }
            }

            $offset = 0;
            if ($this->hasRequestParameter('offset')) {
                $offset = $this->getRequestParameter('offset');
                if (!is_numeric($offset) || (int)$offset != $offset || $offset < 0) {
                    throw new \common_exception_ValidationFailed('offset', '\'Offset\' should be a positive integer');
                }
            }

            $service = DeliveryAssemblyService::singleton();

            /** @var \core_kernel_classes_Resource[] $deliveries */
            $deliveries = $service->getAllAssemblies();
            $overallCount = count($deliveries);
            if ($offset || $limit) {
                if ($overallCount <= $offset) {
                    throw new \common_exception_ValidationFailed('offset', '\'Offset\' is too large');
                }
                $deliveries = array_slice($deliveries, $offset, $limit);
            }

            $mappedDeliveries = [];
            foreach ($deliveries as $delivery) {
                $mappedDeliveries[] = [
                    'uri' => $delivery->getUri(),
                    'label' => $delivery->getLabel(),
                ];
            }

            $response = [
                'items' => $mappedDeliveries,
                'overallCount' => $overallCount,
            ];
            $this->returnSuccess($response);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * Action to retrieve test compilation task status from queue
     */
    public function getStatus()
    {
        try {
            if (!$this->hasRequestParameter(self::TASK_ID_PARAM)) {
                throw new \common_exception_MissingParameter(self::TASK_ID_PARAM, $this->getRequestURI());
            }

            $data = $this->getTaskLogReturnData(
                $this->getRequestParameter(self::TASK_ID_PARAM),
                CompileDelivery::class
            );
            $children = $this->getStatusesForChildren($this->getRequestParameter(self::TASK_ID_PARAM));
            $data['children'] = $children;
            $this->returnSuccess($data);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * @param $taskId
     * @return array
     */
    protected function getStatusesForChildren($taskId)
    {
        /** @var TaskLogInterface $taskLog */
        $taskLog = $this->getServiceManager()->get(TaskLogInterface::SERVICE_ID);
        $filter = (new TaskLogFilter())
            ->eq(TaskLogBrokerInterface::COLUMN_PARENT_ID, $taskId);
        $collection = $taskLog->search($filter);
        $response = [];
        if ($collection->isEmpty()) {
            return $response;
        }
        /** @var EntityInterface $item */
        foreach ($collection as $item) {
            $response[] = [
                'id' => $this->getTaskId($item),
                'label' => $item->getLabel(),
                'status' => $this->getTaskStatus($item),
                'report' => $item->getReport() ? $this->getTaskReport($item) : []
            ];
        }
        return $response;
    }

    /**
     * Return 'Success' instead of 'Completed', required by the specified API.
     *
     * @param EntityInterface $taskLogEntity
     * @return string
     */
    protected function getTaskStatus(EntityInterface $taskLogEntity)
    {
        if ($taskLogEntity->getStatus()->isCreated()) {
            return 'In Progress';
        } elseif ($taskLogEntity->getStatus()->isCompleted()) {
            return 'Success';
        }

        return $taskLogEntity->getStatus()->getLabel();
    }

    /**
     * @param EntityInterface $taskLogEntity
     * @return array
     */
    protected function addExtraReturnData(EntityInterface $taskLogEntity)
    {
        $data = [];

        if ($taskLogEntity->getReport()) {
            $plainReport = $this->getPlainReport($taskLogEntity->getReport());

            //the second report is the report of the compilation test
            if (isset($plainReport[1]) && isset($plainReport[1]->getData()['uriResource'])) {
                $data['delivery'] = $plainReport[1]->getData()['uriResource'];
            }
        }

        return $data;
    }


    /**
     * Create a Delivery Class
     *
     * Label parameter is mandatory
     * If parent class parameter is an uri of valid delivery class, new class will be created under it
     * If not parent class parameter is provided, class will be created under root class
     * Comment parameter is not mandatory, used to describe new created class
     *
     * @return core_kernel_classes_Class
     */
    public function createClass()
    {
        try {
            $class = $this->createSubClass($this->getDeliveryRootClass());

            $result = [
                'message' => __('Class successfully created.'),
                'delivery-uri' => $class->getUri(),
            ];

            $this->returnSuccess($result);
        } catch (\common_exception_ClassAlreadyExists $e) {
            $result = [
                'message' => $e->getMessage(),
                'delivery-uri' => $e->getClass()->getUri(),
            ];
            $this->returnSuccess($result);
        } catch (\Exception $e) {
            $this->returnFailure($e);
        }
    }

    /**
     * Get a delivery class based on parameters
     *
     * If an uri parameter is provided, and it is a delivery class, this delivery class is returned
     * If a label parameter is provided, and only one delivery class has this label, this delivery class is returned
     *
     * @return core_kernel_classes_Class
     * @throws SearchGateWayExeption
     * @throws common_exception_RestApi
     */
    protected function getDeliveryClassByParameters()
    {
        $rootDeliveryClass = $this->getDeliveryRootClass();

        // If an uri is provided, check if it's an existing delivery class
        if ($this->hasRequestParameter(self::REST_DELIVERY_CLASS_URI)) {
            $deliveryClass = $this->getClass($this->getRequestParameter(self::REST_DELIVERY_CLASS_URI));
            if (
                $deliveryClass == $rootDeliveryClass
                || ($deliveryClass->exists() && $deliveryClass->isSubClassOf($rootDeliveryClass))
            ) {
                return $deliveryClass;
            }
            throw new common_exception_RestApi(__('Delivery class uri provided is not a valid delivery class.'));
        }

        if ($this->hasRequestParameter(self::REST_DELIVERY_CLASS_LABEL)) {
            $label = $this->getRequestParameter(self::REST_DELIVERY_CLASS_LABEL);

            $deliveryClasses = $rootDeliveryClass->getSubClasses(true);
            $classes = [$rootDeliveryClass->getUri()];
            foreach ($deliveryClasses as $class) {
                $classes[] = $class->getUri();
            }

            /** @var ComplexSearchService $search */
            $search = $this->getServiceManager()->get(ComplexSearchService::SERVICE_ID);
            $queryBuilder = $search->query();
            $criteria = $queryBuilder->newQuery()
                ->add(OntologyRdfs::RDFS_LABEL)->equals($label)
                ->add(OntologyRdfs::RDFS_SUBCLASSOF)->in($classes)
            ;
            $queryBuilder->setCriteria($criteria);
            $result = $search->getGateway()->search($queryBuilder);

            switch ($result->count()) {
                case 0:
                    throw new common_exception_RestApi(__('Delivery with label "%s" not found', $label));
                case 1:
                    return $this->getClass($result->current()->getUri());
                default:
                    $availableClasses = [];
                    foreach ($result as $raw) {
                        $availableClasses[] = $raw->getUri();
                    }
                    throw new common_exception_RestApi(__(
                        'Multiple delivery class found for label "%s": %s',
                        $label,
                        implode(',', $availableClasses)
                    ));
            }
        }

        return $rootDeliveryClass;
    }

    /**
     * Get the delivery root class
     *
     * @return core_kernel_classes_Class
     */
    protected function getDeliveryRootClass()
    {
        return $this->getClass(DeliveryAssemblyService::CLASS_URI);
    }
}