<?php

namespace Lucent\Query;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Lucent\Database\Database;
use Lucent\Query\BuilderConverter\BuilderConverter;
use Lucent\Query\Filter\AndFilter;
use Lucent\Query\Filter\Argument;
use Lucent\Query\Filter\Filter;
use Lucent\Query\Filter\OrFilter;
use Lucent\Query\Operator\In;
use Lucent\Query\Operator\OperatorDetector;
use function explode;

final class FilterParser
{

    public function __construct(
        public Application      $app,
        public BuilderConverter $builderConverter,
        public OperatorDetector $operatorDetector,
    )
    {
    }

    /**
     * @param array $arguments
     * @return array<Argument>
     */
    private function formatArguments(array $arguments): array
    {
        return collect($arguments)->reduce(function ($c, $v, $k) {
            $c[] = $this->operatorDetector->detect($v, $k);
            return $c;
        }, []);
    }

    private function formatReferences(array $referenceArguments): Argument
    {
        $subqueries = collect($referenceArguments)->reduce(function ($c, $v, $k) {
            $keyWithoutRef = str_replace("children.", "", $k);
            [$field] = explode(".", $keyWithoutRef);
            $referenceField = str_replace($field . ".", "", $keyWithoutRef);
            $c[$field][$referenceField] = $v;
            return $c;
        }, []);


        $sourceIds = collect($subqueries)->reduce(function ($c, $subquery, $k) {

            $query = $this->app->make(Query::class);
            $graph = $query->filter($subquery)->run();

            if (!$graph->hasResults()) {
                return $c;
            }

            $targetIds = collect($graph->records)->pluck("id");
            $sourceIds = Database::make()->table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
            return array_merge($c, $sourceIds->toArray());
        }, []);

        return new Argument(
            field: "id",
            operator: new In(),
            value: $sourceIds
        );

    }

    private function separateMainFromReferenceArguments(Filter $arguments): array
    {
        return collect($arguments->toArray())->partition(function ($v, $k) {
            if (!str_starts_with($k, "children.")) {
                return true;
            }
            return false;
        })->toArray();
    }


    /**
     * @return array<Argument>
     */
    private function parseArguments(Filter $arguments): array
    {
        [$normalArguments, $referenceArguments] = $this->separateMainFromReferenceArguments($arguments);
        $formattedArguments = $this->formatArguments($normalArguments);
        if (!empty($referenceArguments)) {
            $formattedArguments[] = $this->formatReferences($referenceArguments);
        }

        return $formattedArguments;
    }


    public function parse(Builder $builder, Filter $filter): Builder
    {
        $arguments = $this->parseArguments($filter);
        return match (get_class($filter)) {
            AndFilter::class => $this->parseAnd($builder, $arguments),
            OrFilter::class => $this->parseOr($builder, $arguments),
        };
    }

    /**
     * @param array<Argument> $arguments
     */
    private function parseAnd(Builder $builder, array $arguments): Builder
    {
        return collect($arguments)
            ->reduce(fn($cBuilder, Argument $arg) => $this->builderConverter->for($arg)->toAndQueryBuilder($cBuilder), $builder);
    }

    /**
     * @param array<Argument> $arguments
     */
    private function parseOr(Builder $builder, array $arguments): Builder
    {
        return $builder->where(function (Builder $orBuilder) use ($arguments) {
            return collect($arguments)
                ->reduce(fn($cBuilder, Argument $arg) => $this->builderConverter->for($arg)->toOrQueryBuilder($cBuilder), $orBuilder);
        });
    }


}
