<?php

namespace Lucent\Query;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Lucent\Query\Filter\AndFilter;
use Lucent\Query\Filter\Argument;
use Lucent\Query\Filter\Filter;
use Lucent\Query\Filter\OrFilter;

final class FilterParser
{

    public function __construct(public Application $app)
    {
    }


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

    private function formatArgument(mixed $value, string $filter): Argument
    {

        $operator = $this->detectOperator($filter);

        $field = $this->detectField($filter, $operator);
        $formattedValue = match ($operator) {
            "eq" => $this->formatText($value),
            "ne" => $this->formatText($value),
            "eqnum" => $this->formatNumber($value),
            "nenum" => $this->formatNumber($value),
            "object" => $this->formatText($value),
            "in" => $this->formatListString($value),
            "nin" => $this->formatListString($value),
            "innum" => $this->formatListNum($value),
            "ninnum" => $this->formatListNum($value),
            "eqtrue" => true,
            "eqfalse" => false,
            "netrue" => true,
            "nefalse" => false,
            "regex" => "%" . strtolower($value) . "%",
            "gt" => is_numeric($value) ? floatval($value) : $value,
            "gte" => is_numeric($value) ? floatval($value) : $value,
            "lt" => is_numeric($value) ? floatval($value) : $value,
            "lte" => is_numeric($value) ? floatval($value) : $value,
            "null" => null,
            "nnull" => null,
            "exists" => true,
            "nexists" => false,
            default => $value,
        };

        $matchedOperator = Operator::list()[$operator];
        return new Argument(
            field: str_replace(".", "->", $field),
            operator: $matchedOperator->db,
            value: $formattedValue
        );
    }

    private function formatText(string $value): string
    {
        return trim($value);
    }

    private function formatNumber(string $value): float
    {
        return floatval($value);
    }


    private function formatListString(mixed $value): array
    {
        if (is_string($value)) {
            $value = explode(",", $value);
        }

        return array_map(fn($v) => $this->formatText($v), $value);
    }

    private function formatListNum(mixed $value): array
    {
        if (\is_string($value)) {
            $value = explode(",", $value);
        }

        return \array_map(fn($v) => $this->formatNumber($v), $value);
    }


    private function detectOperator(string $filter): string
    {
        $exploded = \explode("_", $filter);
        $candidate = end($exploded);
        $operatorsListNames = collect(Operator::list())->map(fn($o) => $o->name)->toArray();

        if (\in_array($candidate, $operatorsListNames)) {
            return $candidate;
        }
        return 'eq';
    }

    private function detectField(string $filter, string $operator): string
    {

        $exploded = explode("_", $filter);
        $candidate = array_pop($exploded);
        if ($candidate === $operator) {
            return implode("_", $exploded);
        }
        return $filter;
    }


    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 = DB::table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
            return array_merge($c, $sourceIds->toArray());
        }, []);

        return new Argument(
            field: "id",
            operator: "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
    {
        foreach ($arguments as $argument) {
            if ($argument->operator == "in") {
                $builder->whereIn($argument->field, $argument->value);
            } else if ($argument->operator == "nin") {
                $builder->whereNotIn($argument->field, $argument->value);
            } else if ($argument->operator == "exists") {
                $builder->where($argument->field, "!=", "");
                $builder->where($argument->field, "!=", null);
            } elseif ($argument->operator == "filter") {
                $builder->whereJsonContains($argument->field, [$argument->value]);
                // target result
                // filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
                // $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
                // $query->whereJsonContains($filter["field"], [$objectFilters]);
            } else {
                $builder->where($argument->field, $argument->operator, $argument->value);
            }
        }

        return $builder;
    }

    /**
     * @param array<Argument> $arguments
     */
    private function parseOr(Builder $builder, array $arguments): Builder
    {
        $builder->where(function (Builder $orBuilder) use ($arguments) {
            foreach ($arguments as $argument) {
                if ($argument->operator == "in") {
                    $orBuilder->orWhereIn($argument->field, $argument->value);
                } else if ($argument->operator == "nin") {
                    $orBuilder->orWhereNotIn($argument->field, $argument->value);
                } else if ($argument->operator == "exists") {
                    $orBuilder->where($argument->field, "!=", "");
                    $orBuilder->where($argument->field, "!=", null);
                } elseif ($argument->operator == "filter") {
                    $orBuilder->whereJsonContains($argument->field, [$argument->value]);
                    // target result
                    // filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
                    // $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
                    // $query->whereJsonContains($filter["field"], [$objectFilters]);
                } else {
                    $orBuilder->orWhere($argument->field, $argument->operator, $argument->value);
                }
            }
        });
        return $builder;
    }


}
