<?php

namespace idoit\Module\Forms\Controller;

use Exception;
use idoit\Component\ClassLoader\ModuleLoader;
use idoit\Controller\Base;
use idoit\Controller\Responseable;
use idoit\Module\Cmdb\Component\SyncMerger\Config;
use idoit\Module\Cmdb\Component\SyncMerger\Merger;
use idoit\Module\Cmdb\Search\Index\Signals;
use idoit\Module\Forms\Exceptions\DataByAttributeException;
use idoit\Module\Forms\Exceptions\FormRequestBodyException;
use idoit\Module\Forms\Exceptions\ProcessFormDataException;
use idoit\Module\Forms\Exceptions\ProcessorException;
use idoit\Module\Forms\Model\AttributeDefaultValueDao;
use idoit\Module\Forms\Model\CategoryDao;
use idoit\Module\Forms\Model\FormRequestBodyDao;
use idoit\Module\Forms\Model\ProcessorDao;
use idoit\Module\Forms\Model\Processors\AbstractProcessor;
use idoit\Module\Forms\Model\Processors\Interfaces\PreMergeModifierInterface;
use idoit\Module\Forms\Model\Processors\Interfaces\PreSyncModifierInterface;
use idoit\Module\Forms\Model\Validators\ValidatorProvider;
use idoit\Module\Forms\Proxy\RequestProxy;
use isys_application as Application;
use isys_cmdb_dao;
use isys_component_database_proxy;
use isys_component_tree;
use isys_controller as Controller;
use isys_event_manager;
use isys_exception_cmdb;
use isys_exception_dao;
use isys_exception_database;
use isys_import_handler_cmdb;
use isys_module;
use isys_register as Register;
use Throwable;

class ApplyData extends Base implements Controller, Responseable
{
    /**
     * @var int
     */
    private $objectId = null;

    /**
     * @var ProcessorDao
     */
    private $processorDao;

    /**
     * @var array
     */
    private $prepareIssues = [];

    /**
     * @var array
     */
    private $syncIssues = [];

    /**
     * @var isys_cmdb_dao
     */
    private $dao;

    /**
     * @var isys_component_database_proxy
     */
    private $database;

    /**
     * @var string|array|null
     */
    private $response = null;

    /**
     * @var int|null
     */
    private ?int $creationTime = null;

    /**
     * @var array
     */
    private array $globalCategories = [];

    /**
     * @var array
     */
    private array $specificCategories = [];

    /**
     * @param isys_module $p_module
     *
     * @throws \Exception
     */
    public function __construct(isys_module $p_module)
    {
      $this->dao = Application::instance()->container->get('cmdb_dao');
      $this->database = Application::instance()->container->get('database');
    }

    /**
     * @return void
     */
    public function pre()
    {
      header('Content-Type: application/json; charset=UTF-8');
    }

    /**
     * @return void
     */
    public function post()
    {
      echo json_encode($this->response);
      exit;
    }

    /**
     * @return array|string|null
     */
    public function getResponse()
    {
      return $this->response;
    }

    public function dao(Application $p_application)
    {
      // Nothing to do
    }

    public function handle(Register $p_request, Application $p_application)
    {
      // Nothing to do
    }

    public function tree(Register $p_request, Application $p_application, isys_component_tree $p_tree)
    {
      // Nothing to do
    }

    /**
     * @todo Implement prevalidation of the form data
     *
     * @param array $formData
     * @param int $objectTypeId
     *
     * @return array|null
     * @throws ProcessFormDataException
     */
    private function validateFormData(array $formData, int $objectTypeId)
    {
        $dao = CategoryDao::instance($this->database);
        $validator = ValidatorProvider::factory();
        $validationIssues = [];

        foreach($formData as $categoryConst => $categoryData) {
            $categoryDao = $dao->loadCategoryDaoByConstant($categoryConst);
            $categoryDao->set_object_type_id($objectTypeId);
            foreach ($categoryData as $ident =>  $entry) {
                $issuesInEntry = $validator->validate($categoryConst, $entry, $categoryDao);
                if (is_array($issuesInEntry)) {
                  $validationIssues[$categoryConst][$ident] = $issuesInEntry;
                }
            }
        }

        if (!empty($validationIssues)) {
            throw ProcessFormDataException::ValidationIssues($validationIssues);
        }

        return empty($validationIssues) ? null: $validationIssues;
    }

    /**
     * @param array $formData
     * @param int   $objectTypeId
     *
     * @return array
     *
     * @throws DataByAttributeException
     * @throws ProcessFormDataException
     * @throws ProcessorException
     * @throws isys_exception_cmdb
     * @throws isys_exception_dao
     * @throws isys_exception_database
     */
    private function prepareFormData(array $formData, int $objectTypeId)
    {
        $objectTitle = $formData['C__CATG__GLOBAL'][0]['title'] ?: 'Created by forms: ' . date('Y-m-d H:i:s');
        $sysId = $formData['C__CATG__GLOBAL'][0]['sysid'] ?? null;
        $this->creationTime = (int)microtime(true);

        // @see AOF-43 Validate data before creating object.
        $this->validateFormData($formData, $objectTypeId);

        $this->objectId = $this->dao->insert_new_obj($objectTypeId, null, $objectTitle, $sysId, C__RECORD_STATUS__NORMAL);

        isys_event_manager::getInstance()
            ->triggerCMDBEvent(
                'C__LOGBOOK_EVENT__OBJECT_CREATED',
                '',
                $this->objectId,
                $objectTypeId,
                null,
                serialize([
                    'isys_cmdb_dao_category_g_global::title' => [
                        'from' => '',
                        'to' => $objectTitle
                    ]
                ]),
                null,
                null,
                null,
                null,
                1,
                defined_or_default('C__LOGBOOK_SOURCE__FORMS')
            );

        $prepareIssues = $syncData = [];
        $defaultValueDao = AttributeDefaultValueDao::factory($this->database, $objectTypeId);

        $data = $defaultValueDao->getTemplateEntries();
        foreach ($data as $categoryConst => $templateData) {
            foreach ($templateData['data'] as $entry) {
                if ($entry[Config::CONFIG_DATA_ID] > 0) {
                    $entry[Config::CONFIG_DATA_ID] = null;
                    $entry[Config::CONFIG_PROPERTIES]['id'] = null;
                    $syncData[$categoryConst][] = $entry;
                }
            }
        }

        foreach ($formData as $categoryConst => $dataArray) {
            $debugData[] = "Process category {$categoryConst}";

            $categoryDao = CategoryDao::instance($this->database)->loadCategoryDaoByConstant($categoryConst);

            if (empty($dataArray)) {
                continue;
            }

            $processor = $this->processorDao->getProcessor($categoryConst);

            foreach ($dataArray as $entry) {
                if (empty($entry)) {
                  continue;
                }

                $fakeEntry = [
                    Config::CONFIG_DATA_ID    => null,
                    Config::CONFIG_PROPERTIES => array_map(function ($prop) {
                        return [C__DATA__VALUE => $prop];
                    }, $entry)
                ];

                try {
                    if ($processor instanceof PreMergeModifierInterface) {
                        $fakeEntry = $processor->preMergeModify($fakeEntry, $this->objectId);
                    }

                    $currentSyncData = Merger::instance(Config::instance($categoryDao, $this->objectId, $fakeEntry))->getDataForSync();

                    if (!$categoryDao->is_multivalued() && isset($syncData[$categoryConst])) {
                        $templateSyncData = current($syncData[$categoryConst]);
                        $currentSyncData = array_replace_recursive($templateSyncData, $currentSyncData);
                        $syncData[$categoryConst] = [
                            $currentSyncData
                        ];
                        continue;
                    }

                    $syncData[$categoryConst][] = $currentSyncData;
                } catch (Exception $e) {
                    $prepareIssues[$categoryConst][] = $e->getMessage();
                }
            }
        }

        if (!empty($prepareIssues)) {
            throw ProcessFormDataException::PrepareDataIssues($prepareIssues);
        }

        return $syncData;
    }

    /**
     * @param $syncData
     *
     * @throws isys_exception_database
     * @throws DataByAttributeException
     * @throws ProcessorException
     * @throws ProcessFormDataException
     * @throws Exception
     */
    private function applySyncData($syncData)
    {
        if (empty($syncData))
        {
            throw new Exception('Cannot create object on empty sync data');
        }
        $syncIssues = [];
        $objectTypeId = $this->dao->get_objTypeID($this->objectId);

        foreach($syncData as $categoryConst => $entries)
        {
            $processor = $this->processorDao->getProcessor($categoryConst);
            if ($processor instanceof AbstractProcessor) {
                $processor->setDependentSyncData($syncData);
            }

            $categoryDao = CategoryDao::instance($this->database)->loadCategoryDaoByConstant($categoryConst);

            // @see ID-10667 Set object ID before validation.
            $categoryDao->set_object_id($this->objectId);
            $categoryClassName = get_class($categoryDao);

            if ($categoryDao->get_category_type() === C__CMDB__CATEGORY__TYPE_SPECIFIC) {
                $this->specificCategories[] = $categoryConst;
            } else {
                $this->globalCategories[] = $categoryConst;
            }

            foreach ($entries as $data) {
                $mode = isys_import_handler_cmdb::C__CREATE;

                try {
                    if ($processor instanceof PreSyncModifierInterface) {
                        $data = $processor->preSyncModify($data, $this->objectId);
                    }
                    $validationIssues = $categoryDao->validate($data);
                    if (is_array($validationIssues)) {
                        $syncIssues[$categoryConst] = array_merge($syncIssues[$categoryConst], $validationIssues);
                        continue;
                    }

                    if ($data[Config::CONFIG_DATA_ID] !== null) {
                        $mode = isys_import_handler_cmdb::C__UPDATE;
                    }

                    $categoryDao->sync($data, $this->objectId, $mode);

                    $changes = [];

                    foreach ($data[isys_import_handler_cmdb::C__PROPERTIES] as $key => $value) {
                        if (!isset($value[C__DATA__VALUE]) || trim($value[C__DATA__VALUE]) === '') {
                            continue;
                        }

                        $changes[$categoryClassName . '::' . $key] = [
                            'from' => '',
                            'to'   => $value[C__DATA__VALUE]
                        ];
                    }

                    isys_event_manager::getInstance()
                        ->triggerCMDBEvent(
                            'C__LOGBOOK_EVENT__CATEGORY_CHANGED',
                            '',
                            $this->objectId,
                            $objectTypeId,
                            $categoryDao->getCategoryTitle(),
                            serialize($changes),
                            null,
                            null,
                            null,
                            null,
                            count($changes),
                            defined_or_default('C__LOGBOOK_SOURCE__FORMS')
                        );
                } catch (Exception $e) {
                    if (!isset($syncIssues[$categoryConst])) {
                        $syncIssues[$categoryConst] = [];
                    }
                    $syncIssues[$categoryConst][] = $e->getMessage();
                }
            }
        }

        if (!empty($syncIssues)) {
            throw ProcessFormDataException::SyncIssues($syncIssues);
        }
    }

    /**
     * @return void
     * @throws Exception
     */
    private function generateIndex(): void
    {
        Signals::instance()->onPostImport(
            $this->creationTime,
            array_unique($this->globalCategories),
            array_unique($this->specificCategories)
        );
    }

    /**
     * @param Register     $request
     * @param ModuleLoader $moduleLoader
     */
    public function execute(Register $request, ModuleLoader $moduleLoader): void
    {
        $this->response = [];
        try {
            $formBodyObject = FormRequestBodyDao::factory();
        } catch (FormRequestBodyException $e) {
            $this->response['message'] = $e->getMessage();
            return;
        }

        $formData = $formBodyObject->getFormData();
        $objectType = $formBodyObject->getObjectType();

        if (!is_numeric($objectType)) {
            $objectType = $this->dao->getObjectTypeId($objectType);
        }
        $session = Application::instance()->container->get('session');
        $proxy = new RequestProxy($session);

        try {
            try {
                $this->validateFormData($formData, (int)$objectType);
                $this->processorDao = ProcessorDao::factory();
                $this->applySyncData($this->prepareFormData($formData, (int)$objectType));
                $this->generateIndex();
            } catch (ProcessFormDataException $e) {
                http_response_code($e->getCode());
                $this->response['message'] = $e->getMessage();
                $this->response['errors'] = $e->getIssues();
                return;
            } catch (Throwable $e) {
                http_response_code($e->getCode());
                $this->response['message'] = $e->getMessage();
                $this->response['errors'] = [];
                return;
            }

            $this->response['message'] = 'Form successfully applied.';
            $this->response['objectId'] = $this->objectId;
        } finally {
            try {
                $proxy->proxy('POST', 'form/data', json_encode([
                    'form' => $formBodyObject->getFormId(),
                    'data' => [
                        'request' => $formData,
                        'response' => $this->response,
                        'userdata' => $session->get_userdata()
                    ],
                ]));
            } catch (Throwable $e) {
            }
        }
    }
}
