<?php

namespace Shm\ShmBlueprints;

use InvalidArgumentException;
use Shm\ShmDB\mDB;
use Shm\Shm;
use Shm\ShmTypes\ArrayOfType;
use Shm\ShmTypes\StructureType;
use Shm\ShmTypes\UnixDateTimeType;
use Shm\ShmTypes\UnixDateType;

class ShmBlueprintQuery
{


    private StructureType $structure;

    private mixed $prepareArgsFunction = null;



    public $filter = true;

    public $sort = true;





    public $withoutData = false;


    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;
    }

    public function make()
    {




        if ($this->withoutData) {
            $dataType = Shm::arrayOf($this->structure);
        } else {

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

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





        $args = [

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


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

            'limit' => 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();

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


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



        $_this = $this;


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




                $_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 (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[] = [
                        '$match' => [
                            'search_string' => ['$regex' => mb_strtolower(trim($args['search'])), '$options' => 'i'],
                        ],
                    ];
                }



                $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'] ?? 20,
                ];


                if ($onlyHash) {

                    $pipeline[] = [
                        '$project' => [
                            '_id' => 1,
                            'updated_at' => 1,
                        ],
                    ];
                }






                $result = $structure->aggregate(
                    $pipeline,

                )->toArray();




                if ($onlyHash) {


                    return [
                        'hash' => mDB::hashDocuments($result),
                    ];
                }



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



                if ($withoutData) {

                    return $result;
                } else {






                    return [
                        'data' => $result,
                        'limit' => $args['limit'] ?? 20,
                        'offset' => $args['offset'] ?? 0,
                        'hash' =>  mDB::hashDocuments($result),
                        'total' => $total,
                    ];
                }
            },
        ];

        return $result;
    }
}
