<?php declare(strict_types = 1);

namespace idoit\Module\SyneticsFlows\Model;

use idoit\Component\Helper\ArrayHelper;
use idoit\Component\Property\Property;
use idoit\Model\Dao\Base;
use idoit\Module\Multiedit\Component\Filter\CategoryFilter;
use idoit\Module\Multiedit\Component\Multiedit\Exception\CategoryDataException;
use idoit\Module\Multiedit\Model\Categories;
use idoit\Module\Multiedit\Model\CustomCategories;
use idoit\Module\Multiedit\Model\GlobalCategories;
use idoit\Module\Multiedit\Model\SpecificCategories;
use idoit\Module\SyneticsFlows\Blocked;
use idoit\Module\SyneticsFlows\Controller\SearchParams;
use idoit\Module\SyneticsFlows\Model\Dto\Category;
use isys_application;
use isys_auth;
use isys_auth_cmdb_categories;
use isys_cmdb_dao_category;
use isys_cmdb_dao_category_g_custom_fields;

class CategoryDao extends Base
{
    public const SORTING_MAP =  [
        'id' => 'id',
        'title' => 'title',
    ];

    /**
     * @param CategoryFilter|null $categoryFilter
     * @param SearchParams|null $params
     *
     * @return Category[]
     *
     * @throws CategoryDataException
     * @throws \isys_exception_database
     */
    public function getData(?CategoryFilter $categoryFilter = null, ?SearchParams $params = null): array
    {
        $language = isys_application::instance()->container->get('language');
        $database = \isys_application::instance()->container->get('database');

        if ($categoryFilter === null) {
            $categoryFilter = new CategoryFilter();
        }

        $categoryAuth = isys_auth_cmdb_categories::instance();
        $allowedCategories = $categoryAuth->get_allowed_categories();

        if (is_array($allowedCategories)) {
            $allowedCategories = ArrayHelper::concat($allowedCategories, $categoryFilter->getCategories() ?? []);

            // Have to check it separately because it is a default in isys_auth_cmdb_categories
            if (!$categoryAuth->is_allowed_to(\isys_auth::VIEW, 'CATEGORY/C__CATG__GLOBAL')) {
                $allowedCategories = array_filter($allowedCategories, fn($item) => $item !== 'C__CATG__GLOBAL');
            }

            $categoryFilter->setCategories($allowedCategories);
        }

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

        // All categories are allowed
        if ($allowedCategories === true) {
            $allowedCategories = ['*'];
        }

        $globalCategories = GlobalCategories::instance($database)
            ->setBlacklist(Blocked::getBlockedCategoriesAsIds(Blocked::GLOBAL_CATEGORIES))
            ->setFilter($categoryFilter)
            ->setData();
        $globalCategories = $this->extractCategories($globalCategories);

        $specificCategories = SpecificCategories::instance($database)
            ->setBlacklist(Blocked::getBlockedCategoriesAsIds(Blocked::SPECIFIC_CATEGORIES))
            ->setFilter($categoryFilter)
            ->setData();
        $specificCategories = $this->extractCategories($specificCategories);

        $customCategories = CustomCategories::instance($database)
            ->setFilter($categoryFilter)
            ->setData();
        $customCategories = $this->extractCategories($customCategories);

        $allCategories = array_merge($globalCategories, $specificCategories, $customCategories);

        if ($params) {
            $sortId = $params->getSort()?->getId() ?? null;
            if ($sortId && isset(self::SORTING_MAP[$sortId])) {
                $direction = $params->getSort()?->isDesc() ? SORT_DESC : SORT_ASC;
                isys_glob_sort_array_by_column($allCategories, $sortId, $direction);
            }
            $allCategories = array_slice($allCategories, $params->getOffset(), $params->getPerPage());
        }

        return array_map(fn (array $entry) => new Category(
            $entry['id'],
            $entry['title'],
            $entry['multivalue'],
            $entry['className'],
            $entry['requiredAttributes'],
            $entry['parentChildAttributes']
        ), $allCategories);
    }

    /**
     * @param string $id
     *
     * @return Category|null
     *
     * @throws CategoryDataException
     * @throws \isys_exception_database
     */
    public function get(string $id): ?Category
    {
        $categoryFilter = new CategoryFilter();
        $categoryFilter->setCategories([$id]);
        $found = $this->getData($categoryFilter);
        $entry = current(array_filter($found, fn (Category $category) => $category->getId() === $id));
        if ($entry) {
            return $entry;
        }
        return null;
    }

    public function getDaoInstance(string $categoryConst): ?isys_cmdb_dao_category
    {
        if (!isys_auth_cmdb_categories::instance()->is_allowed_to(isys_auth::VIEW, "CATEGORY/{$categoryConst}")) {
            return null;
        }

        $categoryObject = $this->get($categoryConst);
        $className = $categoryObject?->getClassName();
        $categoryConstant = $categoryObject?->getId();

        if (!$className || !class_exists($className) || !defined($categoryConstant)) {
            return null;
        }

        /** @var isys_cmdb_dao_category $categoryInstance */
        $categoryInstance = $className::instance($this->get_database_component());

        if ($categoryInstance instanceof isys_cmdb_dao_category_g_custom_fields) {
            $categoryInstance->set_catg_custom_id(constant($categoryConstant));
        }
        return $categoryInstance;
    }


    /**
     * @param CategoryFilter|null $categoryFilter
     *
     * @return int
     */
    public function getCount(?CategoryFilter $categoryFilter = null): int
    {
        return count($this->getData($categoryFilter));
    }

    /**
     * @param Categories $categories
     *
     * @return array
     * @throws \isys_exception_database
     */
    private function extractCategories(Categories $categories): array
    {
        $return = [];

        if ($categories->getData()) {
            $multiValueList = $categories->getMultivalueCategories();
            $database = isys_application::instance()->container->get('database');
            $language = isys_application::instance()->container->get('language');
            $attributeDao = new AttributeDao($database);

            foreach ($categories->getData() as $categoryCombinedKey => $categoryTitle) {
                [$categoryKey, $categoryClass] = explode(':', $categoryCombinedKey);
                [$categoryType, $categoryId] = explode('_', $categoryKey);

                if (!class_exists($categoryClass)) {
                    continue;
                }

                $categoryObject = $categoryClass::instance($database);

                if (!$categoryObject instanceof isys_cmdb_dao_category) {
                    continue;
                }

                $isMultiValue = (bool)$multiValueList[$categoryKey];
                $const = $categoryObject->get_category_const();

                if ($categoryObject instanceof isys_cmdb_dao_category_g_custom_fields) {
                    $categoryObject->set_catg_custom_id((int)$categoryId)
                        ->setCategoryInfo($categoryId);
                    $const = $categoryObject->get_catg_custom_const();
                    $categoryTitle = $categoryObject->getCategoryTitle();
                }

                if (!defined($const)) {
                    continue;
                }

                if ($this->hasProperties($const)) {

                    $parentChildAttributes = $this->getParentChildProperties($categoryObject);
                    if (!empty($parentChildAttributes)) {
                        $parentChildAttributes = $attributeDao->buildParentChildAttributes($const, $parentChildAttributes, $categoryObject);
                    }

                    $requiredAttributes = $this->getMandatoryProperties($categoryObject);
                    if (!empty($requiredAttributes)) {
                        $requiredAttributes = $attributeDao->buildRequiredAttributes($const, $requiredAttributes, $categoryObject);
                    }

                    $return[] = [
                        'id'                    => $const,
                        'numeric_id'            => constant($const),
                        'title'                 => $language->get($categoryTitle),
                        'multivalue'            => $isMultiValue,
                        'className'             => $categoryClass,
                        'requiredAttributes'    => $requiredAttributes,
                        'parentChildAttributes' => $parentChildAttributes
                    ];
                }
            }
        }

        return $return;
    }

    /**
     * @param isys_cmdb_dao_category $dao
     *
     * @return array
     */
    private function getMandatoryProperties(isys_cmdb_dao_category $dao): array
    {
        $properties = $dao->get_properties(C__PROPERTY__WITH__VALIDATION);
        $mandatoryProperties = array_filter($properties, function ($item) {
            return $item[Property::C__PROPERTY__CHECK][Property::C__PROPERTY__CHECK__MANDATORY];
        });

        return $mandatoryProperties;
    }

    /**
     * @param isys_cmdb_dao_category $dao
     *
     * @return array
     */
    public function getParentChildProperties(isys_cmdb_dao_category $dao): array
    {
        $properties = $dao->get_properties();
        $result = [];
        foreach ($properties as $firstKey => $property) {
            $children = [];
            foreach ($properties as $secondKey => $secondProperty) {
                $dependantPropKey = $secondProperty[Property::C__PROPERTY__DEPENDENCY][Property::C__PROPERTY__DEPENDENCY__PROPKEY]
                    ?? null;
                if ($dependantPropKey !== null && $firstKey === $dependantPropKey) {
                    $children[$secondKey] = $secondProperty;
                    continue;
                }
            }

            if (!empty($children)) {
                $result[] = [
                    'parent'   => [
                        $firstKey => $property
                    ],
                    'children' => $children
                ];
            }
        }

        return $result;
    }

    /**
     * Skip categories which have no attributes which are supported
     *
     * @param $categoryConstant
     *
     * @return bool
     * @throws \isys_exception_database
     */
    private function hasProperties($categoryConstant): bool
    {
        $checkSql = '
            SELECT isys_property_2_cat__prop_key FROM isys_property_2_cat
                WHERE isys_property_2_cat__cat_const = ' . $this->convert_sql_text($categoryConstant) . '
                    AND isys_property_2_cat__prop_type = ' . $this->convert_sql_int(C__PROPERTY_TYPE__STATIC) . '
                    AND isys_property_2_cat__prop_provides & ' . $this->convert_sql_int(C__PROPERTY__PROVIDES__MULTIEDIT) . '
            UNION
            SELECT isys_property_2_cat__prop_key FROM isys_property_2_cat
                WHERE isys_property_2_cat__cat_const =  ' . $this->convert_sql_text($categoryConstant) . '
                    AND isys_property_2_cat__prop_key = ' . $this->convert_sql_text('description');

        $checkResult = $this->retrieve($checkSql);

        if (count($checkResult) === 0) {
            return false;
        }

        return true;
    }

    /**
     * @return CategoryFilter
     */
    public static function getCategoryFilter(?CategoryFilter $categoryFilter = null): CategoryFilter
    {
        if ($categoryFilter === null) {
            $categoryFilter = new CategoryFilter();
        }

        $allowedCategories = isys_auth_cmdb_categories::instance()->get_allowed_categories();

        if (is_array($allowedCategories)) {
            $categories = $categoryFilter->getCategories();
            $categoryFilter->setCategories(ArrayHelper::concat($allowedCategories, $categories));
        }
        return $categoryFilter;
    }
}
