<?php

namespace idoit\Module\System\Model;

use idoit\Component\Property\Property;
use idoit\Model\Dao\Base;
use idoit\Module\Multiedit\Component\Filter\CategoryFilter;
use idoit\Module\Multiedit\Model\CustomCategories;
use idoit\Module\Multiedit\Model\GlobalCategories;
use idoit\Module\Multiedit\Model\SpecificCategories;
use isys_application;
use isys_cmdb_dao_category;
use isys_cmdb_dao_category_g_custom_fields;
use isys_format_json;
use isys_module_cmdb;
use isys_tenantsettings;

class AttributeVisibility extends Base
{
    const SETTINGS_KEY_PATTERN = 'cmdb.attribute-visibility.%s';

    const MANDATORY_FIELDS = [
        'C__CATG__GLOBAL::title',
        'C__CATG__GLOBAL::status',
        'C__CATG__GLOBAL::cmdb_status'
    ];

    const BLOCKED_FIELDS = [
        'C__CATG__GLOBAL::created_by',
        'C__CATG__GLOBAL::changed_by'
    ];

    const CONTACT_CATEGORIES = [
        'C__CATS__PERSON_MASTER',
        'C__CATS__PERSON_GROUP_MASTER',
        'C__CATS__ORGANIZATION_MASTER_DATA',
        'C__CATS__PERSON',
        'C__CATS__PERSON_GROUP',
        'C__CATS__ORGANIZATION',
    ];

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

    /**
     * @return array
     */
    private function supportedCategoryTypes()
    {
        return [
            isys_cmdb_dao_category::TYPE_EDIT,
            isys_cmdb_dao_category::TYPE_FOLDER,
            isys_cmdb_dao_category::TYPE_ASSIGN,
            isys_cmdb_dao_category::TYPE_REAR
        ];
    }

    /**
     * @param string $categoryConstant
     * @param string $propertyKey
     * @param string $id
     * @param string $uId
     * @param string $title
     * @param bool   $isRequired
     * @param bool   $isHidden
     * @param bool   $hideInOverview
     * @param bool   $isNotHideable
     *
     * @return array
     */
    private function formatProperty(string $categoryConstant, string $propertyKey, string $id, string $uId, string $title, bool $isRequired, bool $isHidden, bool $hideInOverview, bool $isNotHideable): array
    {
        return [
            'categoryConstant' => $categoryConstant,
            'propertyKey' => $propertyKey,
            'id' => $id,
            'uId' => $uId,
            'title' => $title,
            'isRequired' => $isRequired,
            'isHidden' => $isHidden,
            'hideInOverview' => $hideInOverview,
            'isNotHideable' => $isNotHideable,
        ];
    }

    /**
     * @param string $propertyKey
     *
     * @return bool
     */
    private function isNotHideable(string $propertyKey): bool
    {
        return in_array($propertyKey, self::MANDATORY_FIELDS);
    }

    /**
     * @param Property $propertyWithValidation
     * @param Property $propertyWithoutValidation
     *
     * @return bool
     */
    private function isRequired(Property $propertyWithValidation, Property $propertyWithoutValidation): bool
    {
        $isRequired = $propertyWithValidation->getCheck()->isMandatory();
        $isSystemRequired = $this->isSystemRequired($propertyWithoutValidation);

        if ($isRequired && !$isSystemRequired) {
            return true;
        }

        return false;
    }

    /**
     * @param Property $propertyWithoutValidation
     *
     * @return bool
     */
    private function isSystemRequired(Property $propertyWithoutValidation): bool
    {
        if ($propertyWithoutValidation->getCheck()) {
            return $propertyWithoutValidation->getCheck()->isMandatory() ?? false;
        }

        return false;
    }

    /**
     * @param string   $propertyKey
     * @param Property $property
     *
     * @return bool
     */
    private function isSkippable(string $propertyKey, Property $property): bool
    {
        [, $propKey] = explode('::', $propertyKey);

        if (in_array($propertyKey, self::BLOCKED_FIELDS) || strlen($property->getUi()->getId()) === 0) {
            return true;
        }

        return false;
    }

    /**
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return array
     */
    public function getProperties(isys_cmdb_dao_category $categoryDao): array
    {
        $propertiesWithValidation = $categoryDao->get_properties(C__PROPERTY__WITH__VALIDATION);
        $propertiesWithoutValidation = $categoryDao->get_properties();
        $categoryConstant = $categoryDao instanceof isys_cmdb_dao_category_g_custom_fields ?
            $categoryDao->get_catg_custom_const() : $categoryDao->get_category_const();
        $language = isys_application::instance()->container->get('language');
        $formattedProperties = [];
        $uiIdPrefix = '';

        if ($categoryDao instanceof isys_cmdb_dao_category_g_custom_fields) {
            $uiIdPrefix = 'C__CATG__CUSTOM__';
        }

        foreach ($propertiesWithValidation as $propKey => $propertyWithValidation) {
            $propertyWithoutValidation = $propertiesWithoutValidation[$propKey];
            if (is_array($propertyWithoutValidation) && !$propertyWithoutValidation instanceof Property) {
                $propertyWithoutValidation = Property::createInstanceFromArray($propertyWithoutValidation);
            }

            if (is_array($propertyWithValidation) && !$propertyWithValidation instanceof Property) {
                $propertyWithValidation = Property::createInstanceFromArray($propertyWithValidation);
            }

            $propertyKey = $categoryConstant . '::' . $propKey;
            $propertyTitle = $language->get($propertyWithValidation->getInfo()->getTitle());

            if ($this->isSkippable($propertyKey, $propertyWithValidation)) {
                continue;
            }

            // @see ID-9014 The description field of custom categories already contains the prefix.
            $uiId = ($uiIdPrefix && $propKey !== 'description' ? $uiIdPrefix : '') . $propertyWithValidation->getUi()->getId();

            $formattedProperties[] = $this->formatProperty(
                $categoryConstant,
                $propKey,
                $propertyWithValidation->getUi()->getId(),
                $uiId,
                $propertyTitle,
                $this->isRequired($propertyWithValidation, $propertyWithoutValidation),
                false,
                false,
                $this->isNotHideable($propertyKey)
            );
        }

        return $formattedProperties;
    }

    /**
     * @param string $constant
     *
     * @return null|isys_cmdb_dao_category
     * @throws \Exception
     */
    public function getCategoryInstanceByConstant(string $constant): ?isys_cmdb_dao_category
    {
        $dao = isys_application::instance()->container->get('cmdb_dao');
        $database = isys_application::instance()->container->get('database');
        $methods = [
            [
                'method' => 'get_catg_by_const',
                'classField' => 'isysgui_catg__class_name'
            ],
            [
                'method' => 'get_cats_by_const',
                'classField' => 'isysgui_cats__class_name'
            ],
        ];

        foreach ($methods as $method) {
            $methodName = $method['method'];
            $categoryInfoResult = $dao->$methodName($constant);
            if ($categoryInfoResult->num_rows() > 0) {
                $className = $categoryInfoResult->get_row_value($method['classField']);
                if (!class_exists($className)) {
                    return null;
                }

                return $className::instance($database);
            }
        }

        $categoryInfoResult = $dao->get_catc_by_const($constant);
        if ($categoryInfoResult->num_rows() > 0) {
            $categoryId = $categoryInfoResult->get_row_value('isysgui_catg_custom__id');
            $classInstance = isys_cmdb_dao_category_g_custom_fields::instance($database);
            $classInstance->set_catg_custom_id($categoryId)
                ->setCategoryInfo($categoryId);

            return $classInstance;
        }

        return null;
    }

    /**
     * @return array
     * @throws \isys_exception_database
     */
    public function getCategories(): array
    {
        $language = isys_application::instance()->container->get('language');
        $cmdbDao = isys_application::instance()->container->get('cmdb_dao');
        $database = isys_application::instance()->container->get('database');
        $globalLabel = $language->get('LC__CMDB__GLOBAL_CATEGORIES');
        $specificLabel = $language->get('LC__CMDB__SPECIFIC_CATEGORIES');
        $customLabel = $language->get('LC__CMDB__CUSTOM_CATEGORIES');
        $supportedCategoryTypes = implode(',', array_map([$cmdbDao, 'convert_sql_int'], $this->supportedCategoryTypes()));
        $specificData = $globalData = $customData = [];

        $changeKey = function ($key) {
            [$categoryIdParts, $class] = explode(':', $key);
            [, $categoryId] = explode('_', $categoryIdParts);

            /**
             * @var isys_cmdb_dao_category $dao;
             */
            $dao = $class::instance(isys_application::instance()->container->get('database'));
            if ($dao instanceof isys_cmdb_dao_category_g_custom_fields) {
                $dao->set_catg_custom_id($categoryId)
                    ->setCategoryInfo($categoryId);
                return $dao->get_catg_custom_const();
            }

            return $dao->get_category_const();
        };

        $filter = new CategoryFilter();
        $specificCategories = SpecificCategories::instance($database);
        $specificCategoryData = $specificCategories
            ->setFilter($filter)
            ->setData()
            ->getData();

        foreach ($specificCategoryData as $key => $data) {
            $specificData[$changeKey($key)] = $data;
        }

        $globalCategories = GlobalCategories::instance($database);
        $globalCategoryData = $globalCategories
            ->setFilter($filter)
            ->setData()
            ->getData();

        foreach ($globalCategoryData as $key => $data) {
            $globalData[$changeKey($key)] = $data;
        }

        $customCategies = CustomCategories::instance($database);
        $customCategoryData = $customCategies
            ->setFilter($filter)
            ->setData()
            ->getData();

        foreach ($customCategoryData as $key => $data) {
            $customData[$changeKey($key)] = $data;
        }

        asort($globalData);
        asort($specificData);
        asort($customData);

        $data = [
            $globalLabel => $globalData,
            $specificLabel => $specificData,
            $customLabel => $customData
        ];

        return $data;
    }

    /**
     * @return array
     * @throws \idoit\Exception\JsonException
     * @throws \isys_exception_database
     */
    public function getConfig(): array
    {
        $config = $configuredProperties = $unconfiguredProperties = [];
        $language = isys_application::instance()->container->get('language');
        $configuredConfig = isys_tenantsettings::getLike('cmdb.attribute-visibility.');

        if (empty($configuredConfig)) {
            return [];
        }

        foreach ($configuredConfig as $configKey => $configuration) {
            [,, $constant] = explode('.', $configKey);

            $categoryInstance = $this->getCategoryInstanceByConstant($constant);
            if (!$categoryInstance instanceof isys_cmdb_dao_category) {
                continue;
            }

            $configuredProperties = isys_format_json::decode($configuration);
            $unconfiguredProperties = $this->getProperties($categoryInstance);

            if (!is_array($configuredProperties) || !is_array($unconfiguredProperties)) {
                continue;
            }

            $newProperties = [];

            /**
             * @see ID-9183
             * Merge the configured and DAO properties with some logic to avoid mixups.
             * This can happen when a new property is introduced and the array keys change.
             */
            foreach ($unconfiguredProperties as $prop) {
                $currentProp = array_filter($configuredProperties, function ($configuredProp) use ($prop) {
                    return $configuredProp['categoryConstant'] === $prop['categoryConstant'] && $configuredProp['propertyKey'] === $prop['propertyKey'];
                });

                // Only merge, if the current property was found in the configured property array.
                if (count($currentProp)) {
                    $prop = array_merge($prop, ...$currentProp);
                }

                $newProperties[] = $prop;
            }

            $config[$constant] = [
                'title' => isys_glob_htmlentities($language->get($categoryInstance->getCategoryTitle())),
                'properties' => $newProperties
            ];
        }

        return $config;
    }

    /**
     * @param $categoryConstant
     * @param $data
     */
    public function saveConfig($categoryConstant, $data): void
    {
        isys_tenantsettings::set(sprintf(self::SETTINGS_KEY_PATTERN, $categoryConstant), isys_format_json::encode($data));
    }

    /**
     * @param array $categoryConstants
     */
    public function removeConfig(array $categoryConstants): void
    {
        array_map(function ($item) {
            isys_tenantsettings::remove(sprintf(self::SETTINGS_KEY_PATTERN, $item));
        }, $categoryConstants);
    }

    /**
     * @param array $removeHiddenFields
     *
     * @throws \idoit\Exception\JsonException
     * @throws \isys_exception_database
     */
    public function unsetHiddenFields(array $removeHiddenFields): void
    {
        $attributeConfig = $this->getConfig();

        foreach ($attributeConfig as $categoryConstant => $data) {
            if (!isset($removeHiddenFields[$categoryConstant])) {
                continue;
            }

            foreach ($data['properties'] as $index => $configuredProperty) {
                if (in_array($configuredProperty['propertyKey'], $removeHiddenFields[$categoryConstant])) {
                    $data['properties'][$index]['isRequired'] = true;
                    $data['properties'][$index]['isHidden'] = false;
                    $data['properties'][$index]['hideInOverview'] = false;
                    continue;
                }
            }

            $this->saveConfig($categoryConstant, $data['properties']);
        }
    }

    /**
     * @param array $removeRequiredFields
     */
    public function unsetMandatoryFields(array $removeRequiredFields): void
    {
        $daoInstances = [];
        $queryPattern = 'SELECT
            isys_validation_config__id as id,
            isys_validation_config__json as jsonConfig
            FROM isys_validation_config WHERE %s';
        $updatePattern = 'UPDATE isys_validation_config SET
            isys_validation_config__json = %s
            WHERE isys_validation_config__id = %s';

        foreach ($removeRequiredFields as $categoryConstant => $data) {
            if (!isset($daoInstances[$categoryConstant])) {
                $daoInstances[$categoryConstant] = $this->getCategoryInstanceByConstant($categoryConstant);
            }
            /**
             * @var isys_cmdb_dao_category $categoryDao
             */
            $categoryDao = $daoInstances[$categoryConstant];
            $condition = [
                'isys_validation_config__category_class = ' . $this->convert_sql_text(get_class($categoryDao))
            ];
            $categoryType = $categoryDao->get_category_type();
            $conditionCategoryIdPattern = '%s = ' . $this->convert_sql_id($categoryDao->get_category_id());
            switch ($categoryType) {
                case C__CMDB__CATEGORY__TYPE_SPECIFIC:
                    $field = 'isys_validation_config__isysgui_cats__id';
                    break;
                case C__CMDB__CATEGORY__TYPE_GLOBAL:
                    $field = 'isys_validation_config__isysgui_catg__id';
                    break;
                default:
                    return;
            }

            if ($categoryDao instanceof isys_cmdb_dao_category_g_custom_fields) {
                $field = 'isys_validation_config__isysgui_catg_custom__id';
                $conditionCategoryIdPattern = '%s = ' . $this->convert_sql_id($categoryDao->get_catg_custom_id());
            }

            $condition[] = sprintf($conditionCategoryIdPattern, $field);
            $query = sprintf($queryPattern, implode(' AND ', $condition));

            $validationConfigDataset = $this->retrieve($query)->get_row();
            $validationConfigId = $validationConfigDataset['id'];
            $validationConfig = isys_format_json::decode($validationConfigDataset['jsonConfig'], true);

            foreach ($data as $property) {
                if (isset($validationConfig[$property['propertyKey']])) {
                    $validationConfig[$property['propertyKey']]['check']['mandatory'] = false;
                }
            }
            $validationConfig = isys_format_json::encode($validationConfig);
            $update = sprintf($updatePattern, $this->convert_sql_text($validationConfig), $this->convert_sql_id($validationConfigId));
            $this->update($update);
        }
        $this->apply_update();

        isys_module_cmdb::create_validation_cache();
    }

    /**
     * @param string $constant
     * @param array  $setting
     *
     * @return array
     */
    private static function getContactDescriptions(string $constant, array $setting): array
    {
        $personFn = function ($setting) {
            $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON', '');
            if ($setting['id'] === 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON', '')) {
                $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON_MASTER', '');
            }

            $setting['uId'] = $setting['id'] = $id;

            return $setting;
        };

        $personGroupFn = function ($setting) {
            $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON_GROUP', '');
            if ($setting['id'] === 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON_GROUP', '')) {
                $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__PERSON_GROUP_MASTER', '');
            }

            $setting['uId'] = $setting['id'] = $id;

            return $setting;
        };

        $organisationFn = function ($setting) {
            $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__ORGANIZATION', '');
            if ($setting['id'] === 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__ORGANIZATION', '')) {
                $id = 'C__CMDB__CAT__COMMENTARY_' . C__CMDB__CATEGORY__TYPE_SPECIFIC . defined_or_default('C__CATS__ORGANIZATION_MASTER_DATA', '');
            }

            $setting['uId'] = $setting['id'] = $id;

            return $setting;
        };

        $categoryConstantFnMap = [
            'C__CATS__PERSON' => $personFn,
            'C__CATS__PERSON_MASTER' => $personFn,
            'C__CATS__ORGANIZATION' => $organisationFn,
            'C__CATS__ORGANIZATION_MASTER_DATA' => $organisationFn,
            'C__CATS__PERSON_GROUP' => $personGroupFn,
            'C__CATS__PERSON_GROUP_MASTER' => $personGroupFn,
        ];

        return $categoryConstantFnMap[$constant]($setting);
    }

    /**
     * @param string|null $categoryConstant
     *
     * @return array
     * @throws \idoit\Exception\JsonException
     */
    public static function getAttributeVisibilityForCategories(?string $categoryConstant = null): array
    {
        $filterSettingsCondition = $filterConfigCondition = fn ($item) => true;

        if ($categoryConstant !== null) {
            $filterConfigCondition = fn ($item) => $item['isHidden'] === true;

            $filterSettingsCondition = fn ($key) => strpos($key, '.' . $categoryConstant) !== false;
        }

        if ($categoryConstant === 'C__CATG__OVERVIEW') {
            $filterConfigCondition = fn ($item) => $item['isHidden'] === true || $item['hideInOverview'] === true;

            $filterSettingsCondition = fn ($key) => true;
        }

        $displaySettings = isys_tenantsettings::getLike(sprintf(AttributeVisibility::SETTINGS_KEY_PATTERN, ''));
        $displaySettings = array_filter(array_map(function ($item) use ($filterConfigCondition) {
            if (empty($item)) {
                return null;
            }
            $config = isys_format_json::decode($item);

            if (!is_array($config)) {
                return null;
            }

            return array_filter($config, $filterConfigCondition);
        }, $displaySettings), $filterSettingsCondition, ARRAY_FILTER_USE_KEY);

        $settings = [];
        if (!empty($displaySettings)) {
            foreach ($displaySettings as $data) {
                if (is_array($data)) {
                    $settings = array_merge($settings, $data);
                }
            }
        }

        if (!empty($settings)) {
            $alteredSettings = $settings;
            foreach ($settings as $setting) {
                if ($setting['propertyKey'] === 'description'
                    && in_array($setting['categoryConstant'], self::CONTACT_CATEGORIES)
                ) {
                    $alteredSettings[] = self::getContactDescriptions($setting['categoryConstant'], $setting);
                }
            }
            $settings = $alteredSettings;
        }

        return $settings;
    }
}
