<?php

namespace idoit\Module\CustomFields\Controller;

use Exception;
use idoit\Component\ConstantManager;
use idoit\Component\Helper\Purify;
use idoit\Component\Helper\Unserialize;
use idoit\Module\Cmdb\Component\Table\Config\Refresher;
use idoit\Module\CustomFields\PropertyTypes;
use isys_application;
use isys_component_database;
use isys_component_template_language_manager;
use isys_custom_fields_dao;
use isys_format_json;
use isys_helper_link;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class Ajax
{
    private isys_component_template_language_manager $language;
    private isys_component_database $database;

    public function __construct()
    {
        $this->language = isys_application::instance()->container->get('language');
        $this->database = isys_application::instance()->container->get('database');
    }

    /**
     * @param Request $request
     * @param int     $id
     * @return JsonResponse
     */
    public function loadConfiguration(Request $request, int $id): JsonResponse
    {
        try {
            $result = [];

            $serializedFieldConfiguration = isys_custom_fields_dao::instance($this->database)
                ->get_data($id)
                ->get_row_value('isysgui_catg_custom__config');

            $fieldConfiguration = Unserialize::toArray($serializedFieldConfiguration ?? Unserialize::EMPTY_ARRAY);

            foreach ($fieldConfiguration as $key => $field) {
                $field['key'] = $key;
                $field['show_in_list'] = (bool)$field['show_in_list'];
                $field['show_inline'] = (bool)$field['show_inline'];
                $field['disable'] = true;

                // This is necessary to determine the correct object browser.
                if ($field['type'] === 'f_popup' && $field['popup'] === 'browser_object') {
                    $field['relation'] = (isset($field['identifier']) && $field['identifier'] > 0) ? 1 : 0;
                }

                $field['configValue'] = PropertyTypes::getTypeByConfiguration($field, $this->language)['configValue'];

                $result[] = $field;
            }

            return new JsonResponse([
                'success' => true,
                'message' => '',
                'data'    => $result,
            ]);
        } catch (\Exception $e) {
            return new JsonResponse([
                'success' => false,
                'message' => $e->getMessage(),
                'data'    => null,
            ]);
        }
    }

    /**
     * @param Request $request
     * @param int     $id
     * @return JsonResponse
     * @throws \idoit\Exception\JsonException
     */
    public function saveConfiguration(Request $request, int $id): JsonResponse
    {
        $htmlPurifier = isys_application::instance()->container->get('htmlpurifier');
        $notify = isys_application::instance()->container->get('notify');
        $logger = isys_application::instance()->container->get('logger');
        $signals = isys_application::instance()->container->get('signals');

        $config = [];
        $l_dao = isys_custom_fields_dao::instance($this->database);

        $changedKeys = [];
        $requestData = $request->request->all();
        $properties = isys_format_json::decode($requestData['properties']);
        $title = $requestData['categoryTitle'];
        $multivalued = $requestData['multivalued'];
        $constant = $requestData['categoryConstant'];
        $labelPosition = $requestData['labelPosition'];
        $icon = $requestData['icon'];
        $objectTypeIds = explode(',', $requestData['assignedObjectTypes']);

        foreach ($properties as $property) {
            /*
             * Removing line breaks and tabs to prevent that the config can not be loaded.
             * Key fields should not have any line breaks or tabs.
             */
            if (!in_array($property['configValue'], ['script', 'html'], true)) {
                $property['title'] = str_replace(["\r", "\n", "\t"], '', $property['title']);
            }

            $key = trim($property['key']);
            $newKey = trim($property['newKey'] ?? '');

            if (is_numeric($newKey)) {
                $newKey = 'c_' . $newKey;
            }

            $keyChanged = $key !== $newKey;

            /*
             * This is necessary, for one reason:
             *
             * If we don't pre- or append a string the key will be an integer. This is a problem because:
             * When we serialize integers errors might appear, when we change from a 32 to a 64 bit machine!
             *
             * @see ID-8404 Only do this, if the identifier is 'numeric'.
             */
            if (is_numeric($key)) {
                $key = 'c_' . $key;

                // Just go sure that the key might have changed.
                if (!$keyChanged) {
                    $keyChanged = $key !== $newKey;
                }
            }

            // Save the changed keys to update the referenced data.
            if ($newKey !== '' && $keyChanged) {
                $changedKeys[] = ['from' => $key, 'to' => $newKey];
                $key = $newKey;
            }

            if (isset($config[$key])) {
                $notify->error("They key '{$key}' seems duplicated!");

                return new JsonResponse([
                    'success' => false,
                    'message' => "The key '{$key}' seems duplicated, save process was cancelled.",
                    'data'    => null,
                ]);
            }

            // If type contains a comma, its a popup which needs more infos.
            if (false !== strpos($property['configValue'], ',')) {
                $data = explode(',', $property['configValue']);

                if ($data[1] === 'yes-no') {
                    $config[$key] = [
                        'type'    => $data[0],
                        'extra'   => $data[1],
                        'title'   => $property['title'],
                        'default' => $property['default']
                    ];
                } elseif ($data[1] === 'date-datetime') {
                    $config[$key] = [
                        'type'    => $data[0],
                        'popup'   => $data[2],
                        'extra'   => $data[1],
                        'title'   => $property['title'],
                        'default' => $property['default']
                    ];
                } else {
                    $config[$key] = [
                        'type'  => $data[0],
                        'popup' => $data[1],
                        'title' => $property['title']
                    ];
                }

                // Dialog or Dialog+?
                if (in_array($data[1], ['dialog', 'dialog_plus', 'report_browser', 'checkboxes'], true)) {
                    if (isset($property['identifier'])) {
                        if ($data[1] !== 'report_browser') {
                            $identifier = trim($property['identifier']);

                            // @see ID-12186 Strip quotes, slashes and bad words as whole words. Keep strings like 'selection'.
                            $cleanCondition = function (string $str): string {
                                return Purify::removeSqlIndicators(str_replace(['"', "'", '\\', '/'], '', $str));
                            };

                            if ($identifier === '') {
                                // If no identifier was passed, use the title and run through 'format constant'.
                                $property['identifier'] = $cleanCondition(Purify::formatConstant($property['title']));
                            } else {
                                $property['identifier'] = $cleanCondition($identifier);
                            }
                        }

                        $config[$key]["identifier"] = $property['identifier'];
                    }

                    if ($data[2] == 1) {
                        $config[$key]['multiselection'] = true;
                    }

                    // Check if we need to process dependency data.
                    if ($data[1] === 'dialog_plus' && isset($property['dialogDependency'], $property['dialogLinkedTo'])) {
                        // Multiselect dialog+ is only allowed to be "secondary" (= it can only depend on a single value dialog+).
                        $config[$key]['dialogDependency'] = $property['dialogDependency'];
                        $config[$key]['dialogLinkedTo'] = $property['dialogLinkedTo'];
                    }
                }

                // Normal Object browser or with relation?
                if ($data[1] === 'browser_object') {
                    if ($data[2] == 1) {
                        $config[$key]["identifier"] = $property['identifier'];
                    }

                    if ($data[3] == 1) {
                        $config[$key]['multiselection'] = true;
                    }
                }
            } elseif (in_array($property['configValue'], ['script', 'html'], true)) {
                // @see ID-9109 In case of HTML or Javascript we can not use the purifier or we might destroy the content.
                $config[$key] = [
                    "type"  => $property['configValue'],
                    "title" => $property['title']
                ];
            } else {
                $config[$key] = [
                    "type"  => $property['configValue'],
                    "title" => $htmlPurifier->purify($property['title'])
                ];
            }

            if (isset($property['visibility'])) {
                $config[$key]['visibility'] = $property['visibility'];
            }

            // Save show_in_list config.
            $config[$key]['show_in_list'] = isset($property['show_in_list']) && $property['show_in_list'];

            // @see  ID-641  Save the 'inline' configuration.
            $config[$key]['show_inline'] = $property['show_inline'];
        }

        if (count($config)) {
            // @see ID-8404 Before further processing we check if we need to update dialog-dependency keys.
            if (count($changedKeys)) {
                foreach ($config as &$property) {
                    if (isset($property['dialogLinkedTo'])) {
                        foreach ($changedKeys as $pair) {
                            if ($property['dialogLinkedTo'] == $pair['from']) {
                                $property['dialogLinkedTo'] = $pair['to'];
                            }
                        }
                    }
                }
            }

            try {
                if (!$l_dao->validateTitle($title)) {
                    throw new Exception($this->language->get('LC__MODULE__QCW__CREATE_CUSTOM_CATEGORY_VALIDATION'));
                }

                $title = Purify::purifyValue(trim($title));

                if ($id > 0) {
                    try {
                        // @see ID-8404 If keys have been changed, we need to update the data-reference.
                        foreach ($changedKeys as $change) {
                            $signals->emit('mod.cmdb.customCategoryPropertyKeyChanged', $id, $change['from'], $change['to']);
                        }
                    } catch (Exception $e) {
                        $logger->critical('An error occured while changing custom-category property keys:', $changedKeys);
                        $notify->error($e->getMessage(), ['sticky' => true]);

                        return new JsonResponse([
                            'success' => false,
                            'message' => $e->getMessage(),
                            'data'    => null,
                        ]);
                    }

                    $l_dao->save($id, $title, $config, 0, 0, $multivalued, $constant, $labelPosition, $icon);
                } else {
                    $id = $l_dao->create($title, $config, 0, 0, $multivalued, $constant, $labelPosition, $icon);

                    if (!is_numeric($id)) {
                        throw new Exception($this->language->get('LC__CMDB__OBJECT_BROWSER__SCRIPT__OBJECT_DATA_LOAD_ERROR'));
                    }
                }

                // Clear all object type assignments.
                $l_dao->clear_assignments($id);

                // Add Generic template.
                if (defined('C__OBJTYPE__GENERIC_TEMPLATE')) {
                    if (!in_array(C__OBJTYPE__GENERIC_TEMPLATE, $objectTypeIds)) {
                        $objectTypeIds[] = C__OBJTYPE__GENERIC_TEMPLATE;
                    }
                }

                if (is_array($objectTypeIds)) {
                    foreach ($objectTypeIds as $objectTypeId) {
                        if ($objectTypeId > 0) {
                            $l_dao->assign($id, $objectTypeId);
                        }
                    }

                    $l_dao->clearUnassignedOverviewTypes($id, $objectTypeIds);
                }

                // Update constant cache:

                /** @var ConstantManager $cm */
                $cm = isys_application::instance()->container->get('constant_manager');
                $cm->deleteTenantCacheFile();

                // @see  ID-6382  Refresh the table configurations of the assigned object types.
                $refresher = new Refresher($this->database);

                $sql = 'SELECT GROUP_CONCAT(isys_obj_type__const) AS constantList
                    FROM isys_obj_type
                    WHERE isys_obj_type__id ' . $l_dao->prepare_in_condition($objectTypeIds) . ';';

                $constantList = $l_dao->retrieve($sql)->get_row_value('constantList');

                $objectTypeConstants = explode(',', $constantList);

                foreach ($objectTypeConstants as $objectTypeConstant) {
                    $refresher->processByObjectTypeConstant($objectTypeConstant);
                }

                // Return a redirect URL so the configuration gets "reloaded".
                $notify->info($this->language->get('LC__INFOBOX__DATA_WAS_SAVED'));

                return new JsonResponse([
                    'success' => true,
                    'message' => '',
                    'data'    => [
                        'redirectUrl' => isys_helper_link::create_url([
                            C__GET__MODULE_ID     => C__MODULE__SYSTEM,
                            C__GET__MODULE_SUB_ID => C__MODULE__CUSTOM_FIELDS,
                            C__GET__TREE_NODE     => C__MODULE__CUSTOM_FIELDS . 1,
                            C__GET__ID            => $id
                        ])
                    ],
                ]);
            } catch (Exception $e) {
                $notify->error($e->getMessage(), ['sticky' => true]);

                return new JsonResponse([
                    'success' => false,
                    'message' => $e->getMessage(),
                    'data'    => null,
                ]);
            }
        }

        $notify->error("Specify your fields, please.");

        return new JsonResponse([
            'success' => false,
            'message' => 'Specify your fields, please.',
            'data'    => null,
        ]);
    }

    /**
     * @param Request $request
     * @return JsonResponse
     */
    public function findPropertyByKey(Request $request): JsonResponse
    {
        try {
            $id = (int)$request->query->get('id');
            $key = $request->query->get('key');

            $customFieldResult = isys_custom_fields_dao::instance($this->database)->get_data();

            while ($customFieldRow = $customFieldResult->get_row()) {
                if ($customFieldRow['isysgui_catg_custom__id'] == $id) {
                    // It should be OK to skip the current category itself, it will be validated when saving.
                    continue;
                }

                $properties = Unserialize::toArray($customFieldRow['isysgui_catg_custom__config']);

                if (isset($properties[$key])) {
                    return new JsonResponse([
                        'success' => true,
                        'message' => '',
                        'data'    => $properties[$key],
                    ]);
                }
            }

            return new JsonResponse([
                'success' => true,
                'message' => '',
                'data'    => null,
            ]);
        } catch (\Exception $e) {
            return new JsonResponse([
                'success' => false,
                'message' => $e->getMessage(),
                'data'    => null,
            ]);
        }
    }
}
