<?php

namespace Shm\ShmBlueprints;

use InvalidArgumentException;
use Shm\ShmDB\mDB;
use Shm\Shm;
use Shm\ShmRPC\ShmRPCUtils\ShmRPCContext;
use Shm\ShmTypes\ArrayOfType;
use Shm\ShmTypes\IDsType;
use Shm\ShmTypes\IDType;
use Shm\ShmTypes\StructureType;
use Shm\ShmTypes\UnixDateTimeType;
use Shm\ShmTypes\UnixDateType;
use Shm\ShmUtils\ShmManticore;

class ShmBlueprintQuery
{


    private StructureType $structure;

    private mixed $prepareArgsFunction = null;



    public $filter = true;

    public $sort = true;

    public $defaultLimit = 20;

    public $unlimited = false;



    public $groupByDateFieldName = null;

    public $groupByDateTimeFieldName = null;

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

    public function limit($limit = 20): static
    {
        $this->defaultLimit = $limit;
        return $this;
    }

    public function groupByDateTime($dateFieldName)
    {

        //Проверка на существование поля в структуре
        if (!$this->structure->haveItemByKey($dateFieldName)) {
            throw new \Exception("Field " . $dateFieldName . " not found in structure: " . $this->structure->key);
        }

        $this->groupByDateTimeFieldName = $dateFieldName;
        return $this;
    }


    public $groupByDateHideItems = false;

    public function groupByDate($dateFieldName, $hideItems = false)
    {

        //Проверка на существование поля в структуре
        if (!$this->structure->haveItemByKey($dateFieldName)) {
            throw new \Exception("Field " . $dateFieldName . " not found in structure: " . $this->structure->key);
        }

        $this->groupByDateFieldName = $dateFieldName;
        $this->groupByDateHideItems = $hideItems;
        return $this;
    }


    public $withoutData = false;

    public $byID = false;

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

    public function __construct(StructureType $structure)
    {
        $this->structure = $structure;
    }

    /**
     * @var callable|null
     */
    public $beforeQuery = null;

    /**
     * @var callable|null
     */
    public $afterQuery = null;

    /**
     * @var callable|null
     */


    private $pipelineFunction = null;



    /**
     * Set a callback to be executed before the mutation and can return new args
     * 
     * @param callable $callback Callback function that receives arguments and can return new arguments
     * @return static
     * @throws InvalidArgumentException If callback is not a callable
     */
    public function prepareArgs(callable $callback): static
    {
        $this->prepareArgsFunction = $callback;
        return $this;
    }


    /**
     * Execute the prepare args callback if set
     */
    public function callPrepareArgs(array &$args): void
    {
        if ($this->prepareArgsFunction !== null) {
            $_args = ($this->prepareArgsFunction)($args);
            if ($_args) {
                $args = $_args;
            }
        }
    }


    /**
     * Set the pipeline for database operations
     * 
     * @param array|callable|null $pipeline MongoDB aggregation pipeline or function returning pipeline
     * @throws InvalidArgumentException If pipeline is neither array nor callable
     */
    public function pipeline(array|callable|null $pipeline): static
    {
        if ($pipeline === null) {
            $this->pipelineFunction = null;
            return $this;
        }

        if (is_array($pipeline)) {
            // Convert array to function for consistency
            $this->pipelineFunction = fn() => $pipeline;
        } elseif (is_callable($pipeline)) {
            $this->pipelineFunction = $pipeline;
        } else {
            throw new InvalidArgumentException('Pipeline должен быть массивом или функцией');
        }

        return $this;
    }

    /**
     * Get the validated pipeline for database operations
     * 
     * @return array MongoDB aggregation pipeline
     */
    public function getPipeline(): array
    {
        if ($this->pipelineFunction === null) {
            return [];
        }

        $pipeline = ($this->pipelineFunction)();

        if (empty($pipeline)) {
            return [];
        }

        // Validate the pipeline structure
        mDB::validatePipeline($pipeline);

        return $pipeline;
    }


    /**
     * Set whether to enable filtering
     */
    public function filter(bool $filter = true): static
    {
        $this->filter = $filter;
        return $this;
    }

    /**
     * Set whether to enable sorting
     */
    public function sort(bool $sort = true): static
    {
        $this->sort = $sort;
        return $this;
    }



    /**
     * Set a callback to be executed before the query
     *
     * @param callable $beforeQuery Callback function that receives query arguments and can modify them
     * @return static
     */
    public function before(callable $beforeQuery): static
    {
        $this->beforeQuery = $beforeQuery;
        return $this;
    }

    /**
     * Set a callback to be executed after the query
     *
     * @param callable $afterQuery Callback function that receives query result and can modify it
     * @return static
     */
    public function after(callable $afterQuery): static
    {
        $this->afterQuery = $afterQuery;
        return $this;
    }



    /**
     * Set whether to return data without the actual content
     */
    public function withoutData(bool $withoutData = true): static
    {
        $this->withoutData = $withoutData;
        return $this;
    }


    private function dataType()
    {

        if ($this->byID) {
            return $this->structure;
        }



        if ($this->withoutData) {
            return Shm::arrayOf($this->structure);
        }

        if ($this->groupByDateFieldName) {
            //  $hideIDArg = true;


            return Shm::structure([
                'data' => Shm::arrayOf(Shm::structure([
                    'key' => Shm::string(),
                    'from' => Shm::unixdatetime(),
                    'to' => Shm::unixdatetime(),
                    'count' => Shm::int(),
                    'items' => $this->groupByDateHideItems ? null : Shm::arrayOf($this->structure),
                ])->staticBaseTypeName($this->structure->collection . 'ByDateItem')),
                'limit' => Shm::int(),

                'offset' => Shm::int(),
                'hash' => Shm::string(),
                'total' => Shm::int(),
            ])->key($this->structure->collection . 'ByDateData');
        }

        if ($this->groupByDateTimeFieldName) {


            return Shm::structure([
                'data' => Shm::arrayOf(Shm::structure([
                    'key' => Shm::string(),
                    'items' => Shm::arrayOf(Shm::structure([
                        'key' => Shm::string(),
                        'items' => Shm::arrayOf($this->structure),
                    ])->staticBaseTypeName($this->structure->collection . 'ByDateTimeItem'))
                ])->staticBaseTypeName($this->structure->collection . 'ByDateTime')),
                'limit' => Shm::int(),

                'offset' => Shm::int(),
                'hash' => Shm::string(),
                'total' => Shm::int(),
            ])->key($this->structure->collection . 'ByDateTimeData');
        }



        return Shm::structure([
            'data' => Shm::arrayOf($this->structure),
            'limit' => Shm::int(),

            'offset' => Shm::int(),
            'hash' => Shm::string(),
            'total' => Shm::int(),
        ])->key($this->structure->key . 'Data');
    }

    private function argsType()
    {


        if ($this->byID) {
            return [
                '_id' => Shm::nonNull(Shm::ID()),
            ];
        }

        $hideIDArg = $this->groupByDateFieldName || $this->groupByDateTimeFieldName;

        $args = [

            "_id" => $hideIDArg ? null : Shm::ID()->default(null),


            'onlyHash' => $this->withoutData ? null : Shm::boolean()->default(false),

            'limit' => $this->unlimited ? null : Shm::int()->default(30),
            'sample' => Shm::boolean(),
            'offset' =>  Shm::int()->default(0),
            'all' => Shm::boolean()->default(false),
            'search' => Shm::string()->default(''),
            'sort' => Shm::structure([
                'direction' => Shm::enum([
                    'ASC' => 'По возрастанию',
                    'DESC' => 'По убыванию',
                ])->default('DESC'),
                'field' => Shm::string(),
            ])
        ];



        $filter = $this->structure->filterType()->fullCleanDefault();
        $extensionsFilter = $this->structure->extensionsFilterType();


        if ($filter) {
            $args['filter'] = $filter;
        }

        if ($extensionsFilter) {


            $args['extensionsFilter'] = $extensionsFilter;
        }


        return $args;
    }


    private function resolveFunction($root, $args)
    {





        $pipeline = $this->getPipeline();


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


        //Получение данных по ID
        if ($this->byID) {
            $pipeline = [
                ...$pipeline,
                [
                    '$match' => [
                        "_id" => mDB::id($args['_id']),
                    ],
                ],
            ];

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

            return $result;
        }

        if (isset($args['extensionsFilter'])) {


            foreach ($args['extensionsFilter'] as $extensionCollection => $extensionFilter) {





                //Находим ключи для загрузки вложенных данных
                $extensionItems = $this->structure->findItemsByCondition(function ($item) use ($extensionCollection) {




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



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





                if (count($extensionItems) > 0) {

                    $extensionDocument = $extensionItems[0]->getDocument();

                    $paths = array_map(function ($item) {
                        if ($item instanceof IDsType || $item instanceof IDType) {
                            return $item->getPathString();
                        }
                    }, $extensionItems);

                    $extensionFilter = $extensionDocument->filterToPipeline($extensionFilter);

                    $ids = $extensionDocument->aggregate([
                        ...$extensionFilter,
                        [
                            '$group' => [
                                '_id' => null,
                                'ids' => ['$push' => '$_id'],
                            ],
                        ],
                    ])->toArray()[0]['ids'] ?? [];


                    $or = [];
                    foreach ($paths as $path) {
                        $or[] = [
                            $path => ['$in' => $ids],

                        ];
                    }

                    if (count($or) > 0) {

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

                            $pipeline[] = [
                                '$match' => [
                                    '$or' => $or,
                                ],
                            ];
                        }
                    }
                }
            }
        }


        if (isset($args['filter'])) {

            $pipelineFilter =  $this->structure->filterToPipeline($args['filter']);


            if ($pipelineFilter) {

                $pipeline = [
                    ...$pipeline,
                    ...$pipelineFilter,
                ];
            }
        };


        //Получение данных по ID
        if (isset($args['_id'])) {

            $pipeline = [
                ...$pipeline,
                [
                    '$match' => [
                        "_id" => mDB::id($args['_id']),
                    ],
                ],
                [
                    '$limit' => 1
                ],
            ];

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

            if ($this->withoutData) {
                return $result ? [$result] : [];
            }

            return [
                'data' => $result ? [$result] : []
            ];
        }




        if (isset($args['search']) && trim($args['search']) !== '') {
            $pipeline[] = ShmManticore::getPipeline($this->structure->collection, mb_strtolower(trim($args['search'])));
        }





        $pipelineData = [];

        $sortField = $args['sort']['field'] ?? null;
        $sortDirection = $args['sort']['direction'] ?? null;


        if ($sortField && $sortDirection) {

            $pipelineData[] = [
                '$sort' => [
                    $sortField => $sortDirection == "DESC" ? -1 : 1,
                ],
            ];
        } else {

            //Проверка нет ли в $pipeline sort
            $hasSort = false;
            foreach ($pipeline as $stage) {
                if (isset($stage['$sort'])) {
                    $hasSort = true;
                    break;
                }
            }



            if (!$hasSort) {

                if ($this->structure->manualSort) {
                    $pipelineData[] = [
                        '$sort' => [
                            "_sortWeight" => -1,

                        ],
                    ];
                } else {


                    $pipelineData[] = [
                        '$sort' => [
                            "_id" => -1,
                        ],


                    ];
                }
            }
        }

        if (isset($args['offset']) && $args['offset'] > 0) {
            $pipelineData[] = [
                '$skip' => $args['offset'],
            ];
        }

        if (!$this->unlimited) {
            $pipelineData[] =
                [
                    '$limit' => $args['limit'] ?? $this->defaultLimit,
                ];
        }




        //Считываем только хеш и выходим
        if (isset($args['onlyHash']) && $args['onlyHash']) {

            $resultHash = $this->structure->aggregateHash([...$pipeline, ...$pipelineData]);


            return [
                'hash' => $resultHash
            ];
        }


        if ($this->groupByDateFieldName) {

            $pipelineData[] = [
                '$match' => [
                    $this->groupByDateFieldName => ['$ne' => null],
                ]
            ];
            $pipelineData[] = [
                '$addFields' => [
                    'dateFormatted' => [
                        '$dateToString' => [
                            'format' => '%Y-%m-%d',
                            'date' => [
                                '$toDate' => [
                                    '$multiply' => ['$' . $this->groupByDateFieldName, 1000]
                                ]
                            ]
                        ]
                    ]
                ]
            ];

            $group = [
                '_id' => '$dateFormatted',
                'key' => ['$first' => '$dateFormatted'],
                'from' => ['$min' => '$' . $this->groupByDateFieldName],
                'to' => ['$max' => '$' . $this->groupByDateFieldName],

                'count' => ['$sum' => 1],
            ];

            if (!$this->groupByDateHideItems) {
                $group['items'] = ['$push' => '$$ROOT'];
            }

            $pipelineData[] = [
                '$group' => $group
            ];
            $pipelineData[] = [
                '$sort' => [
                    '_id' => 1
                ]
            ];
        }


        if ($this->groupByDateTimeFieldName) {

            $pipelineData[] = [
                '$match' => [
                    $this->groupByDateTimeFieldName => ['$ne' => null],
                ]
            ];
            $pipelineData[] =   [
                '$addFields' => [
                    'dateObj' => [
                        '$toDate' => [
                            '$multiply' => ['$' . $this->groupByDateTimeFieldName, 1000] // если секунды
                        ]
                    ]
                ]
            ];
            $pipelineData[] = [
                '$addFields' => [
                    'dateStr' => [
                        '$dateToString' => [
                            'format' => '%Y-%m-%d',
                            'date' => '$dateObj'
                        ]
                    ],
                    'hourStr' => [
                        '$dateToString' => [
                            'format' => '%H:00',
                            'date' => '$dateObj'
                        ]
                    ]
                ]
            ];

            // 2. Группировка по дате + часу
            $pipelineData[] = [
                '$group' => [
                    '_id' => [
                        'date' => '$dateStr',
                        'hour' => '$hourStr'
                    ],
                    'items' => [
                        '$push' => '$$ROOT'
                    ]
                ]
            ];

            // Сортировка по дате и часу
            $pipelineData[] = [
                '$sort' => [
                    '_id.date' => 1,
                    '_id.hour' => 1
                ]
            ];


            // 3. Группировка по дате
            $pipelineData[] = [
                '$group' => [
                    '_id' => '$_id.date',
                    'items' => [
                        '$push' => [
                            'key' => '$_id.hour',
                            'items' => '$items'
                        ]
                    ]
                ]
            ];

            //Сортировка по дате
            $pipelineData[] = [
                '$sort' => [
                    '_id' => 1
                ]
            ];

            // 4. Финальный формат
            $pipelineData[] = [
                '$project' => [
                    '_id' => 0,
                    'key' => '$_id',
                    'items' => 1
                ]
            ];
        }





        $result = $this->structure->aggregate([...$pipeline, [
            '$facet' => [
                'data' => $pipelineData,
                'total' => [
                    [
                        '$count' => 'total',
                    ],
                ]
            ]

        ]])->toArray()[0] ?? [];

        $data = $result['data'] ?? [];
        $total = $result['total'][0]['total'] ?? 0;


        if ($this->withoutData) {

            return $data;
        } else {

            $resultHash = $this->structure->aggregateHash([...$pipeline, ...$pipelineData]);

            return [
                'data' => $data,
                'limit' => $args['limit'] ?? $this->defaultLimit,
                'offset' => $args['offset'] ?? 0,
                'hash' =>  $resultHash,
                'total' => $total,
            ];
        }
    }

    public function make()
    {




        $dataType = $this->dataType();









        $argsStructure = Shm::structure($this->argsType());





        $_this = $this;


        $result = [
            'type' => $dataType,
            'args' =>  $argsStructure,
            'resolve' => function ($root, $args) use ($_this) {

                if (!$_this->structure->collection) {
                    throw new \Exception("Collection not defined for structure: " . $_this->structure->key);
                }

                $_this->callPrepareArgs($args);


                if ($root instanceof ShmRPCContext && $root->args) {
                    $args = $root->args->removeOtherItems($args);
                }

                return $_this->resolveFunction($root, $args);

                /*  $hideIDArg =  !$dataType->haveItemByKey('_id');

                $withoutData = $_this->withoutData;
                $structure = $_this->structure;


                $_this->callPrepareArgs($args);

                $pipeline = $_this->getPipeline();





                $onlyHash = $args['onlyHash'] ?? false;

                $args =  $argsStructure->normalize($args);


                if (!$structure->collection) {
                    throw new \Exception("Collection not defined for structure: " . $structure->key);
                }


                $pipeline = [
                    ...$pipeline,
                    ...$structure->getPipeline()
                ];



                if ($_this->byID) {
                    $pipeline = [
                        ...$pipeline,
                        [
                            '$match' => [
                                "_id" => mDB::id($args['_id']),
                            ],
                        ],
                    ];

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

                    return $result;
                }



                if (! $hideIDArg && isset($args['_id'])) {

                    $pipeline = [
                        ...$pipeline,
                        [
                            '$match' => [
                                "_id" => mDB::id($args['_id']),
                            ],
                        ],
                        [
                            '$limit' => 1
                        ],
                    ];

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


                    if (!$result) {
                        return $withoutData ? [] : [
                            'data' => [],
                        ];
                    } else {

                        $result = $structure->normalize($result);
                        return $withoutData ? [$result] : [
                            'data' => [$result],
                        ];
                    }
                }


                if (isset($args['filter'])) {


                    $pipelineFilter =  $structure->filterToPipeline($args['filter']);



                    if ($pipelineFilter) {

                        $pipeline = [
                            ...$pipeline,
                            ...$pipelineFilter,
                        ];
                    }
                };




                if (isset($args['search']) && trim($args['search']) !== '') {

                    $pipeline[] = ShmManticore::getPipeline($structure->collection, mb_strtolower(trim($args['search'])));
                }



                $total = 0;
                if (!$withoutData && !$onlyHash) {
                    $total =  $structure->aggregate([
                        ...$pipeline,
                        [
                            '$count' => 'total',
                        ],
                    ])->toArray()[0]['total'] ?? 0;
                }





                $_limit = $args['limit'] ?? null;


                if (!$withoutData && $_limit === 0) {
                    return [
                        'data' => [],
                        'limit' => 0,
                        'offset' => 0,

                        'total' => $total,
                    ];
                }



                $sortField = $args['sort']['field'] ?? null;
                $sortDirection = $args['sort']['direction'] ?? null;


                if ($sortField && $sortDirection) {

                    $pipeline[] = [
                        '$sort' => [
                            $sortField => $sortDirection == "DESC" ? -1 : 1,
                        ],
                    ];
                } else {


                    //Проверка нет ли в $pipeline sort
                    $hasSort = false;
                    foreach ($pipeline as $stage) {
                        if (isset($stage['$sort'])) {
                            $hasSort = true;
                            break;
                        }
                    }

                    if (!$hasSort) {

                        if ($structure->manualSort) {

                            $pipeline[] = [
                                '$sort' => [
                                    "_sortWeight" => -1,

                                ],
                            ];
                        } else {

                            $pipeline[] = [
                                '$sort' => [
                                    "_id" => -1,
                                ],
                            ];
                        }
                    }
                }

                if (isset($args['offset']) && $args['offset'] > 0) {

                    $pipeline[] = [
                        '$skip' => $args['offset'],
                    ];
                }




                $pipeline[] = [
                    '$limit' => $args['limit'] ?? $_this->defaultLimit,
                ];










                $resultHash = $structure->aggregateHash($pipeline);






                if ($onlyHash) {



                    return [
                        'hash' => $resultHash
                    ];
                }


                if ($_this->groupByDateFieldName) {
                    $pipeline[] = [

                        '$match' => [
                            $_this->groupByDateFieldName => ['$ne' => null],
                        ]
                    ];
                    $pipeline[] = [
                        '$addFields' => [
                            'dateFormatted' => [
                                '$dateToString' => [
                                    'format' => '%Y-%m-%d',
                                    'date' => [
                                        '$toDate' => [
                                            '$multiply' => ['$' . $_this->groupByDateFieldName, 1000]
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ];

                    $group = [
                        '_id' => '$dateFormatted',
                        'key' => ['$first' => '$dateFormatted'],
                        'from' => ['$min' => '$' . $_this->groupByDateFieldName],
                        'to' => ['$max' => '$' . $_this->groupByDateFieldName],

                    ];

                    if (!$_this->groupByDateHideItems) {
                        $group['items'] = ['$push' => '$$ROOT'];
                    }

                    $pipeline[] = [
                        '$group' => $group
                    ];
                    $pipeline[] = [
                        '$sort' => [
                            '_id' => 1
                        ]
                    ];
                }


                if ($_this->groupByDateTimeFieldName) {

                    $pipeline[] = [
                        '$match' => [
                            $_this->groupByDateTimeFieldName => ['$ne' => null],
                        ]
                    ];
                    $pipeline[] =   [
                        '$addFields' => [
                            'dateObj' => [
                                '$toDate' => [
                                    '$multiply' => ['$' . $_this->groupByDateTimeFieldName, 1000] // если секунды
                                ]
                            ]
                        ]
                    ];
                    $pipeline[] = [
                        '$addFields' => [
                            'dateStr' => [
                                '$dateToString' => [
                                    'format' => '%Y-%m-%d',
                                    'date' => '$dateObj'
                                ]
                            ],
                            'hourStr' => [
                                '$dateToString' => [
                                    'format' => '%H:00',
                                    'date' => '$dateObj'
                                ]
                            ]
                        ]
                    ];

                    // 2. Группировка по дате + часу
                    $pipeline[] = [
                        '$group' => [
                            '_id' => [
                                'date' => '$dateStr',
                                'hour' => '$hourStr'
                            ],
                            'items' => [
                                '$push' => '$$ROOT'
                            ]
                        ]
                    ];

                    // Сортировка по дате и часу
                    $pipeline[] = [
                        '$sort' => [
                            '_id.date' => 1,
                            '_id.hour' => 1
                        ]
                    ];


                    // 3. Группировка по дате
                    $pipeline[] = [
                        '$group' => [
                            '_id' => '$_id.date',
                            'items' => [
                                '$push' => [
                                    'key' => '$_id.hour',
                                    'items' => '$items'
                                ]
                            ]
                        ]
                    ];

                    //Сортировка по дате
                    $pipeline[] = [
                        '$sort' => [
                            '_id' => 1
                        ]
                    ];

                    // 4. Финальный формат
                    $pipeline[] = [
                        '$project' => [
                            '_id' => 0,
                            'key' => '$_id',
                            'items' => 1
                        ]
                    ];
                }


                // echo json_encode($pipeline, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
                // exit;


                $result = $structure->aggregate($pipeline)->toArray();






                if ($withoutData) {
                    $result =   Shm::arrayOf($structure)->normalize($result);


                    return $result;
                } else {

                    $result =   $dataType->items['data']->normalize($result);





                    return [
                        'data' => $result,
                        'limit' => $args['limit'] ?? $_this->defaultLimit,
                        'offset' => $args['offset'] ?? 0,
                        'hash' =>  $resultHash,
                        'total' => $total,
                    ];
                }*/
            },
        ];

        return $result;
    }
}
