<?php

namespace Lucent\Query;

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Lucent\Edge\Edge;
use Lucent\Primitive\Collection;
use Lucent\Query\DatabaseGraph\DatabaseGraph;
use Lucent\Query\Filter\AndFilter;
use Lucent\Query\Filter\OrFilter;
use Lucent\Record\InputFormatter;
use Lucent\Record\QueryRecord;
use Lucent\Record\Record;

final  class Query
{

    /**
     * @var array<AndFilter> $filters
     */
    public array $filters;
    public QueryOptions $options;

    public function __construct(
        public readonly FilterParser   $filterParser,
        public readonly InputFormatter $inputFormatter,
        public readonly DatabaseGraph  $databaseGraph,
    )
    {
        $this->options = new QueryOptions();
    }

    public function filter(array $filterArguments): Query
    {
        $this->filters[] = new AndFilter($filterArguments);
        return $this;
    }

    public function orFilter(array $filterArguments): Query
    {
        $this->filters[] = new OrFilter($filterArguments);
        return $this;
    }


    public function run(): Graph
    {
        $resultsRecords = $this->mainQuery();
        $ids = array_map(function ($rec) {
            return $rec->id;
        }, $resultsRecords);

        $resultChildrenEdgesTargetIds = [];
        $resultChildrenEdges = [];
        if ($this->options->childrenDepth > 0 && !empty($ids)) {
            $resultChildrenEdges = $this->getChildren($ids);
            $resultChildrenEdgesTargetIds = array_map(fn($e) => $e->target, $resultChildrenEdges);
        }
        $resultParentSourceTargetIds = [];
        $resultParentEdges = [];
        if ($this->options->parentsDepth > 0 && !empty($ids)) {
            $resultParentEdges = $this->getParents($ids);
            $resultParentSourceTargetIds = array_map(fn($e) => $e->source, $resultParentEdges);
        }

        $edgesIds = collect($resultParentSourceTargetIds)->merge($resultChildrenEdgesTargetIds)->unique()->values()->toArray();
        $edgeRecords = [];
        if (!empty($edgesIds)) {
            $edgeRecords = DB::table('records')
                ->whereIn("id", $edgesIds)
                ->whereIn("status", $this->options->status)
                ->get()->toArray();
        }
        $resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray();
//        $resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
//            ->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
//            ->toArray();

        $formattedRecords = $this->formatRecords($resultsRecordsUnique, $resultChildrenEdges, $resultParentEdges);
        $this->reset();
        return $formattedRecords;

    }

    private function reset()
    {
        $this->options = new QueryOptions();
        $this->filters = [];
    }

    private function formatRecords(array $records, array $edges, array $parentEdges): Graph
    {
        $queryRecords = collect($records)->map(function ($recordData) {

            $record = Record::fromDB($recordData);
            $record->data = $this->inputFormatter->fill($record->schema, $record->data);
            $queryRecord = QueryRecord::fromRecord($record);
            $queryRecord->isRoot = data_get($recordData, "isRoot") === true;
            return $queryRecord;
        })->toArray();


        $queryEdges = collect($edges)->map(function ($edgeData) {

            return Edge::fromArray((array)$edgeData);

        })->sortBy("rank")->values()->toArray();

        $queryParentEdges = collect($parentEdges)->map(function ($edgeData) {


            return Edge::fromArray((array)$edgeData);

        })->sortBy("rank")->values()->toArray();



        return new Graph(
            new Collection($queryRecords),
            new Collection($queryEdges),
            new Collection($queryParentEdges),
            $this->options,
        );
    }


    public function tree(): Collection
    {
        return $this->run()->tree();
    }


    private function parseFilters(Builder $query): Builder
    {
        foreach ($this->filters as $filter) {
            $query = $this->filterParser->parse($query, $filter);
        }
        $query->whereIn("status", $this->options->status);
        return $query;
    }


    private function mainQuery(): array
    {
        $query = DB::table("records");
        $query = $this->parseFilters($query);
        if ($this->options->limit > 0) {
            $query->limit($this->options->limit);
            $query->offset($this->options->skip);
        }

        $query = $this->orderByQuery($query);

        return $query->get()->map(function ($r) {
            $r->isRoot = true;
            return $r;
        })->toArray();
    }

    private
    function getChildren(array $ids): array
    {
        return $this->databaseGraph->getChildren($ids, $this->options);
    }

    private function getParents(array $ids): array
    {
        return $this->databaseGraph->getParents($ids, $this->options);
    }


    public
    function runWithCount(): Graph
    {

        $query = DB::table("records");
        $query = $this->parseFilters($query);
        $graph = $this->run();
        $graph->total = $query->count();
        return $graph;
    }


    public function limit(int $limit): Query
    {
        $this->options->limit = $limit;
        return $this;
    }

    public
    function skip(int $skip): Query
    {
        $this->options->skip = $skip;
        return $this;
    }

    public function childrenDepth(int $depth): Query
    {
        $this->options->childrenDepth = $depth;
        return $this;
    }

    public function childrenLimit(int $limit): Query
    {
        $this->options->childrenLimit = $limit;
        return $this;
    }

    public function childrenFields(array $fields): Query
    {
        $this->options->childrenFields = $fields;
        return $this;
    }

    public function parentFields(array $fields): Query
    {
        $this->options->parentFields = $fields;
        return $this;
    }

    public function parentsDepth(int $depth): Query
    {
        $this->options->parentsDepth = $depth;
        return $this;
    }

    public function parentsLimit(int $limit): Query
    {
        $this->options->parentsLimit = $limit;
        return $this;
    }


    public function sort(string $sort): Query
    {
        $this->options->sort[] = $sort;
        return $this;
    }

    public function status(array $status): Query
    {
        $this->options->status = $status;
        return $this;
    }

    public
    function orderByQuery(Builder $query): Builder
    {
        foreach ($this->options->sort as $item) {
            $field = str_replace(".", "->", ltrim($item, '-'));
            $dir = str_starts_with($item, '-') ? "desc" : "asc";
            if ($field) {
                $query->orderBy($field, $dir);
            }
        }
        return $query;
    }
}
