<?php

namespace Shm\ShmTypes;

use Shm\ShmDB\mDB;


use Shm\Shm;
use Shm\ShmAddons\AdminActions\AdminAction;
use Shm\ShmRPC\ShmRPCCodeGen\TSType;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPostDeletePayload;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPostInsertPayload;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPostUpdatePayload;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPreDeletePayload;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPreInsertPayload;
use Shm\ShmTypes\Classes\EventClasses\ShmEventPreUpdatePayload;
use Shm\ShmTypes\Classes\ShmEventPayload;

use Shm\ShmTypes\CompositeTypes\BalanceTypes\BalanceType;
use Shm\ShmTypes\CompositeTypes\BalanceTypes\BalanceUtils;
use Shm\ShmTypes\Utils\JsonLogicBuilder;

use Shm\ShmUtils\AutoPostfix;
use Shm\ShmUtils\DeepAccess;
use Shm\ShmUtils\Inflect;
use Shm\ShmUtils\ProcessLogs;
use Shm\ShmUtils\Response;
use Shm\ShmUtils\ShmInit;
use Shm\ShmUtils\ShmUtils;
use stdClass;
use Traversable;

/**
 * Structure type for schema definitions
 * 
 * This class represents a structure type that can contain multiple fields
 * and provides complex validation and normalization capabilities.
 */
class StructureType extends BaseType
{
    public string $type = 'structure';
    public ?string $collection = null;
    private array $pipeline = [];
    private array $insertValues = [];

    public ?int $redisTTL = 300;

    public function setRedisTTL(int | null $ttl): static
    {
        $this->redisTTL = $ttl;
        return $this;
    }



    public bool $apikey = false;


    public array $locGroupsTitle = [];



    private array $actions = [];

    public function addAction(AdminAction $action): static
    {
        $this->actions[] = $action;
        return $this;
    }

    public function getActionByKey(string $key): ?AdminAction
    {
        foreach ($this->actions as $action) {
            if ($action->key === $key) {
                return $action;
            }
        }
        return null;
    }

    public function getActions(): array
    {
        return $this->actions;
    }




    public bool $manualSort = false;





    /**
     * Set API key flag
     * 
     * @param bool $apikey Whether this structure is an API key
     * @return static
     */
    public function apikey(bool $apikey = true): static
    {
        $this->apikey = $apikey;
        return $this;
    }

    /**
     * Set manual sort flag
     * 
     * @param bool $manualSort Whether manual sorting is enabled
     * @return static
     */
    public function manualSort(bool $manualSort = true): static
    {
        $this->manualSort = $manualSort;
        return $this;
    }


    public function translitKeys(): static
    {
        $newItems = [];

        foreach ($this->items as $key => $item) {
            $translitKey = ShmUtils::translitIfCyrillic($key);
            $item->key($translitKey);
            $newItems[$translitKey] = $item;
        }

        $this->items = $newItems;
        return $this;
    }






    public function update(array $update): static
    {


        foreach ($update as $key => $value) {
            $key = ShmUtils::cleanKey($key);
            $this->items[$key] = $value;
        }

        $this->rebuild($this->items);


        return $this;
    }


    public function hideFields(array $hideFields): static
    {
        foreach ($hideFields as $key) {
            if (isset($this->items[$key])) {
                $this->items[$key]->hide = true;
            }
        }

        return $this;
    }

    public function removeField(string $key): static
    {
        if (isset($this->items[$key])) {
            unset($this->items[$key]);
        }
        return $this;
    }

    public function addFieldIfNotExists(string $key, BaseType $type): static
    {

        $key = ShmUtils::cleanKey($key);

        if (!isset($this->items[$key])) {
            $this->items[$key] = $type->key($key);
        } else {
        }

        return $this;
    }

    public function addField(string $key, BaseType $type): static
    {

        $key = ShmUtils::cleanKey($key);

        $this->items[$key] = $type->key($key);


        return $this;
    }

    private $canDeleteHandler = null;

    public function getCanDelete(array $ids): ?bool
    {
        if ($this->canDeleteHandler) {
            return call_user_func($this->canDeleteHandler, $ids);
        }
        return false;
    }

    public function canDeleteHasHandler(): bool
    {
        return $this->canDeleteHandler !== null;
    }


    /**
     * @param bool | callable $canDelete
     * @return static
     * 
     * @example
     * $structure->canDelete(function (array $ids) {
     *     return true;
     * });
     * $structure->canDelete(true);
     */
    public function canDelete(bool | callable $canDelete = true): static
    {
        if (is_callable($canDelete)) {
            $this->canDeleteHandler = $canDelete;
            return $this;
        }

        $this->canDeleteHandler = function () use ($canDelete) {
            return $canDelete;
        };

        return $this;
    }




    private $canUpdateHandler = null;

    public function getCanUpdate(array $ids): ?bool
    {
        if ($this->canUpdateHandler) {
            return call_user_func($this->canUpdateHandler, $ids);
        }
        return false;
    }


    public function canUpdateHasHandler(): bool
    {
        return $this->canUpdateHandler !== null;
    }

    /**
     * @param bool | callable $canUpdate
     * @return static
     * 
     * @example
     * $structure->canUpdate(function (array $ids) {
     *     return true;
     * });
     * $structure->canUpdate(true);
     */
    public function canUpdate(bool | callable $canUpdate = true): static
    {

        if (is_callable($canUpdate)) {
            $this->canUpdateHandler = $canUpdate;
            return $this;
        }

        $this->canUpdateHandler = function () use ($canUpdate) {
            return $canUpdate;
        };


        return $this;
    }

    private $canCreateHandler = null;

    public function getCanCreate(): ?bool
    {
        if ($this->canCreateHandler) {
            return call_user_func($this->canCreateHandler);
        }
        return false;
    }

    public function canCreateRemoveHandler(): static
    {
        $this->canCreateHandler = null;
        return $this;
    }

    public function canUpdateRemoveHandler(): static
    {
        $this->canUpdateHandler = null;
        return $this;
    }

    public function canDeleteRemoveHandler(): static
    {
        $this->canDeleteHandler = null;
        return $this;
    }

    public function canCreateHasHandler(): bool
    {
        return $this->canCreateHandler !== null;
    }

    public function canCreate(bool | callable $canCreate = true): static
    {

        if (is_callable($canCreate)) {
            $this->canCreateHandler = $canCreate;
            return $this;
        }

        $this->canCreateHandler = function () use ($canCreate) {
            return $canCreate;
        };

        return $this;
    }

    public array $filterPresets = [];


    public array $boardFilterPresets = [];

    public function addBoardFilterPreset(string $key, string $title, array $filter): static
    {
        $this->boardFilterPresets[$key] = [
            'key' => $key,
            'title' => $title,
            'filter' => $filter,
        ];
        return $this;
    }



    public function addFilterPreset(string $key, string $title, array $filter): static
    {

        $filter = mDB::replaceObjectIdsToString($filter);

        $this->filterPresets[$key] = [
            'key' => $key,
            'title' => $title,
            'filter' => $filter,
        ];
        return $this;
    }












    public function insertValues(array $insertValues): static
    {
        $this->insertValues = $insertValues;
        return $this;
    }


    public function getInsertValues(): array
    {
        return $this->insertValues;
    }



    public function addPipeline(array $pipeline): static
    {

        if (count($pipeline) > 0) {
            mDB::validatePipeline($pipeline);
        }

        $this->pipeline = [...$this->pipeline, ...$pipeline];



        return $this;
    }

    public function pipeline(array $pipeline): static
    {

        if (count($pipeline) > 0) {
            mDB::validatePipeline($pipeline);
        }

        $this->pipeline = $pipeline;

        return $this;
    }

    public function getPipeline(): array
    {
        mDB::validatePipeline($this->pipeline);


        return $this->pipeline;
    }

    public function collection(string $collection): static
    {

        $this->collection = $collection;
        return $this;
    }


    //Переменная в которой будет хранится кастомная функция для обработки и создания display значения
    public $displayFunc = null;


    //Устанавливаем кастомную функцию для обработки и создания display значения
    //Функция на вход принимает объект с данными и должна вернуть строку
    /**
     * @param callable $function Функция на вход принимает объект с данными и должна вернуть строку
     * @return static
     * @example
     * $structure->setDisplayFunc(function ($values) {
     *     return $values['name'] . ' ' . $values['surname'];
     * });
     */
    public function setDisplayFunc(callable $function): static
    {
        $this->displayFunc = $function;
        return $this;
    }


    public function displayValues($values): array | string | null
    {

        if ($this->displayFunc) {
            return call_user_func($this->displayFunc, $values);
        }


        $displayValues = [];
        foreach ($this->items as $key => $item) {

            if (isset($values[$key]) && $item->display) {



                $val = $item->displayValues($values[$key]);



                if ($val) {

                    if (is_array($val)) {
                        $displayValues = [
                            ...$displayValues,
                            ...$val
                        ];
                    } else {
                        $displayValues[] = $val;
                    }
                }
            }
        }

        return $displayValues;
    }


    public function fallbackDisplayValues($values): array | string | null
    {



        $displayValues = [];
        foreach ($this->items as $key => $item) {



            if (isset($values[$key])) {


                $val = $item->fallbackDisplayValues($values[$key]);


                if ($val) {

                    if (is_array($val)) {
                        $displayValues = [
                            ...$displayValues,
                            ...$val
                        ];
                    } else {
                        $displayValues[] = $val;
                    }
                }
            }
        }

        return $displayValues;
    }



    private function formatHeatmap(array $mongoResult): array
    {
        $dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];

        $data = [];
        $xAxisSet = [];

        foreach ($mongoResult as $dayEntry) {
            $dayOfWeek = (int) $dayEntry['_id']; // 1–7
            $dayIndex = $dayOfWeek - 1; // 0–6

            foreach ($dayEntry['hours'] as $hourEntry) {
                $hour = (int) $hourEntry['hour'];
                $avgCount = (float) $hourEntry['avgCount'];
                $avgCount = ceil($avgCount); // округляем вверх

                $xAxisSet[$hour] = true;

                // временно сохраняем как есть — заменим потом, когда сформируем индексы
                $data[] = ['hour' => $hour, 'day' => $dayIndex, 'value' => $avgCount];
            }
        }

        // Собираем и форматируем xAxis
        $xAxis = array_keys($xAxisSet);
        sort($xAxis);
        $xAxisFormatted = array_map(fn($h) => str_pad((string)$h, 2, '0', STR_PAD_LEFT) . ':00', $xAxis);
        $xIndexMap = array_flip($xAxisFormatted); // '09:00' => 0

        // Переформатируем data с использованием индексов
        $finalData = [];
        foreach ($data as $item) {
            $hourStr = str_pad((string)$item['hour'], 2, '0', STR_PAD_LEFT) . ':00';
            $xIndex = $xIndexMap[$hourStr] ?? null;
            if ($xIndex !== null) {
                $finalData[] = [$xIndex, $item['day'], $item['value']];
            }
        }

        return [
            'xAxis' => $xAxisFormatted,
            'yAxis' => $dayLabels,
            'data' => $finalData,
        ];
    }









    public function rebuild(array $items): void
    {

        $_items = [];
        foreach ($items as $key => $field) {


            $key = ShmUtils::cleanKey($key);




            ShmUtils::isValidKey($key);

            if (!$field) {
                continue;
            }

            /* if ($key == '_id' && !($field instanceof IDType)) {
                throw new \Exception("Field '_id' must be an instance of IDType.");
            }*/


            if (!$field instanceof BaseType) {

                throw new \Exception("Field '{$key}' must be an instance of BaseType." .
                    gettype($field) . " given.");
            }

            if ($field->type == 'visualGroup') {

                foreach ($field->items as $subKey => $subField) {


                    $subField->key($subKey)->group($field->title ?? "Default", $field->assets['icon'] ?? null);


                    $subField->setParent($this);

                    $_items[$subKey] = $subField;
                }

                continue;
            }




            $field->key($key);

            $field->setParent($this);

            $_items[$key] = $field;
        }




        $this->items =  $_items;
    }


    public function setVisualGroup(string $title = "", $icon = null, array $keys = []): static
    {

        if (!$title || !$keys || count($keys) == 0) {
            return $this;
        }

        foreach ($this->items as $key => $field) {

            if (in_array($key, $keys)) {

                $field->group($title, $icon ?? null);
            }
        }
        return $this;
    }

    /**
     * @param array<string, BaseType> $items
     */
    public function __construct(array $items)
    {

        $this->rebuild($items);
    }





    public function normalize(mixed $value, $addDefaultValues = false, string | null $processId = null): mixed
    {





        if ($addDefaultValues) {


            /* if (!(is_array($value) || $value instanceof Traversable)) {
                //$value = [];
                return null;
            }*/

            foreach ($this->items as $name => $type) {
                if ($processId) {
                    ProcessLogs::addLog($processId, "Normalizing field '{$name}' in StructureType '{$this->key}'");
                }



                if ($type instanceof IDType) {

                    if (!$type->defaultIsSet && (!isset($value[$name]) || $value[$name] === null)) {

                        continue;
                    }
                }

                $value[$name] =  $type->normalize($value[$name] ?? null, $addDefaultValues, $processId);
            }
        } else {




            if (!(is_array($value) || $value instanceof Traversable)) {
                return null;
            }



            foreach ($this->items as $key => $type) {





                if (isset($value[$key]) || $type instanceof UUIDType) {

                    $value[$key] = $this->items[$key]->normalize($value[$key] ?? null, $addDefaultValues, $processId);
                }


                if ($type->notNull && !isset($value[$key])) {
                    unset($value[$key]);
                    continue;
                }
            }
        }




        return $value;
    }


    public function getItems(): array
    {
        return $this->items;
    }



    public function removeOtherItems(mixed $value): mixed
    {




        if (!(is_array($value) || $value instanceof Traversable)) {
            return null;
        }


        if (isset($this->items['*'])) {
            return $value;
        }

        $newValue = [];



        foreach ($value as $key => $val) {
            if (isset($this->items[$key]) && !$this->items[$key]->hide) {
                $newValue[$key] = $this->items[$key]->removeOtherItems($val);
            }
        }

        return $newValue;
    }



    public function validate(mixed $value): void
    {
        parent::validate($value);
        if ($value === null) {
            return;
        }
        if (!is_array($value)) {
            $field = $this->title ?? 'Value';
            throw new \Exception("{$field} must be an object/structure (associative array).");
        }
        foreach ($this->items as $name => $type) {
            try {
                $type->validate($value[$name] ?? null);
            } catch (\Exception $e) {
                $field = $this->title ?? $name;
                throw new \Exception("{$field}.{$name}: " . $e->getMessage());
            }
        }
    }

    private $staticBaseTypeName = null;

    public function staticBaseTypeName(string $name): static
    {
        $this->staticBaseTypeName = ShmUtils::translitIfCyrillic(ShmUtils::onlyLetters($name));
        return $this;
    }



    public function baseTypeName()
    {

        if ($this->staticBaseTypeName) {
            return $this->staticBaseTypeName;
        }




        $typeName = '';

        if ($this->collection) {

            $_collection = $this->collection;
            $_collection = explode('_', $_collection);
            $_collection = array_map(fn($item) => ucfirst($item), $_collection);
            $_collection = implode('', $_collection);

            $typeName = Inflect::singularize(ShmUtils::onlyLetters($_collection));
        } else {

            if (!$this->key) {


                throw new \Exception("baseTypeName -> Key is not set for StructureType. " . implode(', ', array_keys($this->items)));
            }

            $keys = array_keys($this->items);
            $keys = array_map(fn($type) => $type === '*' ? 'mixed' : $type, $keys);
            $typeName = Inflect::singularize(ShmUtils::onlyLetters($this->key)) .  AutoPostfix::get($keys);
        }

        $baseTypePrefix = null;

        return $baseTypePrefix ? $baseTypePrefix . $typeName : $typeName;
    }





    public function typeName()
    {
        return $this->baseTypeName() . 'Type';
    }

    public function inputTypeName()
    {
        return $this->baseTypeName() . 'Input';
    }

    public function filterTypeName()
    {
        return $this->baseTypeName() . 'FilterInput';
    }


    public function deepFindItemByKey(string $key): ?StructureType
    {


        foreach ($this->items as $item) {


            if ($item->key === $key) {
                return $item;
            }


            if ($item instanceof StructureType) {

                if ($item instanceof StructureType) {
                    $val =  $item->deepFindItemByKey($key);
                    if ($val) {
                        return $val;
                    }
                }
            }
        }



        return null;
    }




    public function findItemByCollection(string $collection, $ignoreReverse = false): ?StructureType
    {

        $collections = $this->getAllCollections();

        if (isset($collections[$collection])) {
            return $collections[$collection];
        }



        foreach ($this->items as $item) {




            if ($item instanceof IDType || $item instanceof IDsType) {




                $document = $item->getDocument();



                if ($document && $document->collection === $collection) {
                    return $document;
                }

                if ($document && !$ignoreReverse) {
                    $val =  $document->findItemByCollection($collection, true);
                    if ($val) {
                        return $val;
                    }
                }
            }

            if ($item instanceof ArrayOfType) {

                if ($item->itemType instanceof StructureType && $item->itemType->collection === $collection) {
                    return $item->itemType;
                }

                if ($item->itemType instanceof StructureType && !$ignoreReverse) {


                    $val =  $item->itemType->findItemByCollection($collection);
                    if ($val) {
                        return $val;
                    }
                }
            }


            if ($item instanceof StructureType) {
                if ($item->collection === $collection)
                    return $item;



                if ($item instanceof StructureType && !$ignoreReverse) {
                    $val =  $item->findItemByCollection($collection);
                    if ($val) {
                        return $val;
                    }
                }
            }
        }



        return null;
    }

    public function haveItemByKey(string $key): bool
    {
        return isset($this->items[$key]);
    }

    public function findItemByKey(string $key): ?BaseType
    {

        if (isset($this->items[$key])) {
            return $this->items[$key];
        }

        return null;
    }

    public function findItemsByKey(array $keys): array
    {
        $result = [];

        foreach ($keys as $key) {
            $result[$key] = $this->findItemByKey($key);
        }

        if (count($result) === 0) {

            throw new \Exception("findItemsByKey -> Keys not found: " . implode(', ', $keys));
        }
        return $result;
    }


    public function findItemByType(string | BaseType $type): ?BaseType
    {

        if ($type instanceof BaseType) {
            $type = $type->type;
        }

        foreach ($this->items as $item) {
            if ($item->type === $type) {
                return $item;
            }
        }


        return null;
    }





    public function fullCleanDefault(): static
    {
        $this->defaultIsSet = false;
        $this->default = null;

        foreach ($this->items as $key => $field) {

            $field->fullCleanDefault();
        }

        return $this;
    }







    /**
     * Функция все ключи который идет через точку преобразует во вложенные массивы
     */
    private function  normalizeDotKeys(array $data): array
    {

        $result = [];

        foreach ($data as $key => $value) {

            if (is_array($value)) {

                $value = $this->normalizeDotKeys($value);
            }

            $parts = explode('.', (string)$key);

            $current = &$result;

            foreach ($parts as $part) {

                if (!isset($current[$part]) || !is_array($current[$part])) {

                    $current[$part] ??= [];
                }

                $current = &$current[$part];
            }

            $current = $value;

            unset($current);
        }

        return $result;
    }



    public function filterToPipeline($filter, array | null $absolutePath = null): ?array
    {


        if (!$absolutePath) {
            $filter = $this->normalizeDotKeys($filter);
        }

        if (!is_array($filter) && !is_object($filter)) {
            return null;
        }


        if ($absolutePath !== null) {
            $absolutePath = [...$absolutePath, $this->key];
        } else {
            $absolutePath = [];
        }
        $pipeline = [];

        foreach ($filter as $key => $value) {

            if ($value === null) {
                continue;
            }
            if (!isset($this->items[$key])) {
                continue;
            }




            $type = $this->items[$key];


            $pipelineItem = $type->filterToPipeline($value,  $absolutePath);

            $pipeline = [...$pipeline, ...($pipelineItem ?? [])];
        }


        return  $pipeline;
    }


    public ?BaseType $filterType = null;


    public function extensionsFilterType(): ?BaseType
    {


        $fields = [];

        $extensionsStructure = $this->extensionsStructure();




        foreach ($extensionsStructure as $extension => $extensionStructure) {




            $extensionFilterType = $extensionStructure->filterType();



            if ($extensionFilterType) {
                $fields[$extension] = $extensionFilterType;
            }
        }


        if (count($fields) == 0) {
            return null;
        }
        return Shm::structure($fields);
    }


    public function filterType($safeMode = false): ?BaseType
    {


        if ($this->type == "structure" && $this->filterType) {
            return $this->filterType;
        }

        $fields = [];

        foreach ($this->items as $key => $field) {

            $input = $field->filterType($safeMode);
            if ($input) {
                $fields[$key] = $input;
            }
        }
        if (count($fields) == 0) {
            return null;
        }
        $itemTypeFilter = Shm::structure($fields)->editable()->title($this->title)->staticBaseTypeName($this->key . 'FilterArgs')->inAdmin($this->inAdmin);

        if ($this->type == "structure") {
            $this->filterType =  $itemTypeFilter;
        }

        return  $itemTypeFilter;
    }





    public function tsType(): TSType
    {
        $TSType = new TSType();




        $value = [];

        foreach ($this->items as $key => $item) {
            $separate = $item->nullable ? '?: ' : ': ';




            if ($item->hide) {
                continue;
            }





            if ($key == "*") {
                $key = '[key: string]';
                $separate = ': ';
            }

            if ($item instanceof StaticType) {

                $value[] = $key .  $separate . $item->getStaticValueTS();
                continue;
            }


            if ($item instanceof SelfRefType) {

                $_type = $item->resolveType();

                if ($_type instanceof StructureType) {

                    $value[] = $key .  $separate . $_type->typeName();
                } else {
                    throw new \Exception("SelfRefType must resolve to a StructureType, got " . get_class($_type));
                }
                continue;
            }

            if ($item->deprecated) {

                $prefix = "/**\n * @deprecated " . $item->deprecated . "\n */\n";
            } else {
                $prefix = "";
            }

            $value[] = $prefix . $key .  $separate . $item->tsType()->getTsTypeName();
        }

        $TSType = new TSType($this->typeName(), '{\n' . implode(',\n', $value) . '\n}');




        return $TSType;
    }


    public function tsInputType(): TSType
    {
        $TSType = new TSType();



        $value = [];

        foreach ($this->items as $key => $item) {
            if (!$item->editable) {
                continue;
            }

            if ($item instanceof StaticType) {
                continue;
            }


            $separate = $item->nullable ? '?: ' : ': ';

            if ($key == "*") {
                $key = '[key: string]';
                $separate = ': ';
            }


            $value[] = $key .  $separate . $item->tsInputType()->getTsTypeName();
        }

        $TSType = new TSType($this->inputTypeName(), '{\n' . implode(',\n', $value) . '\n}');




        return $TSType;
    }









    public function updateOne(array $filter, array $update, array $options = []): \MongoDB\UpdateResult
    {



        $filter = mDB::replaceStringToObjectIds($filter);



        $eventPayload = new ShmEventPayload();
        $eventPayload->preUpdatePayload = new ShmEventPreUpdatePayload();
        $eventPayload->preUpdatePayload->filter = $filter;
        $eventPayload->preUpdatePayload->update = $update;



        $this->callEventPreUpdate($eventPayload);




        $arrayFields = [];

        foreach (['$pull', '$addToSet', '$push'] as $op) {
            if (isset($update[$op]) && is_array($update[$op])) {
                foreach ($update[$op] as $field => $value) {
                    $arrayFields[$field] = true;
                }
            }
        }


        if (count($arrayFields) > 0) {
            $orConditions = [];

            foreach (array_keys($arrayFields) as $field) {
                $orConditions[] = [$field => ['$exists' => false]];
                $orConditions[] = [$field => ['$not' => ['$type' => 'array']]];
            }

            if (count($orConditions) > 0) {
                // Найти документ и привести нужные поля к массивам
                $setData = [];
                foreach (array_keys($arrayFields) as $field) {
                    $setData[$field] = [];
                }

                $finalFilter = $filter;

                if (isset($finalFilter['$or'])) {
                    $finalFilter = [
                        '$and' => [
                            $filter,
                            ['$or' => $orConditions]
                        ]
                    ];
                } else {
                    $finalFilter['$or'] = $orConditions;
                }




                mDB::_collection($this->collection)->updateOne(

                    $finalFilter,
                    [
                        '$set' => $setData
                    ]
                );
            }
        }


        if (isset($update['$set'])) {

            if (array_key_exists("_id", (array) $update['$set'])) {
                unset($update['$set']['_id']);
            }
            $update['$set']["_needRecalculateSearch"] = true;
            $update['$set'] = $this->normalize($update['$set']);
        }

        if (isset($update['$unset'])) {
            if (array_key_exists("_id", (array) $update['$unset'])) {
                unset($update['$unset']['_id']);
            }
            $update['$set']["_needRecalculateSearch"] = true;
            // $update['$unset'] is already set above
        }

        $result =  mDB::collection($this->collection)->updateOne($filter, $update, [
            ...$options,
        ]);


        $eventPayload->postUpdatePayload = new ShmEventPostUpdatePayload();
        $eventPayload->postUpdatePayload->filter = $filter;
        $eventPayload->postUpdatePayload->update = $update;
        $this->callEventPostUpdate($eventPayload);


        return    $result;
    }

    public function _updateOne(array $filter, array $update, array $options = []): \MongoDB\UpdateResult
    {


        $update =  mDB::collection($this->collection)->updateOne($filter, $update, [
            ...$options,
        ]);





        return $update;
    }




    public function updateMany(array $filter, array $update, array $options = []): \MongoDB\UpdateResult
    {



        $filter = mDB::replaceStringToObjectIds($filter);


        $eventPayload = new ShmEventPayload();
        $eventPayload->preUpdatePayload = new ShmEventPreUpdatePayload();
        $eventPayload->preUpdatePayload->filter = $filter;
        $eventPayload->preUpdatePayload->update = $update;
        $this->callEventPreUpdate($eventPayload);


        $arrayFields = [];

        foreach (['$pull', '$addToSet', '$push'] as $op) {
            if (isset($update[$op]) && is_array($update[$op])) {
                foreach ($update[$op] as $field => $value) {
                    $arrayFields[$field] = true;
                }
            }
        }

        if (count($arrayFields) > 0) {
            $orConditions = [];

            foreach (array_keys($arrayFields) as $field) {
                $orConditions[] = [$field => ['$exists' => false]];
                $orConditions[] = [$field => ['$not' => ['$type' => 'array']]];
            }

            if (count($orConditions) > 0) {
                // Найти документ и привести нужные поля к массивам
                $setData = [];
                foreach (array_keys($arrayFields) as $field) {
                    $setData[$field] = [];
                }

                $finalFilter = $filter;

                if (isset($finalFilter['$or'])) {
                    $finalFilter = [
                        '$and' => [
                            $filter,
                            ['$or' => $orConditions]
                        ]
                    ];
                } else {
                    $finalFilter['$or'] = $orConditions;
                }


                mDB::_collection($this->collection)->updateMany(

                    $finalFilter,
                    [
                        '$set' => $setData
                    ]
                );
            }
        }


        if (isset($update['$set'])) {

            if (array_key_exists("_id", (array) $update['$set'])) {
                unset($update['$set']['_id']);
            }
            $update['$set']["_needRecalculateSearch"] = true;
            $update['$set'] = $this->normalize($update['$set']);
        }

        if (isset($update['$unset'])) {
            if (array_key_exists("_id", (array) $update['$unset'])) {
                unset($update['$unset']['_id']);
            }
            $update['$set']["_needRecalculateSearch"] = true;
            // $update['$unset'] is already set above
        }

        $result =  mDB::collection($this->collection)->updateMany($filter, $update, [
            ...$options,
        ]);


        $eventPayload->postUpdatePayload = new ShmEventPostUpdatePayload();
        $eventPayload->postUpdatePayload->filter = $filter;
        $eventPayload->postUpdatePayload->update = $update;
        $this->callEventPostUpdate($eventPayload);






        return   $result;
    }

    public function deleteOne(array $filter, array $options = [])
    {
        $filter = mDB::replaceStringToObjectIds($filter);

        $eventPayload = new ShmEventPayload();
        $eventPayload->preDeletePayload = new ShmEventPreDeletePayload();
        $eventPayload->preDeletePayload->filter = $filter;
        $this->callEventPreDelete($eventPayload);

        $deleteOneResult =   mDB::collection($this->collection)->deleteOne($filter, $options);



        $eventPayload->postDeletePayload = new ShmEventPostDeletePayload();
        $eventPayload->postDeletePayload->filter = $filter;

        $this->callEventPostDelete($eventPayload);

        return $deleteOneResult;
    }

    public function deleteMany(array $filter, array $options = [])
    {
        $filter = mDB::replaceStringToObjectIds($filter);

        $eventPayload = new ShmEventPayload();
        $eventPayload->preDeletePayload = new ShmEventPreDeletePayload();
        $eventPayload->preDeletePayload->filter = $filter;
        $this->callEventPreDelete($eventPayload);

        $deleteManyResult =   mDB::collection($this->collection)->deleteMany($filter, $options);

        $eventPayload->postDeletePayload = new ShmEventPostDeletePayload();
        $eventPayload->postDeletePayload->filter = $filter;
        $this->callEventPostDelete($eventPayload);

        return $deleteManyResult;
    }







    public function find(array $filter, array $options = [])
    {
        $filter = mDB::replaceStringToObjectIds($filter);
        $pipeline = [];

        if (count($filter) > 0) {

            $pipeline = [
                [
                    '$match' => $filter
                ]
            ];
        }

        $pipeline = [
            ...$pipeline,
            ...$this->getPipeline(),

        ];

        if (isset($options['sort']) && is_array($options['sort'])) {
            $pipeline[] = [
                '$sort' => $options['sort']
            ];
        } else {

            if ($this->manualSort) {

                $pipeline[] = [
                    '$sort' => ["_sortWeight" => -1]
                ];
            }
        }
        if (isset($options['skip']) && is_int($options['skip'])) {
            $pipeline[] = [
                '$skip' => $options['skip']
            ];
        }
        if (isset($options['limit']) && is_numeric($options['limit'])) {


            $pipeline[] = [
                '$limit' =>  (int) $options['limit']
            ];
        }


        if (isset($options['projection']) && is_array($options['projection'])) {
            $pipeline[] = [
                '$project' => $options['projection']
            ];
        }



        return   mDB::collection($this->collection)->aggregate($pipeline)->toArray();
    }

    //bulkWrite
    public function bulkWrite(array $bulkWrite): \MongoDB\BulkWriteResult
    {
        return mDB::collection($this->collection)->bulkWrite($bulkWrite);
    }

    public function distinct(string $field, array $filter = [])
    {

        return  mDB::collection($this->collection)->distinct($field, $filter);
    }

    public function count(array $filter, array $options = []): int
    {
        return mDB::collection($this->collection)->countDocuments($filter, $options);
    }


    public function balance(string $currency): ?BalanceType
    {

        $currency = BalanceUtils::normalizeCurrency($currency);

        $paymentItems = $this->findItemsByCondition(function ($item) use ($currency) {
            if ($item instanceof BalanceType) {


                if ($item->currency == $currency) {
                    return true;
                }
            }
            return false;
        });

        return $paymentItems[0] ?? null;
    }


    public function findOne(array $filter, array $options = [])
    {

        $filter = mDB::replaceStringToObjectIds($filter);

        $pipeline = [];

        if (count($filter) > 0) {
            $pipeline[] = [
                '$match' => $filter
            ];
        }


        $pipeline = [
            ...$pipeline,
            ...$this->getPipeline(),
            [
                '$limit' => 1
            ]
        ];

        return   mDB::collection($this->collection)->aggregate($pipeline)->toArray()[0] ?? null;
    }




    public function insertOne($document, array $options = []): \MongoDB\InsertOneResult
    {


        $document = [
            ...$document,
            ...$this->getInsertValues()
        ];

        $eventPayload = new ShmEventPayload();

        $eventPayload->preInsertPayload = new ShmEventPreInsertPayload();

        $eventPayload->preInsertPayload->insertDocuments = [$document];



        $this->callEventPreInsert($eventPayload);


        $document = $this->normalize($document, true);





        $insertResult = mDB::collection($this->collection)->insertOne($document, $options);


        $eventPayload->postInsertPayload = new ShmEventPostInsertPayload();
        $eventPayload->postInsertPayload->insertDocuments = [$document];
        $eventPayload->postInsertPayload->insertIDs = [$insertResult->getInsertedId()];

        $this->callEventPostInsert($eventPayload);

        return $insertResult;
    }





    public function insertMany(array $documents, array $options = []): \MongoDB\InsertManyResult
    {

        foreach ($documents as &$document) {
            $document = [
                ...$document,
                ...$this->getInsertValues()
            ];
        }

        $documents = Shm::arrayOf($this)->normalize($documents, true);

        $eventPayload = new ShmEventPayload();
        $eventPayload->preInsertPayload = new ShmEventPreInsertPayload();
        $eventPayload->preInsertPayload->insertDocuments = $documents;

        $this->callEventPreInsert($eventPayload);

        $insertResult = mDB::collection($this->collection)->insertMany($documents, $options);


        $eventPayload->postInsertPayload = new ShmEventPostInsertPayload();
        $eventPayload->postInsertPayload->insertDocuments = $documents;
        $eventPayload->postInsertPayload->insertIDs = $insertResult->getInsertedIds();


        $this->callEventPostInsert($eventPayload);

        return $insertResult;
    }


    public function moveRow($_id, $aboveId = null, $belowId = null)
    {

        $currentId = $_id;
        $aboveWeight = null;
        $belowWeight = null;

        if ($aboveId !== null) {
            $aboveWeight = $this->findOne([
                "_id" => mDB::id($aboveId)
            ])->_sortWeight ?? null;
        }

        if ($belowId !== null) {
            $belowWeight = $this->findOne([
                "_id" => mDB::id($belowId)
            ])->_sortWeight ?? null;
        }
        $newWeight = null;

        if ($aboveWeight &&  $belowWeight) {
            $newWeight = ($aboveWeight + $belowWeight) / 2;
        } else if ($aboveWeight) {
            $newWeight = $aboveWeight / 2;
        } else if ($belowWeight) {
            $newWeight = $belowWeight + 1;
        }

        //Округляем в вверх
        $newWeight = ceil($newWeight);

        if ($newWeight) {
            $this->updateOne([
                "_id" => mDB::id($currentId)
            ], [
                '$set' => [
                    '_sortWeight' => $newWeight
                ]
            ]);
            return true;
        } else {
            return false;
        }
    }





    private function formatAggregateHash($updated_min, $updated_avg, $updated_max): string
    {
        $updated_min = md5($updated_min ?? "none");
        $updated_avg = md5($updated_avg ?? "none");
        $updated_max = md5($updated_max ?? "none");

        return md5($updated_min . $updated_avg . $updated_max);
    }

    public function hashFromAggregateRows(array $rows): string
    {
        $updatedAts = [];

        foreach ($rows as $row) {
            $updatedAt = is_array($row)
                ? ($row['updated_at'] ?? null)
                : ($row->updated_at ?? null);

            if ($updatedAt !== null) {
                $updatedAts[] = $updatedAt;
            }
        }

        if ($updatedAts === []) {
            return $this->formatAggregateHash(null, null, null);
        }

        return $this->formatAggregateHash(
            min($updatedAts),
            array_sum($updatedAts) / count($updatedAts),
            max($updatedAts)
        );
    }

    public function aggregateWithHash(array $pipeline = []): array
    {
        $data = $this->aggregate($pipeline)->toArray();

        return [
            'data' => $data,
            'hash' => $this->hashFromAggregateRows($data),
        ];
    }

    public  function aggregateHash(array $pipeline = [])
    {


        $pipeline = array_merge([
            [
                '$match' => [
                    "deleted_at" => ['$exists' => false],
                ]
            ],
        ], $pipeline);

        $pipeline[] = [
            '$group' => [
                '_id' => null,
                'updated_min' => ['$min' => '$updated_at'],
                'updated_avg' => ['$avg' => '$updated_at'],
                'updated_max' => ['$max' => '$updated_at'],
            ],
        ];

        $result = mDB::collection($this->collection)->aggregate($pipeline)->toArray()[0] ?? null;

        return $this->formatAggregateHash(
            $result['updated_min'] ?? null,
            $result['updated_avg'] ?? null,
            $result['updated_max'] ?? null
        );
    }



    private function haveSortStage(array $pipeline = []): bool
    {
        foreach ($pipeline as $stage) {
            if (isset($stage['$sort'])) {
                return true;
            }
        }
        return false;
    }


    public  function aggregate(array $pipeline = [])
    {


        $pipeline = array_merge([
            [
                '$match' => [
                    "deleted_at" => ['$exists' => false],
                ]
            ],
        ], $pipeline);

        if ($this->manualSort && !$this->haveSortStage($pipeline)) {
            $pipeline[] = [
                '$sort' => [
                    '_sortWeight' => -1,
                ]
            ];
        }




        return mDB::collection($this->collection)->aggregate($pipeline);
    }



    public function hideNotInTable(): static
    {

        if ($this->type == "structure") {


            foreach ($this->items as $key => $item) {

                $item->hideNotInTable();
            }
        } else {
            if (!$this->inTable) {
                $this->hide = true;
            }
        }
        return $this;
    }


    private $badgeHandler;

    public function setBadge(callable $handler): static
    {
        $this->badgeHandler = $handler;
        return $this;
    }

    public function hasBadge(): bool
    {
        return $this->badgeHandler !== null;
    }

    public function getBadge(): mixed
    {

        if (!$this->badgeHandler) {
            return null;
        }
        return ($this->badgeHandler)();
    }


    private $chatNameHandler;

    /** Установка обработчика имени чата */
    /** @param callable($_id): string $handler */
    public function setChatNameHandler(callable $handler): static
    {
        $this->chatNameHandler = $handler;
        return $this;
    }

    /** Получение имени чата */
    /** @param $_id string */
    public function getChatNameHandler(string $_id): string
    {
        if (!$this->chatNameHandler) {
            return '';
        }
        return ($this->chatNameHandler)($_id);
    }



    /** @var array<string, list<callable(ShmEventPayload $event):void>> */
    private array $eventHandlers = [];

    private function normalizeEvent(string $event): string
    {
        $event = trim($event);
        if ($event === '') {
            throw new \InvalidArgumentException('Event name cannot be empty');
        }

        if (!in_array($event, ShmEventPayload::ALLOWED_EVENTS, true)) {
            throw new \InvalidArgumentException(
                'Unknown event: ' . $event . '. Allowed: ' . implode(', ', ShmEventPayload::ALLOWED_EVENTS)
            );
        }
        return $event;
    }



    /** Перед регистрацией пользователя
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostRegister(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_REGISTER, $handler);
    }

    /** После входа в систему
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostLogin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_LOGIN, $handler);
    }

    /** Перед добавлением в базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreInsert(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_INSERT, $handler);
    }

    /** После добавления в базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostInsert(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_INSERT, $handler);
    }

    /** Перед обновлением в базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreUpdate(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_UPDATE, $handler);
    }

    /** После обновления в базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostUpdate(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_UPDATE, $handler);
    }

    /** Перед удалением из базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreDelete(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_DELETE, $handler);
    }

    /** После удаления из базу данных
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostDelete(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_DELETE, $handler);
    }



    /** Перед добавлением в базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreInsertAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_INSERT_ADMIN, $handler);
    }

    /** После добавления в базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostInsertAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_INSERT_ADMIN, $handler);
    }
    /** Перед обновлением в базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreUpdateAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_UPDATE_ADMIN, $handler);
    }
    /** После обновления в базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostUpdateAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_UPDATE_ADMIN, $handler);
    }
    /** Перед удалением из базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPreDeleteAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_PRE_DELETE_ADMIN, $handler);
    }
    /** После удаления из базу данных через админку
     * @param callable(ShmEventPayload $event): void $handler
     */
    public function addEventPostDeleteAdmin(callable $handler): static
    {
        return $this->addEvent(ShmEventPayload::EVENT_POST_DELETE_ADMIN, $handler);
    }
    /**
     * Зарегистрировать обработчик события.
     * @param  $event
     * @param callable(ShmEventPayload $event): void $handler
     */
    private function addEvent(string $event, callable $handler): static
    {


        //Проверка параметров
        if (!in_array($event, ShmEventPayload::ALLOWED_EVENTS)) {
            throw new \InvalidArgumentException('Unknown event: ' . $event . '. Allowed: ' . implode(', ', ShmEventPayload::ALLOWED_EVENTS));
        }
        if (!is_callable($handler)) {
            throw new \InvalidArgumentException('Handler must be a callable');
        }




        $event = $this->normalizeEvent($event);
        $this->eventHandlers[$event] ??= [];
        $this->eventHandlers[$event][] = $handler;
        return $this;
    }


    /** Перед добавлением в базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPreInsert(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_INSERT, $payload);
    }

    /** После добавления в базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPostInsert(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_INSERT, $payload);
    }

    /** Перед обновлением в базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPreUpdate(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_UPDATE, $payload);
    }

    /** После обновления в базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPostUpdate(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_UPDATE, $payload);
    }


    /** Перед удалением из базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPreDelete(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_DELETE, $payload);
    }

    /** После удаления из базу данных
     * @param ShmEventPayload $payload
     */
    public function callEventPostDelete(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_DELETE, $payload);
    }

    /** Перед добавлением в базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPreInsertAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_INSERT_ADMIN, $payload);
    }

    /** После добавления в базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPostInsertAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_INSERT_ADMIN, $payload);
    }
    /** Перед обновлением в базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPreUpdateAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_UPDATE_ADMIN, $payload);
    }
    /** После обновления в базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPostUpdateAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_UPDATE_ADMIN, $payload);
    }
    /** Перед удалением из базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPreDeleteAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_PRE_DELETE_ADMIN, $payload);
    }
    /** После удаления из базу данных через админку
     * @param ShmEventPayload $payload
     */
    public function callEventPostDeleteAdmin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_DELETE_ADMIN, $payload);
    }

    public function callEventPostRegister(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_REGISTER, $payload);
    }

    public function callEventPostLogin(ShmEventPayload $payload): void
    {
        $this->callEvent(ShmEventPayload::EVENT_POST_LOGIN, $payload);
    }

    /**
     * Вызвать обработчики события.
     * @param  $event
     * @param ShmEventPayload $payload
     */
    private function callEvent(string $event, ShmEventPayload $payload): void
    {
        $event = $this->normalizeEvent($event);
        $handlers = $this->eventHandlers[$event] ?? [];
        if (!$handlers) return;

        foreach (array_values($handlers) as $handler) {
            try {
                $handler($payload);
            } catch (\Throwable $e) {
                ShmInit::sendOnError($e);
            }
        }
    }

    /** Получить список разрешённых событий*/
    public static function allowedEvents(): array
    {
        return ShmEventPayload::ALLOWED_EVENTS;
    }
}