<?php

namespace Shm\ShmUtils\ShmDoctor\ClassGenerator;

use Shm\ShmTypes\StructureType;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Shm\Collection\Collection;
use Shm\ShmUtils\ShmInit;
use Shm\ShmTypes\ArrayOfType;
use Shm\ShmTypes\CompositeTypes\VizType\VizType;
use Shm\ShmTypes\EnumType;
use Shm\ShmTypes\IDsType;
use Shm\ShmTypes\IDType;

use Shm\ShmUtils\ShmDoctor\Utils\CodeGenerator;

class FieldFileGenerator
{

    public $dir;
    public $namespace;
    public $structure;
    public $collectionClass;

    public function __construct() {}

    public function setDir($dir)
    {
        $this->dir = $dir;
    }
    public function setNamespace($namespace)
    {
        $this->namespace = $namespace;
    }
    public function setStructure($structure)
    {
        $this->structure = $structure;
    }
    public function setCollectionClass($collectionClass)
    {
        $this->collectionClass = $collectionClass;
    }

    /**
     * Generate field class for a structure
     */
    public  function generate(): void
    {

        if (!$this->structure) {
            throw new \Exception('Structure is not set');
            return;
        }
        if (!$this->dir) {
            throw new \Exception('Dir is not set');
            return;
        }
        if (!$this->namespace) {
            throw new \Exception('Namespace prefix is not set');
            return;
        }

        $this->structure->updateKeys();
        $className = ucfirst(str_replace([' ', '-', '_'], '', $this->structure->collection));

        $dir = $this->dir . '/';

        if (!is_dir($dir)) {
            mkdir($dir, 0777, true);
        }

        $classFullName = $className . 'Field';
        $class = new ClassType($classFullName);
        $class->setComment(self::fieldClassComment());

        $uses = ["use Shm\\ShmUtils\\DeepAccess;"];
        $this->addCollectionConstants($class, $uses);
        $this->addConstantByStructure($class, $this->structure);

        $filePath = $dir . $classFullName . '.php';
        $output = "<?php\n\nnamespace " . $this->namespace . ";\n\n";
        $output .= implode("\n", $uses) . "\n\n";
        $output .= $this->buildStructureComment($classFullName);
        $output .= $class . "\n\n";

        file_put_contents($filePath, $output);
    }

    /**
     * PHPDoc for generated Field classes — instructions for API/code agents.
     */
    private static function fieldClassComment(): string
    {
        return <<<'COMMENT'
Контракт строковых ключей полей. Не DTO, не коллекция, не значения полей.

Как выбирать константу:
1) Документ: filter, $set, $unset, sort, projection → константа без суффикса (полный путь с точками).
   updateOne([_ID => $id], ['$set' => [STATUS => $v, INIT_RESULT__PAYMENT_ID => $pid]])
2) Собрать вложенный Structure как PHP-массив → внутри родителя ключи `_KEY` (локальное имя).
   SEAT => [SEAT__NUMBER_KEY => 1, SEAT__ROW_KEY => 'A']
3) Собрать / прочитать элемент массива → `EL_{ARRAY}__{FIELD}` (путь от элемента, не от документа).
   '$push' => [CANCEL_RESULT => [EL_CANCEL_RESULT__REQUEST => $req]]
   foreach (NOTIFICATIONS_GET($doc) as $item) { EL_NOTIFICATIONS__UUID_GET($item); }
4) Mongo: «поле в любом элементе массива» → `{ARRAY}__IDX__{FIELD}` = 'array.field'.
   Только filter/aggregate. Запрещено с *_GET, DeepAccess и как ключ PHP-массива документа.

Чтение:
- `{CONST}_GET($data, $default)` — значение по пути. $default только если пути нет, а не когда в данных null.
- Вложенное поле нельзя читать как $data[INIT_RESULT__PAYMENT_ID]: в PHP нет ключа с точкой.
- EL_*_GET только от элемента массива. У IDX нет GET.
- ID-поле: GET возвращает ObjectId/скаляр, не подгруженный документ.

Запись:
- Ключи документа = полные пути (п.1). Ключи элемента / $push = EL_ (п.3).
- Не используйте *_GET для записи.

Не путать:
- COLLECTION_CLASS — PHP-класс коллекции (findOne, updateOne, insertOne).
- COLLECTION_NAME — строка-имя в MongoDB. Класс туда не подставлять.
- Константа поля — ключ. `{FIELD}_ENUM_{VALUE}` — значение. Пишите STATUS => STATUS_ENUM_ACTIVE, не STATUS => STATUS.
- Карта «Полная структура» над классом — справочник формы, не шаблон insert со всеми null.

Жёстко:
- Имена полей только константами этого класса, никогда строками ('status', 'members.owner').
- Нет константы → нет поля в схеме. Не выдумывайте имена и не склеивайте пути ('a.'.$x).
- Не берите константы из чужого *Field для документа этой коллекции.
- `__` в имени константы = точка в пути. `_KEY` / `EL_` / `IDX` / `_ENUM_` — разные роли, не взаимозаменяемы.

Файл генерируется автоматически. Не редактируйте вручную.
COMMENT;
    }

    private function addCollectionConstants(ClassType $class, array &$uses): void
    {
        if ($this->collectionClass) {
            $shortName = $this->collectionClass;
            $lastSlash = strrpos($this->collectionClass, '\\');

            if ($lastSlash !== false) {
                $collectionNs = substr($this->collectionClass, 0, $lastSlash);
                $shortName = substr($this->collectionClass, $lastSlash + 1);

                if ($collectionNs !== $this->namespace) {
                    $uses[] = 'use ' . $this->collectionClass . ';';
                }
            }

            $class->addConstant('COLLECTION_CLASS', new Literal($shortName . '::class'))
                ->setVisibility('public')
                ->addComment('Класс коллекции');
        }

        if ($this->structure->collection) {
            $class->addConstant('COLLECTION_NAME', $this->structure->collection)
                ->setVisibility('public')
                ->addComment('Имя коллекции');
        }
    }

    private function fieldConstName(string $namePrefix, string $key, bool $isArrayItem, string $suffix = ''): string
    {
        $constName = strtoupper(str_replace('.', '__', CodeGenerator::toSnakeCase($namePrefix) . CodeGenerator::toSnakeCase($key) . $suffix));

        if ($isArrayItem) {
            $constName = 'EL_' . $constName;
        }

        return $constName;
    }

    private function hasNamedFields($type): bool
    {
        if (!($type instanceof StructureType)) {
            return false;
        }

        foreach ($type->items as $key => $item) {
            if ($key === '*') {
                continue;
            }
            if ($item instanceof VizType) {
                continue;
            }
            return true;
        }

        return false;
    }

    private function buildStructureComment(string $className): string
    {
        $body = $this->structureMapLines($className, $this->structure);

        return "/*\nПолная структура\n[\n" . $body . "]\n*/\n\n";
    }

    private function structureMapLines(
        string $className,
        StructureType $structure,
        string $namePrefix = '',
        bool $isArrayItem = false,
        int $indent = 1
    ): string {
        $pad = str_repeat('    ', $indent);
        $out = '';

        foreach ($structure->items as $key => $item) {
            if ($key == '*') {
                continue;
            }
            if ($item instanceof VizType) {
                continue;
            }

            $constName = ($namePrefix !== '' && !$isArrayItem)
                ? $this->fieldConstName($namePrefix, $key, false, '_KEY')
                : $this->fieldConstName($namePrefix, $key, $isArrayItem);
            $ref = $className . '::' . $constName;

            if ($item instanceof ArrayOfType && $item->itemType instanceof StructureType) {
                $inner = $this->structureMapLines(
                    $className,
                    $item->itemType,
                    $namePrefix . $key . '.',
                    true,
                    $indent + 2
                );
                $pad1 = str_repeat('    ', $indent + 1);
                $out .= $pad . $ref . " => [\n";
                $out .= $pad1 . "[\n";
                $out .= $inner;
                $out .= $pad1 . "],\n";
                $out .= $pad . "],\n";
            } elseif ($item instanceof StructureType && $this->hasNamedFields($item)) {
                $inner = $this->structureMapLines(
                    $className,
                    $item,
                    $namePrefix . $key . '.',
                    false,
                    $indent + 1
                );
                $out .= $pad . $ref . " => [\n";
                $out .= $inner;
                $out .= $pad . "],\n";
            } else {
                $out .= $pad . $ref . " => null,\n";
            }
        }

        return $out;
    }

    /**
     * Add constants to class based on structure
     */
    private function addConstantByStructure(
        ClassType $class,
        StructureType $structure,
        string $namePrefix = '',
        string $valuePrefix = '',
        bool $isArrayItem = false,
        string $anyIndexNamePrefix = '',
        string $docPathPrefix = ''
    ): void {
        foreach ($structure->items as $key => $item) {
            if ($key == '*') {
                continue;
            }

            // Если это viz — пропускаем (настройка аналитики, не поле данных)
            if ($item instanceof VizType) {
                continue;
            }

            $comment = (string) ($item->title ?? $key);

            if ($item instanceof IDsType) {
                $comment .= " (ObjectId[])";
            } elseif ($item instanceof IDType) {
                $comment .= " (ObjectId)";
            }

            if ($item instanceof StructureType) {
                $comment .= " (Structure)";
            }
            if ($item instanceof ArrayOfType) {
                $comment .= " (Array)";
            }
            if ($item instanceof EnumType) {
                $comment .= " (Enum)";
            }

            $constName = $this->fieldConstName($namePrefix, $key, $isArrayItem);
            $constValue = $valuePrefix . $item->key;
            $constComment = $isArrayItem
                ? $comment . ' — ключ поля элемента массива'
                : $comment . ' полный пусть ключа';

            try {
                $class->addConstant($constName, $constValue)
                    ->setVisibility('public')
                    ->addComment($constComment);
            } catch (\Throwable $error) {
            }

            $method = $class->addMethod($constName . '_GET');
            $method->setStatic();
            $method->setVisibility('public');
            $method->addParameter('data');
            $method->addParameter('defaultValue')->setDefaultValue(null);
            $method->setBody('return DeepAccess::get($data, self::' . $constName . ', $defaultValue);');

            if ($isArrayItem) {
                $anyIndexConstName = $this->fieldConstName($anyIndexNamePrefix, $key, false);
                $anyIndexValue = $docPathPrefix . $item->key;

                try {
                    $class->addConstant($anyIndexConstName, $anyIndexValue)
                        ->setVisibility('public')
                        ->addComment($comment . ' — путь через массив (MongoDB IDX), не для GET');
                } catch (\Throwable $error) {
                }
            }

            if ($namePrefix && (!$isArrayItem || $valuePrefix !== '')) {
                $constNameOneKey = $this->fieldConstName($namePrefix, $key, false, '_KEY');

                try {
                    $class->addConstant($constNameOneKey, $item->key)
                        ->setVisibility('public')
                        ->addComment($comment . ' ключ');
                } catch (\Throwable $error) {
                }
            }

            // Обработка EnumType: создание констант для каждого значения
            if ($item instanceof EnumType) {
                $values = $item->values ?? [];

                $enumConstVALUES = $this->fieldConstName($namePrefix, $key, false, '_ENUM_ALL_KEYS_VALUES');

                try {
                    $class->addConstant($enumConstVALUES, $values)
                        ->setVisibility('public');
                } catch (\Throwable $error) {
                }

                foreach ($values as $enumKey => $enumValue) {
                    $enumConstSecondName = $this->fieldConstName($namePrefix, $key, false, '_ENUM_' . CodeGenerator::toSnakeCase($enumKey));
                    $enumConstValue = $enumKey;

                    try {
                        $class->addConstant($enumConstSecondName, $enumConstValue)
                            ->setVisibility('public')
                            ->setComment("Enum {$key}: {$enumValue}");
                    } catch (\Throwable $error) {
                    }
                }

                $enumConstSecondName = $this->fieldConstName($namePrefix, $key, false, '_ENUM_ALL_KEYS');
                $enumConstValue = array_keys($values);

                try {
                    $class->addConstant($enumConstSecondName, $enumConstValue)
                        ->setVisibility('public')
                        ->setComment("Enum {$key}");
                } catch (\Throwable $error) {
                }
            }

            if ($item instanceof StructureType) {
                self::addConstantByStructure(
                    $class,
                    $item,
                    $namePrefix . $key . '.',
                    $valuePrefix . $key . '.',
                    $isArrayItem,
                    $anyIndexNamePrefix . $key . '.',
                    $docPathPrefix . $key . '.'
                );
            }

            if ($item instanceof ArrayOfType && $item->itemType instanceof StructureType) {
                self::addConstantByStructure(
                    $class,
                    $item->itemType,
                    $namePrefix . $key . '.',
                    '',
                    true,
                    $anyIndexNamePrefix . $key . '.idx.',
                    $docPathPrefix . $key . '.'
                );
            }
        }
    }
}
