<?php

namespace Shm\ShmTypes;


use Nette\PhpGenerator\Method;
use Sentry\Util\Arr;
use Shm\Shm;
use Shm\ShmRPC\ShmRPCCodeGen\TSType;
use Shm\ShmTypes\CompositeTypes\BalanceTypes\BalanceType;
use Shm\ShmTypes\Utils\JsonLogicBuilder;
use Shm\ShmUtils\AutoPostfix;

use Shm\ShmUtils\ShmInit;
use Shm\ShmUtils\ShmUtils;
use Traversable;

/**
 * Base class for all schema types
 * 
 * This abstract class provides common functionality for all schema types
 * including validation, normalization, and metadata management.
 */
abstract class BaseType
{
    public bool $hide = false;
    public bool $unique = false;
    public bool $globalUnique = false;

    public ?string $description = null;

    public $path = [];


    public $lookupKey = null;
    private $lookupStructureHandler = null;

    public function lookupKey($key, callable | StructureType $structureHandler): static
    {
        $this->lookupKey = $key;

        if (is_callable($structureHandler)) {
            $this->lookupStructureHandler = $structureHandler;
        } else {
            $this->lookupStructureHandler =  function ($value) use ($structureHandler) {
                return $structureHandler;
            };
        }


        return $this;
    }

    public function hasLookupKey(): bool
    {
        return $this->lookupKey !== null && $this->lookupStructureHandler !== null;
    }



    /**
     * Call only from root element!
     */
    public function updatePath(array | null $prevPath = null)
    {

        $this->path = $prevPath ?? [];

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

                $item->updatePath([...$this->path, $key]);
            }
        }

        if (isset($this->itemType)) {
            $this->itemType->updatePath([...$this->path, '[]']);
        }
    }


    public bool $indexed = false;


    protected ?BaseType $parent = null;

    public function setParent(BaseType | null $parent): void
    {
        $this->parent = $parent;
    }

    public function getParent(): ?BaseType
    {
        return $this->parent;
    }

    public function indexed(bool $indexed = true): static
    {
        $this->indexed = $indexed;
        return $this;
    }

    public bool $compositeType = false;

    //Поиск элементов функции условия
    public function findItemsByCondition(callable $condition): array
    {
        $result = [];





        if ($condition($this)) {


            $result[] = $this;
        }

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



                $found = $item->findItemsByCondition($condition);



                foreach ($found as $foundItem) {

                    $result[] = $foundItem;
                }
            }
        }

        if (isset($this->itemType)) {
            $result = [...$result, ...$this->itemType->findItemsByCondition($condition)];
        }

        return $result;
    }



    public function extensionsStructure(): array
    {

        $result = [];

        if ($this->hide) {
            return $result;
        }

        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                $result = [...$result, ...$item->extensionsStructure()];
            }
        }

        if (isset($this->itemType)) {
            $result = [...$result, ...$this->itemType->extensionsStructure()];
        }

        if ($this instanceof IDsType || $this instanceof IDType) {
            if ($this->collection && $this->collection != '_files') {
                $document = $this->getDocument();
                if ($document) {
                    $result[$this->collection] =  $document;
                }
            }
        }

        return $result;
    }





    public function getPathArray($path = [], bool $withArray = false): array
    {



        if ($this instanceof ArrayOfType) {
            if (!$withArray) {
                throw new \Exception("ArrayOfType does not have a unic path");
            } else {



                return [...$path];
            }
        }
        if ($this instanceof StructureType && $this->collection) {
            return [...$path];
        }

        if ($this->parent) {

            if ($this->parent instanceof ArrayOfType) {
                if (!$withArray) {
                    throw new \Exception("ArrayOfType does not have a unic path");
                } else {

                    return $this->parent->getPathArray([$this->key, '[]', ...$path], $withArray);
                }
            }

            return $this->parent->getPathArray([$this->key, ...$path], $withArray);
        }

        return [$this->key, ...$path];
    }


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

        if ($this instanceof BalanceType && $this->currency == $currency) {
            return $this;
        }

        if ($this instanceof StructureType && isset($this->items)) {
            foreach ($this->items as $item) {
                $result = $item->findBalanceFieldByCurrency($currency);

                if ($result) {
                    return $result;
                }
            }
        }

        return null;
    }

    public function getPathString($path = [], bool $withArray = false): string
    {
        $path = $this->getPathArray($path, $withArray);

        if (!$path) {


            throw new \Exception("Path is not set for " . $this->type . " " . $this->key);
        }

        return implode('.', $path);
    }



    public function getParentCollection($default = null): ?string
    {

        if (!$this->parent) {

            if ($default) {
                return $default;
            }

            throw new \Exception("Parent is not set for " . $this->type . " " . $this->key);
        }

        if ($this->parent instanceof StructureType && $this->parent->collection) {
            return $this->parent->collection;
        } else {
            return $this->parent->getParentCollection($default);
        }

        return null;
    }

    public function getRootParent()
    {

        $result = null;
        if (!$this->parent) {
            $result = $this;
        }
        if (!$result) {
            if ($this->parent instanceof StructureType && $this->parent->collection) {
                $result = $this->parent;
            } else {
                $result = $this->parent->getRootParent();
            }
        }



        return $result;
    }

    public ?string $deprecated = null;


    public function deprecated(string $deprecated): static
    {
        $this->deprecated = $deprecated;
        return $this;
    }




    /**
     * Output transformers that are applied before sending data to RPC
     * @var callable[]
     */
    public array $outputTransformers = [];

    /**
     * Set an output transformer that will be applied before sending data to RPC
     * If $enabled = false, the transformer will not be registered
     *
     * @param callable $fn function(mixed $root, mixed $value): mixed
     * @param bool $enabled Whether to enable the transformer
     */
    public function onOutput(callable $fn, bool $enabled = true): static
    {
        if ($enabled) {
            $this->outputTransformers[] = $fn;
        }
        return $this;
    }

    public function hasOutputTransformers(): bool
    {
        if (!empty($this->outputTransformers)) {
            return true;
        }
        if (isset($this->items)) {
            foreach ($this->items as $item) {
                if ($item->hasOutputTransformers()) {
                    return true;
                }
            }
        }
        if (isset($this->itemType) && $this->itemType instanceof BaseType) {
            if ($this->itemType->hasOutputTransformers()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Рекурсивно применяет все зарегистрированные onOutput-хендлеры.
     */
    public function applyOutput(mixed $root, mixed $value): mixed
    {





        if ($this instanceof StructureType && $this->collection) {


            $root = $value;
        }

        // Применяем обработчики текущего узла
        foreach ($this->outputTransformers as $fn) {

            $value = $fn($root, $value);
        }

        // Спуск в подтипы
        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType && isset($value[$key])) {

                    $value[$key] = $item->applyOutput($root, $value[$key]);
                }
            }
        }

        // Спуск в itemType (массивы/коллекции)
        if (isset($this->itemType) && (is_array($value) || $value instanceof Traversable)) {


            foreach ($value as $i => $v) {
                $value[$i] = $this->itemType->applyOutput($root, $v);
            }
        }

        return $value;
    }

    public function toOutput(mixed $value): mixed
    {




        $root = null;



        return $this->applyOutput($root, $value);
    }

    public function globalUnique(bool $globalUnique = true): static
    {
        $this->globalUnique = $globalUnique;
        return $this;
    }


    public function unique(bool $unique = true): static
    {
        $this->unique = $unique;
        return $this;
    }

    public function ai(bool $ai = true): static
    {
        return $this;
    }

    public function description(string $description): static
    {
        $this->description = $description;
        return $this;
    }





    public $display = false;

    public $displayPrefix = null;


    public function display(bool | string $display = true): static
    {

        if (is_string($display)) {
            $this->displayPrefix = $display;
            $this->display = true;
            return $this;
        }

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


    public $notNull = false;

    public function notNull(bool $notNull = true): static
    {
        $this->notNull = $notNull;
        return $this;
    }

    public $single = false;

    public function hide($hide = true): static
    {


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

    public array $assets = [];

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


    /** @var array<string, BaseType> */
    public array $items;

    public array $values;


    public $group = [
        'key' => 'default'
    ];

    public function hideNotInTable(): static
    {

        if (!$this->key || $this->key == '_id') {
            return $this;
        }

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

    public function group(string $groupTitle, string | null $icon): static
    {
        $this->group = [
            'key' => md5($groupTitle),
            'icon' => $icon,
            'title' => $groupTitle,
        ];
        return $this;
    }

    public BaseType | null $itemType = null;

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


    public function exportRow(mixed $value): string | array | null
    {
        return "Error " . $this->key . ":  Export not implemented for " . $this->type;
    }


    public  $key = null;



    public  $min = null;
    public  $max = null;

    public bool $editable = false;

    private bool $editableSet = false;

    private bool $inAdminSet = false;

    private bool $inTableSet = false;


    public function isInTableSet(): bool
    {
        return $this->inTableSet;
    }


    public function isInAdminSet(): bool
    {
        return $this->inAdminSet;
    }


    public function isEditableSet(): bool
    {
        return $this->editableSet;
    }

    public bool $inAdmin = false;

    public bool $inTable = false;

    public int $col = 24;

    public int $row = 1;

    /**
     * Whether the value is required.
     */
    public bool $required = false;

    /**
     * Whether the value can be null.
     */
    public bool $nullable = true;

    /**
     * Default value if input is missing or null (when nullable).
     */
    public mixed $default = null;

    /**
     * Human-readable field title (used in error messages).
     */
    public ?string $title = null;



    /**
     * The internal type name (e.g., "string", "int", "array").
     */
    public string $type = 'mixed';

    public function __construct()
    {
        // No initialization by default
    }




    public function findIDTypeByCollection(string $collection): ?array
    {

        $result = [];

        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                if ($item instanceof IDType && $item->collection === $collection) {
                    $result[] = $item;
                }
                if ($item instanceof IDsType && $item->collection === $collection) {
                    $result[] = $item;
                }

                if ($item instanceof ArrayOfType) {



                    if ($item->itemType instanceof StructureType) {


                        $result = [...$result, ...$item->itemType->findIDTypeByCollection($collection)];
                    }
                }

                if ($item instanceof StructureType) {
                    $result = [...$result, ...$item->findIDTypeByCollection($collection)];
                }

                if (isset($this->itemType)) {
                    $result = [...$result, ...$this->itemType->findIDTypeByCollection($collection)];
                }
            }
        }

        return $result;
    }


    public function getAllCollections($result = [])
    {


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

                if ($item instanceof StructureType && $item->collection) {



                    $result[$item->collection] = $item;
                } else {


                    $result =  $item->getAllCollections($result);
                }
            }
        }

        if (isset($this->itemType)) {
            $result = $this->itemType->getAllCollections($result);
        }


        return $result;
    }


    public bool $report = false;

    public function report(bool $report = true): static
    {
        $this->report = $report;
        return $this;
    }


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

        if ($value && (is_string($value) || is_numeric($value))) {
            return $value;
        }

        return null;
    }



    public function equals(mixed $a, mixed $b): bool
    {
        return $a === $b;
    }



    /**
     * Set whether this field is for admin forms.
     * Ignores children fields
     */
    public function  inAdminThis(bool $inAdmin = true): static
    {
        $this->inAdmin = $inAdmin;
        $this->inAdminSet = true;


        return $this;
    }


    /**
     * Set whether this field is for admin forms.
     * For all children fields, this will also set the inAdmin property.
     */

    public function inAdmin(bool $isAdmin = true): static
    {
        $this->inAdmin = $isAdmin;

        $this->inAdminSet = true;

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

                if ($key === '_id') {
                    continue;
                }

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

                $item->inAdmin($isAdmin);
            }
        }

        if (isset($this->itemType)) {

            if (!$this->itemType->isInAdminSet()) {


                $this->itemType->inAdmin($isAdmin);
            }
        }

        return $this;
    }


    public function single(bool $single = true): static
    {
        $this->single = $single;
        return $this;
    }


    /**
     * Set the key for this field.
     * This is used to identify the field in forms and data structures.
     */

    public function key(string $key): static
    {

        ShmUtils::isValidKey($key);
        $this->key = $key;


        if (isset($this->itemType)) {
            $this->itemType->keyIfNot($key);
        }



        return $this;
    }


    //Установить ключ если если он не установлен
    public function keyIfNot(string $key): static
    {

        ShmUtils::isValidKey($key);

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



    /**
     * Set the minimum value (for numeric types).
     */
    public function min(int|float $min): static
    {
        $this->min = $min;
        return $this;
    }
    /**
     * Set the maximum value (for numeric types).
     */
    public function max(int|float $max): static
    {
        $this->max = $max;
        return $this;
    }


    public function icon(string | null $icon): static
    {
        $this->assets([
            'icon' => $icon,
        ]);

        return $this;
    }


    public function setRow(int $row): static
    {
        $this->row($row);
        return $this;
    }


    /**
     * Set the column width for admin forms.
     * 12 = half-width, 24 = full-width.
     */
    public function setCol(int $col): static
    {
        $this->col($col);
        return $this;
    }



    public null | int $tablePriority = null;


    public function inTableThis(bool | int $inTableThis = true): static
    {
        if (is_int($inTableThis)) {
            $this->tablePriority = $inTableThis;
            $this->inTable = true;

            return $this;
        }

        $this->inTable = $inTableThis;

        return $this;
    }

    /**
     * Set whether this field is for table display.
     * If true, it will use the table layout.
     */
    public function inTable(bool | int $isInTable = true): static
    {

        if (is_int($isInTable)) {
            $this->tablePriority = $isInTable;
            $this->inTable = true;

            return $this;
        }

        $this->inTable = $isInTable;


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

                if ($key === '_id') {
                    continue;
                }

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

                $item->inTable($isInTable);
            }
        }

        if (isset($this->itemType)) {

            if (!$this->itemType->isInTableSet()) {
                $this->itemType->inTable($isInTable);
            }
        }


        return $this;
    }

    public $cond = null;


    public function condLogic(JsonLogicBuilder $cond): static
    {
        $this->cond = $cond->build();
        return $this;
    }


    /**
     * Set a condition using JsonLogicBuilder.
     * This allows complex conditions to be applied to the field.
     */
    public  function cond(JsonLogicBuilder $cond): static
    {
        $this->cond = $cond->build();
        return $this;
    }




    public $localCond = null;


    /**
     * Set a condition using JsonLogicBuilder.
     * This allows complex conditions to be applied to the field.
     */
    public  function localCond(JsonLogicBuilder $cond): static
    {
        $this->localCond = $cond->build();
        return $this;
    }

    public function addUUIDInArray(): static
    {



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

                $item->addUUIDInArray();
            }
        }

        if (isset($this->itemType)) {


            if ($this->itemType instanceof StructureType && !$this->itemType->collection && !$this->itemType->haveItemByKey('_id')) {
                $this->itemType->addField('uuid', Shm::uuid());
            }



            $this->itemType->addUUIDInArray();
        }

        return $this;
    }


    public function childrenInAdmin(bool $inAdmin = true)
    {

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



                if (!$item->isInAdminSet()) {
                    $item->inAdmin($inAdmin);
                }
            }
        }

        if (isset($this->itemType)) {
            if (!$this->itemType->isInAdminSet()) {
                $this->itemType->inAdmin($inAdmin);
            }
        }



        return $this;
    }

    public function childrenEditable(bool $isEditable = true)
    {

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



                if (!$item->isEditableSet()) {
                    $item->editable($isEditable);
                }
            }
        }

        if (isset($this->itemType)) {
            if (!$this->itemType->isEditableSet()) {
                $this->itemType->editable($isEditable);
            }
        }



        return $this;
    }


    public function  editableThis(bool $editable = true): static
    {
        $this->editable = $editable;
        $this->editableSet = true;


        return $this;
    }



    /**
     * Set whether this field is editable.
     * If true, it will be editable.
     */
    public function editable(bool $isEditable = true): static
    {
        $this->editable = $isEditable;
        $this->editableSet = true;


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

                if (!$item->editableSet) {
                    $item->editable($isEditable);
                }
            }
        }

        if (isset($this->itemType)) {
            if (!$this->itemType->editableSet) {
                $this->itemType->editable($isEditable);
            }
        }



        return $this;
    }

    public function row(int $row): static
    {
        $this->row = $row;
        return $this;
    }

    /**
     * Set the column width for admin forms.
     * 12 = half-width, 24 = full-width.
     */
    public function col(int $col): static
    {
        $this->col = $col;

        return $this;
    }

    public function type(string $type): static
    {
        $this->type = $type;

        return $this;
    }


    /**
     * Mark the field as required or optional.
     */
    public function required(bool $isRequired = true): static
    {
        $this->required = $isRequired;
        return $this;
    }



    public function depthExpand(): BaseType | static
    {


        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                $this->items[$key] = $item->depthExpand();
            }
        }

        if (isset($this->itemType)) {
            $this->itemType = $this->itemType->depthExpand();
        }




        return $this;
    }



    public function hideDocuments(): void
    {

        if ($this instanceof IDsType || $this instanceof IDType) {
            $this->hide = true;
        }

        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                $item->hideDocuments();
            }
        }

        if (isset($this->itemType)) {
            $this->itemType->hideDocuments();
        }
    }


    public function fullRequired(bool $required = true): static
    {
        $this->required = $required;

        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                $item->fullRequired($required);
            }
        }

        if (isset($this->itemType)) {
            $this->itemType->fullRequired($required);
        }

        return $this;
    }



    /**
     * Mark the field as nullable or not.
     */
    public function nullable(bool $isNullable = true): static
    {
        $this->nullable = $isNullable;
        return $this;
    }

    public function noNullable(): static
    {
        $this->nullable = false;
        return $this;
    }

    public bool $defaultIsSet = false;

    /**
     * Set a default value or function.
     * Can accept either a value or a callable function (without parameters).
     * 
     * @param mixed $value The default value or a callable function
     */
    public function default(mixed $value): static
    {
        $this->defaultIsSet = true;
        $this->default = $value;
        return $this;
    }

    /**
     * Get the default value.
     * If default is a callable function, executes it and returns the result.
     * Otherwise, returns the stored value.
     * 
     * @return mixed The default value or the result of the default function
     */
    public function getDefault(): mixed
    {
        if (!$this->defaultIsSet) {
            return null;
        }

        if (is_callable($this->default)) {
            return call_user_func($this->default);
        }

        return $this->default;
    }


    /**
     * Clear the default value.
     * This will remove any previously set default.
     */
    public function cleanDefault(): static
    {
        $this->defaultIsSet = false;
        $this->default = null;
        return $this;
    }


    public function setTitle(null | string $title): static
    {


        return $this->title($title);
    }

    /**
     * Set a title for use in error messages.
     */
    public function title(null | string $title): static
    {
        $this->title = $title;

        return $this;
    }



    public bool $private = false;

    public function private(bool $private = true): static
    {
        $this->private = $private;
        return $this;
    }


    /**
     * Normalize the input value to the expected type.
     */
    public function normalizePrivate(mixed $value): mixed
    {

        if ($this->private) return null;



        if (isset($this->items)) {
            foreach ($this->items as $name => $type) {
                if (isset($value[$name])) {
                    $value[$name] =  $type->normalizePrivate($value[$name]);
                }
            }
        }

        if (isset($this->itemType)) {


            if ((is_array($value) || $value instanceof Traversable)) {




                foreach ($value as  $index => $valueItem) {
                    if (!$valueItem) {
                        continue;
                    }

                    $value[$index] = $this->itemType->normalizePrivate($valueItem);
                }
            }
        }


        return $value;
    }




    /**
     * Normalize the input value to the expected type.
     */
    public function normalize(mixed $value, $addDefaultValues = false, string | null $processId = null): mixed
    {

        if ($addDefaultValues && $value === null && $this->defaultIsSet) {
            return $this->getDefault();
        }

        return $value;
    }


    public function removeOtherItems(mixed $value): mixed
    {
        return $value;
    }


    /**
     * Validate the value. Should throw if invalid.
     */
    public function validate(mixed $value): void
    {
        if ($value === null) {
            if (!$this->nullable && $this->required) {
                $field = $this->title ?? 'Value';
                throw new \Exception("{$field} is required and cannot be null.");
            }
        }
    }






    //  public ?BaseType $filterType = null;

    public function filterType($safeMode = false): ?BaseType
    {
        return null;
    }
    public function filterToPipeline($filter, array | null $absolutePath = null): ?array
    {
        return null;
    }


    public function haveGetDisplayProjection(): bool
    {
        if ($this instanceof StructureType && isset($this->items)) {

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

                if ($item->haveGetDisplayProjection()) {
                    return true;
                }
            }
        }

        if ($this instanceof ArrayOfType && isset($this->itemType)) {
            if ($this->itemType->haveGetDisplayProjection()) {
                return true;
            }
        }

        if ($this->display) {
            return true;
        }

        return false;
    }





    /**
     * Преобразует строку/коллбек в предикат: fn($node): bool
     * @param string|callable $criteria
     * @return callable
     */
    private function makePredicate(string|callable $criteria): callable
    {
        if (is_string($criteria)) {
            $prop = $criteria;
            return static function ($node) use ($prop): bool {
                // безопасно проверяем свойство
                return property_exists($node, $prop) && (bool) $node->{$prop};
            };
        }

        if (is_callable($criteria)) {
            return $criteria;
        }

        throw new \InvalidArgumentException('Criteria must be string or callable.');
    }

    /**
     * Рекурсивно проверяет, есть ли true где-либо в узле/детях по критерию.
     * @param string|callable $criteria
     */
    public function hasTrueValueDeep(string|callable $criteria): bool
    {
        $pred = $this->makePredicate($criteria);

        if ($pred($this)) {
            return true;
        }

        if (isset($this->items)) {
            foreach ($this->items as $item) {
                if ($item->hasTrueValueDeep($pred)) {
                    return true;
                }
            }
        }

        if (isset($this->itemType) && $this->itemType->hasTrueValueDeep($pred)) {
            return true;
        }

        return false;
    }

    /**
     * Получает проекцию по типу или произвольному предикату.
     * Если передана строка — валидируем, как раньше.
     *
     * @param string|callable $criteria 'inAdmin'|'hide'|'display'|'inTable' или callable($node): bool
     * @param bool $childrenCalled
     * @return array
     */
    public function getProjection(string|callable $criteria, bool $childrenCalled = false): array
    {
        // Валидация только для строкового режима (сохраняем прежнее поведение)
        if (is_string($criteria)) {
            if (!in_array($criteria, ['inAdmin', 'hide', 'display', 'inTable'], true)) {
                throw new \LogicException('Invalid projection type: ' . $criteria);
            }
        }

        $pred = $this->makePredicate($criteria);

        if ($this->type === 'structure' && isset($this->items)) {
            if (!$pred($this) && !$this->hasTrueValueDeep($pred)) {
                return [];
            }

            $res = [];
            foreach ($this->items as $item) {
                $val = $item->getProjection($pred, true);

                if (!is_array($val)) {
                    throw new \LogicException('Projection must return an array for structure items. Key: ' . $item->key);
                }

                if (count($val) === 0) {
                    continue;
                }

                $res = array_merge($res, $val);
            }

            if (!$childrenCalled) {
                return $res;
            }

            return [$this->key => $res];
        }

        if ($this->type === 'array' && isset($this->itemType)) {
            if (!$pred($this) && !$this->hasTrueValueDeep($pred)) {
                return [];
            }

            $val = $this->itemType->getProjection($pred, true);

            if (!is_array($val)) {
                throw new \LogicException('Projection must return an array for array itemType. Key: ' . $this->key);
            }

            // if (!$childrenCalled) {
            //     return $val;
            // }

            return  $val;
        }

        if ($pred($this)) {
            return [$this->key => 1];
        }

        return [];
    }




    public function removeValuesByCriteria(string|callable $criteria, $values): mixed
    {
        $pred = $this->makePredicate($criteria);

        if ($this->type == 'structure' && isset($this->items)) {
            foreach ($this->items as $key => $item) {

                if ($pred($item)) {
                    unset($values[$key]);
                } else {
                    $values[$key] = $item->removeValuesByCriteria($pred, $values[$key] ?? null);
                }
            }
        }



        return $values;
    }





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




    public function tsType(): TSType
    {
        $TSType = new TSType('any');
        $TSType->comment($this->title);
        return $TSType;
    }

    public function tsInputType(): TSType
    {
        return $this->tsType();
    }



    public function updateKeys(null | string $rootKey = null)
    {

        if ($this->key === null && !$rootKey) {
            throw new \LogicException('Keys must be set before updating keys.');
        }

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

                $item->updateKeys($key);
            }
        }
        if (isset($this->itemType)) {
            if ($rootKey)
                $this->itemType->updateKeys($rootKey);
        }

        if ($rootKey)
            $this->keyIfNot($rootKey);
    }











    public function haveID(): bool
    {


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

                if ($item->haveID()) {
                    return true;
                }
            }
        }

        if (isset($this->itemType)) {
            if ($this->itemType->haveID()) {
                return true;
            }
        }

        return  false;
    }













    public $columnsWidth = null;

    public function width(int $width): static
    {
        $this->columnsWidth = $width;
        return $this;
    }


    public function setColumnsWidth(int $width): static
    {
        $this->columnsWidth = $width;
        return $this;
    }









    public function getKeysGraph(): array
    {
        if (!$this->key) {
            return ['->X'];
        }


        if (isset($this->items)) {

            $keys = [];
            foreach ($this->items as $key => $item) {
                $keys = [
                    $key => $item->getKeysGraph(),
                    ...$keys
                ];
            }
            return $keys;
        }

        if (isset($this->itemType)) {
            return [$this->key . "[]" => $this->itemType->getKeysGraph()];
        }



        return [$this->key];
    }


    private $onInsertEvent = null;


    private $onUpdateEvent = null;

    /**
     * Устанавливает обработчик события вставки или изменения значения.
     *
     * @param callable $handler Функция с сигнатурой function(array $_ids, mixed $newValue, array $docs)
     */
    public function insertOrUpdateEvent(callable $handler): static
    {
        $this->onInsertEvent = $handler;
        $this->onUpdateEvent = $handler;
        return $this;
    }


    /**
     * Устанавливает обработчик события изменения значения.
     *
     * @param callable $handler Функция с сигнатурой function(array $_ids, mixed $newValue, array $docs)
     */
    /* public function updateEvent(callable $handler): static
    {
        $this->onUpdateEvent = $handler;
        return $this;
    }*/

    private $onBeforeInsertEvent = null;
    private $onBeforeUpdateEvent = null;
    /**
     * Устанавливает обработчик события перед вставкой значения, должно возвращать значение.
     *
     * @param callable $handler Функция с сигнатурой function($value): mixed
     */
    public function beforeInsertEvent(callable $handler): static
    {
        $this->onBeforeInsertEvent = $handler;
        return $this;
    }

    /**
     * Устанавливает обработчик события перед изменением значения, должно возвращать значение.
     *
     * @param callable $handler Функция с сигнатурой function($value): mixed
     */
    public function beforeUpdateEvent(callable $handler): static
    {
        $this->onBeforeUpdateEvent = $handler;
        return $this;
    }


    /**
     * Устанавливает обработчик события перед вставкой или изменением значения, должно возвращать значение.
     *
     * @param callable $handler Функция с сигнатурой function($value): mixed
     */
    public function beforeInsertOrUpdateEvent(callable $handler): static
    {
        $this->onBeforeInsertEvent = $handler;
        $this->onBeforeUpdateEvent = $handler;
        return $this;
    }


    /**
     * Устанавливает обработчик события вставки значения.
     *
     * @param callable $handler Функция с сигнатурой function(array $_ids, mixed $newValue, array $docs)
     */
    public function insertEvent(callable $handler): static
    {
        $this->onInsertEvent = $handler;
        return $this;
    }


    public function haveBeforeInsertEvent(): bool
    {
        if (isset($this->items)) {

            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    if (is_callable($item->onBeforeInsertEvent)) {
                        return true;
                    }
                }
            }
        }

        if (is_callable($this->onBeforeInsertEvent)) {
            return true;
        }

        return false;
    }

    public function haveBeforeUpdateEvent(): bool
    {
        if (isset($this->items)) {

            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    if (is_callable($item->onBeforeUpdateEvent)) {
                        return true;
                    }
                }
            }
        }

        if (is_callable($this->onBeforeUpdateEvent)) {
            return true;
        }

        return false;
    }


    public function haveInsertEvent(): bool
    {
        if (isset($this->items)) {

            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    if (is_callable($item->onInsertEvent)) {
                        return true;
                    }
                }
            }
        }

        if (is_callable($this->onInsertEvent)) {
            return true;
        }

        return false;
    }


    public function haveUpdateEvent(): bool
    {
        if (isset($this->items)) {

            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    if (is_callable($item->onUpdateEvent)) {
                        return true;
                    }
                }
            }
        }

        if (is_callable($this->onUpdateEvent)) {
            return true;
        }

        return false;
    }


    private  function  groupChangedIdsByNewValue(array $newDocs, array $oldDocs, array $allNewDocs): array
    {
        $oldById = [];
        foreach ($oldDocs as $doc) {
            $oldById[(string)$doc['_id']] = $doc;
        }

        $grouped = [];

        $allNewDocsById = [];
        foreach ($allNewDocs as $doc) {
            $allNewDocsById[(string)$doc['_id']] = $doc;
        }

        foreach ($newDocs as $newDoc) {
            $idStr = (string)$newDoc['_id'];
            $newValue = $newDoc['_value'] ?? null;
            $oldValue = $oldById[$idStr]['_value'] ?? null;

            if ($newValue !== $oldValue) {
                $key = json_encode($newValue); // сериализуем для группировки
                if (!isset($grouped[$key])) {
                    $grouped[$key] = [
                        'new_value' => $newValue,
                        'ids' => []
                    ];
                }
                $grouped[$key]['ids'][] = $newDoc['_id'];
                if (isset($allNewDocsById[$idStr]))
                    $grouped[$key]['all_new_docs'][] = $allNewDocsById[$idStr];
            }
        }

        return array_values($grouped); // массив без сериализованных ключей
    }

    /**
     * Вызывает ранее установленный обработчик изменения значения.
     */
    public function callUpdateEvent($newDocs, $oldDocs, $allNewDocs): void
    {

        if (isset($this->items)) {

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

                if (in_array($key, ['_id',  'created_at', 'updated_at'])) {
                    continue;
                }


                if ($item instanceof BaseType) {


                    if ($item->haveUpdateEvent()) {



                        $_newDocs = array_map(function ($item) use ($key) {
                            return ['_value' => $item['_value'][$key], '_id' => $item['_id']];
                        }, array_filter($newDocs, function ($doc) use ($key) {
                            return isset($doc['_value'][$key]);
                        }));

                        $_oldDocs = array_map(function ($item) use ($key) {
                            return ['_value' => $item['_value'][$key], '_id' => $item['_id']];
                        }, array_filter($oldDocs, function ($doc) use ($key) {
                            return isset($doc['_value'][$key]);
                        }));



                        if (count($_newDocs) == 0) {
                            continue;
                        }

                        $item->callUpdateEvent($_newDocs, $_oldDocs, $allNewDocs);
                    }
                }
            }
        }

        if (is_callable($this->onUpdateEvent)) {

            $groupChangeds = ($this->groupChangedIdsByNewValue($newDocs, $oldDocs, $allNewDocs));

            foreach ($groupChangeds as $groupChanged) {
                $ids = $groupChanged['ids'];
                $newValue = $groupChanged['new_value'];
                $allNewDocs = $groupChanged['all_new_docs'] ?? [];


                call_user_func($this->onUpdateEvent, $ids, $newValue, $allNewDocs);
            }
        }
    }


    public function callBeforeInsertEvent($value): mixed
    {

        if (is_callable($this->onBeforeInsertEvent)) {
            return call_user_func($this->onBeforeInsertEvent, $value);
        }

        if (isset($this->items)) {

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

                if ($item instanceof BaseType) {
                    if ($item->haveBeforeInsertEvent() && isset($value[$key])) {
                        $value[$key] = $item->callBeforeInsertEvent($value[$key]);
                    }
                }
            }
        }

        return $value;
    }

    public function callBeforeUpdateEvent($value): mixed
    {

        if (is_callable($this->onBeforeUpdateEvent)) {
            return call_user_func($this->onBeforeUpdateEvent, $value);
        }

        if (isset($this->items)) {

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

                if ($item instanceof BaseType) {
                    if ($item->haveBeforeUpdateEvent() && isset($value[$key])) {
                        $value[$key] = $item->callBeforeUpdateEvent($value[$key]);
                    }
                }
            }
        }

        return $value;
    }

    public function callInsertEvent($newDocs, $allNewDocs): void
    {

        if (isset($this->items)) {

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

                if (in_array($key, ['_id',  'created_at', 'updated_at'])) {
                    continue;
                }

                if ($item instanceof BaseType) {

                    if ($item->haveInsertEvent()) {
                        $_newDocs = array_map(function ($item) use ($key) {
                            return ['_value' => $item['_value'][$key], '_id' => $item['_id']];
                        }, $newDocs);

                        $item->callInsertEvent($_newDocs, $allNewDocs);
                    }
                }
            }
        }

        if (is_callable($this->onInsertEvent)) {
            foreach ($newDocs as $doc) {
                call_user_func($this->onInsertEvent, [$doc['_id']], $doc['_value'], $allNewDocs);
            }
        }
    }



    public function createIndex($absolutePath = null): array
    {


        $result = [];

        if (isset($this->items)) {
            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    $result = [...$result,   ...$item->createIndex([...($absolutePath ?? []), $key])];
                }
            }
        }

        if (isset($this->itemType)) {
            $result = [
                ...$result,
                ...$this->itemType->createIndex([...($absolutePath ?? [])])
            ];
        }

        return $result;
    }

    private  function removeNullValues($data, array $onlyKeys = [])
    {

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

            //Если key содержит в начале $ то значит это агригация и ее не трогаем
            if (strpos($key, '$') === 0) {
                continue;
            }

            if ($onlyKeys && !in_array($key, $onlyKeys)) {


                if (is_array($val)) {

                    $valKeys = array_keys($val);


                    if (!array_intersect($valKeys, $onlyKeys)) {

                        unset($data[$key]);
                        continue;
                    }
                } else {
                    unset($data[$key]);
                    continue;
                }
            }


            if ($val === null || $val === false || $val == [] || $val == '' || $val == 0) {
                unset($data[$key]);
                continue;
            }
            if (is_array($val) || is_object($val)) {
                $data[$key] = $this->removeNullValues($val, $onlyKeys);
                if ($val === null || $val === false) {
                    unset($data[$key]);
                }
            }
        }

        return $data;
    }


    public function json()
    {
        $data = json_decode(json_encode([...get_object_vars($this)]), true);
        $data = $this->removeNullValues($data);

        return $data;
    }

    public function devJson()
    {


        $item = [
            'path' => $this->path,
            'items' => null,
            'itemValue' => null,
        ];


        if (isset($this->items)) {

            foreach ($this->items as $key => $val) {
                $item['items'][$key] =  $val->devJson();
            }
        }
        if (isset($this->itemType) && $this->itemType instanceof BaseType) {
            $item['itemValue'] =    $this->itemType->devJson();
        }


        return $item;
    }


    public function clone(): static
    {
        $clone = clone $this;

        if (isset($this->items)) {
            $clone->items = [];
            foreach ($this->items as $key => $item) {
                if ($item instanceof BaseType) {
                    $clone->items[$key] = $item->clone();
                } else {
                    $clone->items[$key] = $item;
                }
            }
        }
        if (isset($this->itemType) && $this->itemType instanceof BaseType) {
            $clone->itemType = $this->itemType->clone();
        }

        return $clone;
    }
}