<?php

namespace Shm\ShmAddons\EventTicketAddon\Types\HallType;

use Shm\Shm;
use Shm\ShmDB\mDB;
use Shm\ShmRPC\ShmRPCCodeGen\TSType;
use Shm\ShmTypes\BaseType;
use Traversable;

class HallType extends BaseType
{
    public string $type = 'hall';


    public bool $compositeType = true;




    private static function isTempClientId(mixed $id): bool
    {
        return is_string($id) && !mDB::isMongoId($id);
    }

    /**
     * @return \MongoDB\BSON\ObjectId|null
     */
    private static function toMongoObjectId(mixed $id): ?\MongoDB\BSON\ObjectId
    {
        if ($id instanceof \MongoDB\BSON\ObjectId) {
            return $id;
        }

        $converted = mDB::id($id);

        return $converted instanceof \MongoDB\BSON\ObjectId ? $converted : null;
    }

    /**
     * Клиент всегда шлёт sector строкой (JSON). В Mongo это должен быть ObjectId:
     * временный tml_* — из карты после insert сектора, уже существующий — из самой строки.
     *
     * @param array<string, mixed> $tmpIdToMongoId
     */
    private static function resolveSectorObjectId(mixed $sectorRef, array $tmpIdToMongoId): ?\MongoDB\BSON\ObjectId
    {
        if (self::isTempClientId($sectorRef)) {
            return self::toMongoObjectId($tmpIdToMongoId[$sectorRef] ?? null);
        }

        return self::toMongoObjectId($sectorRef);
    }

    /**
     * @param array<string, mixed> $document
     * @return array{updateOne: array{0: array, 1: array}}
     */
    private static function bulkUpdateOneOp(array $document): array
    {
        $filter = mDB::replaceStringToObjectIds([HallElementsField::_ID => $document[HallElementsField::_ID]]);
        $set = $document;
        unset($set[HallElementsField::_ID]);

        return [
            'updateOne' => [
                $filter,
                ['$set' => $set],
            ],
        ];
    }

    /**
     * @param list<array{updateOne: array{0: array, 1: array}}> $operations
     */
    private static function flushBulkUpdates(array $operations): void
    {
        if ($operations === []) {
            return;
        }

        HallElements::_collection()->bulkWrite($operations);
    }


    public function tsType(): TSType
    {



        $TSType = new TSType('string');
        $TSType->comment($this->title);

        return $TSType;
    }



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



        // Пустое значение — зал не задан, сохранять нечего.
        if (!$values) return null;


        $isArray = is_array($values) || $values instanceof Traversable;

        // Если пришла уже сохранённая ссылка на зал (UUID-строка), нормализация не нужна —
        // элементы уже лежат в HallElements, просто пробрасываем идентификатор дальше.
        if (!$isArray && is_string($values)) {
            return $values;
        }




        // --- Дальше ожидаем массив элементов схемы зала (места, секторы, стены и т.д.) ---
        // Клиент шлёт полный снимок схемы: новые элементы с временными _id (tml_*),
        // существующие — с реальными Mongo ObjectId. Задача normalize:
        // 1) вставить новые, 
        // 2) обновить существующие, 
        // 3) удалить то, чего больше нет в снимке,
        // 4) вернуть hallUUID как значение поля (вместо массива элементов).

        $hallUUID = null;
        // Все элементы одного зала должны нести один и тот же hallUUID —
        // без него нельзя понять, к какому залу относятся документы и что чистить при sync.
        foreach ($values as $value) {
            if (!isset($value[HallElementsField::HALL_U_U_I_D])) {
                Shm::error('У элементов зала отсутствует идентификатор зала');
            }
            $hallUUID = $value[HallElementsField::HALL_U_U_I_D];
        }



        // Карта временных клиентских id → реальных Mongo ObjectId после insert.
        // Нужна, чтобы:
        // - связать места/столы с только что созданным сектором (поле sector),
        // - собрать итоговый список _id для удаления «лишних» документов.
        $tmpIdToMongoId = [];

        // ========== ШАГ 1: секторы первыми ==========
        // Секторы обрабатываем отдельно и раньше остальных, потому что другие элементы
        // (места и т.п.) ссылаются на сектор через поле sector. Если сектор новый
        // (id вида tml_...), его Mongo _id ещё неизвестен — сначала создаём сектор,
        // запоминаем соответствие tmp→mongo, и только потом пишем зависимые элементы.
        $sectorDocsToInsert = [];
        $sectorTmpIds = [];
        $sectorBulkUpdates = [];

        foreach ($values as $value) {
            $valueType = HallElementsField::TYPE_GET($value);
            if (!is_array($value) || !in_array($valueType, [HallElementsField::TYPE_ENUM_SECTOR, HallElementsField::TYPE_ENUM_SECTOR_POLYGON], true)) {
                continue;
            }

            if (self::isTempClientId($value[HallElementsField::_ID] ?? null)) {
                // Новый сектор: убираем временный _id — Mongo сам сгенерирует ObjectId.
                $doc = $value;
                unset($doc[HallElementsField::_ID]);
                $sectorDocsToInsert[] = $doc;
                $sectorTmpIds[] = $value[HallElementsField::_ID];
            } else {
                // Уже существующий сектор — готовим updateOne в bulk.
                $sectorBulkUpdates[] = self::bulkUpdateOneOp($value);
            }
        }

        // Массовая вставка новых секторов и фиксация tmpId → inserted ObjectId
        // (порядок insertMany совпадает с порядком sectorTmpIds).
        if ($sectorDocsToInsert !== []) {
            $insertResult = HallElements::_collection()->insertMany($sectorDocsToInsert);
            $insertedIds = $insertResult->getInsertedIds();
            foreach ($sectorTmpIds as $i => $tmpId) {
                $tmpIdToMongoId[$tmpId] = $insertedIds[$i];
            }
        }

        // Обновление уже существующих секторов одним bulkWrite.
        self::flushBulkUpdates($sectorBulkUpdates);

        // ========== ШАГ 2: остальные элементы (кроме sector и sector_polygon) ==========
        // sector_polygon пропускаем здесь намеренно (не пишем/не обновляем в этом проходе).
        // Для элементов с привязкой к сектору приводим sector к ObjectId (и tml_*, и уже существующий hex).
        $otherDocsToInsert = [];
        $otherTmpIds = [];
        $otherBulkUpdates = [];

        foreach ($values as $value) {

            $valueType = HallElementsField::TYPE_GET($value);
            if (!is_array($value) || in_array($valueType, [HallElementsField::TYPE_ENUM_SECTOR, HallElementsField::TYPE_ENUM_SECTOR_POLYGON], true)) {
                continue;
            }

            $doc = $value;
            // Место/стол ссылается на сектор строкой с клиента (tml_* или hex ObjectId).
            // После шага 1 временные id уже смаплены; существующие тоже нужно привести к ObjectId,
            // иначе в документе останется строка вместо _id.
            if (isset($doc[HallElementsField::SECTOR])) {
                $doc[HallElementsField::SECTOR] = self::resolveSectorObjectId(
                    $doc[HallElementsField::SECTOR],
                    $tmpIdToMongoId
                );
            }

            if (self::isTempClientId($doc[HallElementsField::_ID] ?? null)) {
                // Новый элемент схемы — insert без временного _id.
                $tmpKey = $doc[HallElementsField::_ID];
                unset($doc[HallElementsField::_ID]);
                $otherDocsToInsert[] = $doc;
                $otherTmpIds[] = $tmpKey;
            } else {
                // Существующий элемент — update по реальному _id.
                $otherBulkUpdates[] = self::bulkUpdateOneOp($doc);
            }
        }


        // Вставка новых элементов + расширение карты tmp→mongo (для финального sync).
        if ($otherDocsToInsert !== []) {
            $insertResult = HallElements::_collection()->insertMany($otherDocsToInsert);
            $insertedIds = $insertResult->getInsertedIds();
            foreach ($otherTmpIds as $i => $tmpId) {
                $tmpIdToMongoId[$tmpId] = $insertedIds[$i];
            }
        }

        self::flushBulkUpdates($otherBulkUpdates);

        // ========== ШАГ 3: полный список актуальных _id схемы ==========
        // Собираем Mongo ObjectId всех элементов из входящего снимка
        // (для новых — из карты tmpIdToMongoId, для старых — из самого _id).
        // Этот список — «истина» текущего состояния зала на клиенте.
        $allMongoIds = [];
        foreach ($values as $value) {
            if (!is_array($value) || !isset($value[HallElementsField::_ID])) {
                continue;
            }

            $clientId = $value[HallElementsField::_ID];
            if (self::isTempClientId($clientId)) {
                // Элемент был новым: если по какой-то причине не попал в insert — пропускаем.
                if (!isset($tmpIdToMongoId[$clientId])) {
                    continue;
                }
                $mongoId = self::toMongoObjectId($tmpIdToMongoId[$clientId]);
            } else {
                $mongoId = self::toMongoObjectId($clientId);
            }

            if ($mongoId !== null) {
                $allMongoIds[] = $mongoId;
            }
        }



        // ========== ШАГ 4: sync-удаление ==========
        // Всё, что есть в БД у этого hallUUID, но отсутствует во входящем снимке —
        // считается удалённым на клиенте и вычищается из HallElements.
        // $nin = «не из списка актуальных id».
        HallElements::_collection()->deleteMany([HallElementsField::HALL_U_U_I_D => $hallUUID, HallElementsField::_ID => ['$nin' => $allMongoIds]]);


        // В поле схемы сохраняем не массив элементов, а идентификатор зала.
        return  $hallUUID;
    }
}
