<?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\SyneticsFlows\Model\Dto\Attribute;
use idoit\Module\SyneticsFlows\Model\Dto\DependantChildAttribute;
use idoit\Module\SyneticsFlows\Model\Dto\ParentAttribute;
use idoit\Module\SyneticsFlows\Model\Dto\ParentChildAttribute;
use idoit\Module\SyneticsFlows\Model\Dto\RequiredAttribute;
use idoit\Module\SyneticsFlows\Blocked;
use idoit\Module\SyneticsFlows\Controller\SearchParams;
use idoit\Module\SyneticsFlows\Filter\AttributeFilter;
use isys_application;
use isys_auth_cmdb_categories;
use isys_cmdb_dao_category;
use isys_cmdb_dao_category_g_custom_fields;

class AttributeDao extends Base
{
    public const PARENT_ATTRIBUTE = 'parent';
    public const CHILD_ATTRIBUTES = 'children';
    public const OBJECT_BROWSER_TYPE = 'object_browser';
    public const OBJECT_BROWSER_MULTI_TYPE = 'object_browser_multi';
    public const LOCATION_BROWSER_TYPE = 'location_browser';
    public const CONNECTOR_BROWSER_TYPE = 'connector_browser';
    public const FC_PORT_BROWSER_TYPE    = 'fc_port_broser';
    public const SAN_ZONING_BROWSER_TYPE = 'san_zoning_browser';
    public const FILE_BROWSER_TYPE = 'file_browser';
    public const RELATION_BROWSER_TYPE = 'relation_browser';
    public const SANPOOL_BROWSER_TYPE = 'sanpool_browser';

    public const AVAILABLE_ATTRIBUTE_TYPES = [
        Property::C__PROPERTY__INFO__TYPE__TEXT,
        Property::C__PROPERTY__INFO__TYPE__TEXTAREA,
        Property::C__PROPERTY__INFO__TYPE__DOUBLE,
        Property::C__PROPERTY__INFO__TYPE__FLOAT,
        Property::C__PROPERTY__INFO__TYPE__INT,
        Property::C__PROPERTY__INFO__TYPE__MONEY,
        Property::C__PROPERTY__INFO__TYPE__DIALOG,
        Property::C__PROPERTY__INFO__TYPE__DIALOG_PLUS,
        Property::C__PROPERTY__INFO__TYPE__DIALOG_LIST,
        Property::C__PROPERTY__INFO__TYPE__MULTISELECT,
        Property::C__PROPERTY__INFO__TYPE__DATE,
        Property::C__PROPERTY__INFO__TYPE__DATETIME,
        Property::C__PROPERTY__INFO__TYPE__COMMENTARY,
        Property::C__PROPERTY__INFO__TYPE__PASSWORD,
        self::OBJECT_BROWSER_TYPE,
        self::OBJECT_BROWSER_MULTI_TYPE,
        self::LOCATION_BROWSER_TYPE
    ];

    public const SORTING_MAP =  [
        'id' => 'id',
        'title' => 'title',
    ];

    public const ALWAYS_SUPPORTED_ATTRIBUTES = [
        'id',
        'description',
    ];

    /**
     * @param string                 $categoryConstant
     * @param array                  $data
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return ParentChildAttribute[]
     * @throws \ReflectionException
     *
     */
    public function buildParentChildAttributes(
        string $categoryConstant,
        array $data,
        isys_cmdb_dao_category $categoryDao
    ): array {
        $parentChildAttributes = [];
        $language = isys_application::instance()->container->get('language');

        foreach ($data as $dependantAttributes) {
            $parentPropertyData = $dependantAttributes[self::PARENT_ATTRIBUTE];
            $parentPropertyKey = key($parentPropertyData);
            $parentProperty = current($parentPropertyData);

            if ($parentProperty instanceof Property) {
                $parentProperty = $parentProperty->toArray();
            }

            if (($attributeType = $this->getAttributeType($parentPropertyKey, $parentProperty, $categoryDao)) ===
                null) {
                continue;
            }

            $children = $this->buildDependantChilds($parentPropertyKey, $categoryConstant,
                $dependantAttributes[self::CHILD_ATTRIBUTES], $categoryDao);

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

            $parentChildAttributes[] = new ParentChildAttribute(
                new ParentAttribute(
                    "{$categoryConstant}.{$parentPropertyKey}",
                    $language->get($parentProperty[C__PROPERTY__INFO][C__PROPERTY__INFO__TITLE]),
                    $attributeType,
                    (bool)$parentProperty[C__PROPERTY__CHECK][C__PROPERTY__CHECK__MANDATORY]
                ),
                $children
            );
        }

        return $parentChildAttributes;
    }

    /**
     * @param string                 $categoryConst
     * @param array                  $data
     * @param isys_cmdb_dao_category $daoInstance
     *
     * @return RequiredAttribute[]
     *
     * @throws \ReflectionException
     */
    public function buildRequiredAttributes(
        string $categoryConst,
        array $data,
        isys_cmdb_dao_category $daoInstance
    ): array {
        $requiredAttribues = [];
        $language = isys_application::instance()->container->get('language');

        foreach ($data as $propertyKey => $property) {
            if ($property instanceof Property) {
                $property = $property->toArray();
            }

            if (($attributeType = $this->getAttributeType($propertyKey, $property, $daoInstance)) === null) {
                continue;
            }

            $requiredAttribues[] = new RequiredAttribute(
                "{$categoryConst}.{$propertyKey}",
                $language->get($property[C__PROPERTY__INFO][C__PROPERTY__INFO__TITLE]),
                $attributeType,
                (bool)$property[C__PROPERTY__CHECK][C__PROPERTY__CHECK__MANDATORY]
            );
        }

        return $requiredAttribues;
    }

    /**
     * @param array                  $property
     * @param isys_cmdb_dao_category $daoInstance
     *
     * @return string
     */
    private function getPropertyType(array $property, isys_cmdb_dao_category $daoInstance): string
    {
        $type = $property[C__PROPERTY__INFO][C__PROPERTY__INFO__TYPE];
        $uiType = $property[C__PROPERTY__UI][C__PROPERTY__UI__TYPE] ?: '';
        $uiParams = $property[C__PROPERTY__UI][C__PROPERTY__UI__PARAMS] ?: [];

        if ($type === Property::C__PROPERTY__INFO__TYPE__TEXT && $uiType === C__PROPERTY__UI__TYPE__PASSWORD) {
            return Property::C__PROPERTY__INFO__TYPE__PASSWORD;
        }

        if ($type === Property::C__PROPERTY__INFO__TYPE__N2M) {
            return self::OBJECT_BROWSER_MULTI_TYPE;
        }

        if ($type === Property::C__PROPERTY__INFO__TYPE__OBJECT_BROWSER) {
            switch ($uiParams['p_strPopupType']) {
                case 'browser_object_ng':

                    if (isset($uiParams['multiselection']) && $uiParams['multiselection']) {
                        return self::OBJECT_BROWSER_MULTI_TYPE;
                    }

                    return self::OBJECT_BROWSER_TYPE;
                case 'browser_location':
                    return self::LOCATION_BROWSER_TYPE;

                case 'cable_connection_ng':
                    return self::CONNECTOR_BROWSER_TYPE;

                case 'fc_port':
                    return self::FC_PORT_BROWSER_TYPE;

                case 'fc_port_san_zoning':
                    return self::SAN_ZONING_BROWSER_TYPE;

                case 'file':
                    return self::FILE_BROWSER_TYPE;

                case 'object_relation':
                    return self::RELATION_BROWSER_TYPE;

                case 'sanpool':
                    return self::SANPOOL_BROWSER_TYPE;
            }
        }

        return (string)$type;
    }

    /**
     * @param string                 $propertyKey
     * @param array                  $property
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return string|null
     */
    public function getAttributeType(string $propertyKey, array $property, isys_cmdb_dao_category $categoryDao): ?string
    {
        $attributeType = $this->getPropertyType($property, $categoryDao);

        if (!in_array($attributeType, self::AVAILABLE_ATTRIBUTE_TYPES, true)) {
            // Skip properties that are not available.
            return null;
        }

        if (!self::isSupportable($property, $propertyKey)) {
            // Skip virtual properties.
            return null;
        }

        return $attributeType;
    }

    /**
     * @param array  $property
     * @param string $key
     *
     * @return bool
     */
    public static function isSupportable(array $property, string $key): bool
    {
        if (in_array($key, self::ALWAYS_SUPPORTED_ATTRIBUTES)) {
            return true;
        }

        // This check needs to be there because of the conversion of Property from object to array
        if (isset($property[C__PROPERTY__PROVIDES]['virtual']) && isset($property[C__PROPERTY__PROVIDES]['multiedit'])) {
            if ($property[C__PROPERTY__PROVIDES]['virtual']) {
                return false;
            }

            return true;
        }

        if ($property[C__PROPERTY__PROVIDES][C__PROPERTY__PROVIDES__VIRTUAL]) {
            return false;
        }

        return true;
    }

    /**
     * @param string                 $parentPropertyKey
     * @param string                 $categoryConstant
     * @param array                  $children
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return DependantChildAttribute[]
     * @throws \ReflectionException
     */
    private function buildDependantChilds(
        string $parentPropertyKey,
        string $categoryConstant,
        array $children,
        isys_cmdb_dao_category $categoryDao
    ) {
        $return = [];
        $language = isys_application::instance()->container->get('language');

        foreach ($children as $propKey => $property) {
            if ($property instanceof Property) {
                $property = $property->toArray();
            }

            if (($attributeType = $this->getAttributeType($propKey, $property, $categoryDao)) === null) {
                continue;
            }

            $return[] = new DependantChildAttribute(
                "{$categoryConstant}.{$propKey}",
                $language->get($property[C__PROPERTY__INFO][C__PROPERTY__INFO__TITLE]),
                $attributeType,
                (bool)$property[C__PROPERTY__CHECK][C__PROPERTY__CHECK__MANDATORY],
                "{$categoryConstant}.{$parentPropertyKey}"
            );
        }

        return $return;
    }

    /**
     * @param string                 $categoryConstant
     * @param string                 $propertyKey
     * @param array                  $parentChildProperties
     * @param isys_cmdb_dao_category $categoryDao
     * @param string                 $direction
     *
     * @return ParentChildAttribute|null
     * @throws \ReflectionException
     */
    private function getDependantAttribute(
        string $categoryConstant,
        string $propertyKey,
        array $parentChildProperties,
        isys_cmdb_dao_category $categoryDao,
        string $direction = self::CHILD_ATTRIBUTES
    ): ?ParentChildAttribute {
        $parentChildProperty = array_filter($parentChildProperties, function ($item) use ($propertyKey, $direction) {
            return isset($item[$direction][$propertyKey]);
        });

        if (!empty($parentChildProperty)) {
            return $this->buildParentChildAttributes($categoryConstant, $parentChildProperty, $categoryDao)[0] ?? null;
        }

        return null;
    }

    /**
     * @param AttributeFilter|null $attributeFilter
     * @param SearchParams|null    $params
     *
     * @return array
     * @throws \ReflectionException
     */
    public function getData(?AttributeFilter $attributeFilter = null, ?SearchParams $params = null): array
    {
        $database = isys_application::instance()->container->get('database');
        $language = isys_application::instance()->container->get('language');
        $categoryDao = new CategoryDao($database);
        $categories = $categoryDao->getData($attributeFilter?->getCategoryFilter());
        $attribute = $attributeFilter?->getAttribute() ?? null;
        $attributes = [];

        foreach ($categories as $category) {
            $daoName = $category->getClassName();

            if (!defined($category->getId())) {
                continue;
            }

            if (!class_exists($daoName) || in_array($category->getId(), ArrayHelper::concat(Blocked::GLOBAL_CATEGORIES, Blocked::SPECIFIC_CATEGORIES), true)) {
                // Skip blocked categories.
                continue;
            }

            /** @var isys_cmdb_dao_category $daoInstance */
            $daoInstance = $daoName::instance($database);
            $categoryId = constant($category->getId());

            if ($daoInstance instanceof isys_cmdb_dao_category_g_custom_fields) {
                $daoInstance->set_catg_custom_id($categoryId);
            }

            $properties = $daoInstance->get_properties(C__PROPERTY__WITH__VALIDATION);
            $parentChildProperties = $categoryDao->getParentChildProperties($daoInstance);

            foreach ($properties as $key => $property) {
                $parent = null;
                $children = [];

                if (in_array($category->getId() . '.' . $key, Blocked::ATTRIBUTES, true)) {
                    // Skip blocked properties.
                    continue;
                }

                if ($attribute !== null && $attribute !== $key) {
                    continue;
                }

                if ($attributeFilter?->isExcludeReadOnly()
                    && $property[C__PROPERTY__DATA][C__PROPERTY__DATA__READONLY]
                ) {
                    continue;
                }

                if ($property instanceof Property) {
                    $property = $property->toArray();
                }

                if (($attributeType = $this->getAttributeType($key, $property, $daoInstance)) === null) {
                    continue;
                }

                $childAttributes = $this->getDependantAttribute($category->getId(), $key, $parentChildProperties, $daoInstance, self::PARENT_ATTRIBUTE);
                if ($childAttributes instanceof ParentChildAttribute) {
                    $children = $childAttributes->getChildren();
                }

                $parentAttribute = $this->getDependantAttribute($category->getId(), $key, $parentChildProperties, $daoInstance, self::CHILD_ATTRIBUTES);
                if ($parentAttribute instanceof ParentChildAttribute) {
                    $parent = $parentAttribute->getParentAttribute();
                }

                $attributes[] = [
                    'id'    => "{$category->getId()}.{$key}",
                    'categoryId' => $category->getId(),
                    'title' => $language->get($property[C__PROPERTY__INFO][C__PROPERTY__INFO__TITLE]),
                    'type'  => $attributeType,
                    'isSystemRequired' => (bool) $property[C__PROPERTY__CHECK][C__PROPERTY__CHECK__MANDATORY],
                    self::PARENT_ATTRIBUTE => $parent,
                    self::CHILD_ATTRIBUTES => $children
                ];
            }
        }

        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($attributes, self::SORTING_MAP[$sortId], $direction);
            }

            $attributes = array_slice($attributes, $params->getOffset(), $params->getPerPage());
        }

        return array_map(fn (array $entry) => new Attribute(
            $entry['id'],
            $entry['categoryId'],
            $entry['title'],
            $entry['type'],
            $entry['isSystemRequired'],
            $entry[self::PARENT_ATTRIBUTE],
            $entry[self::CHILD_ATTRIBUTES]
        ), $attributes);
    }

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

    /**
     * @param string $id
     *
     * @return Attribute|null
     */
    public function get(string $id): ?Attribute
    {
        $attributeFilter = AttributeFilter::factory($id);
        return $this->getData($attributeFilter)[0] ?? null;
    }
}
