<?php

declare(strict_types=1);

namespace App\Http\Api\Query\Base;

use App\Contracts\Http\Api\Field\SelectableField;
use App\Enums\Http\Api\Paging\PaginationStrategy;
use App\Http\Api\Field\Field;
use App\Http\Api\Filter\HasFilter;
use App\Http\Api\Query\ReadQuery;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Scope\ScopeParser;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\BaseResource;
use App\Scout\Elasticsearch\Elasticsearch;
use App\Scout\Search;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use RuntimeException;

/**
 * Class EloquentReadQuery.
 */
abstract class EloquentReadQuery extends ReadQuery
{
    /**
     * Prepare model for json resource.
     *
     * @param  Model  $model
     * @return BaseResource
     */
    public function show(Model $model): BaseResource
    {
        $constrainedEagerLoads = $this->constrainEagerLoads();

        return $this->resource($model->load($constrainedEagerLoads));
    }

    /**
     * Prepare models for resource collection.
     *
     * @return BaseCollection
     */
    public function index(): BaseCollection
    {
        $schema = $this->schema();

        // initialize builder, returning early if not resolved
        $builder = $this->builder();

        // eager load relations with constraints
        $builder->with($this->constrainEagerLoads());

        // select fields
        $fieldCriteria = $this->getFieldCriteria($schema->type());
        $selectedFields = collect($schema->fields())
            ->filter(fn (Field $field) => $field instanceof SelectableField && $field->shouldSelect($fieldCriteria))
            ->map(fn (Field $field) => $field->getColumn());
        $builder->select($builder->qualifyColumns($selectedFields->all()));

        // apply filters
        $scope = ScopeParser::parse($schema->type());
        foreach ($this->getFilterCriteria() as $criteria) {
            foreach ($schema->filters() as $filter) {
                if ($criteria->shouldFilter($filter, $scope)) {
                    $criteria->filter($builder, $filter, $this);
                }
            }
        }

        // special case: only apply HasFilter to top-level models
        if (! empty($schema->allowedIncludes())) {
            $hasFilter = new HasFilter($schema->allowedIncludes());
            foreach ($this->getFilterCriteria() as $criteria) {
                if ($criteria->shouldFilter($hasFilter, $scope)) {
                    $criteria->filter($builder, $hasFilter, $this);
                }
            }
        }

        // apply sorts
        foreach ($this->getSortCriteria() as $sortCriterion) {
            foreach ($schema->sorts() as $sort) {
                if ($sortCriterion->shouldSort($sort, $scope)) {
                    $sortCriterion->sort($builder, $sort);
                }
            }
        }

        // paginate
        $paginationCriteria = $this->getPagingCriteria(PaginationStrategy::OFFSET());
        $collection = $paginationCriteria !== null
            ? $paginationCriteria->paginate($builder)
            : $builder->get();

        // return paginated resource collection
        return $this->collection($collection);
    }

    /**
     * Constrain eager loads by binding callbacks that filter on the relations.
     *
     * @return array
     */
    public function constrainEagerLoads(): array
    {
        $constrainedEagerLoads = [];

        $schema = $this->schema();

        $includeCriteria = $this->getIncludeCriteria($schema->type());
        if ($includeCriteria === null) {
            return $constrainedEagerLoads;
        }

        foreach ($includeCriteria->getPaths() as $allowedIncludePath) {
            $relationSchema = $schema->relation($allowedIncludePath);
            if ($relationSchema === null) {
                throw new RuntimeException("Unknown relation '$allowedIncludePath' for type '{$schema->type()}'.");
            }

            $scope = ScopeParser::parse($allowedIncludePath);
            $constrainedEagerLoads[$allowedIncludePath] = function (Relation $relation) use ($scope, $relationSchema) {
                $relationBuilder = $relation->getQuery();

                // select fields
                $fieldCriteria = $this->getFieldCriteria($relationSchema->type());
                $selectedFields = collect($relationSchema->fields())
                    ->filter(fn (Field $field) => $field instanceof SelectableField && $field->shouldSelect($fieldCriteria))
                    ->map(fn (Field $field) => $field->getColumn());
                $relationBuilder->select($relationBuilder->qualifyColumns($selectedFields->all()));

                // apply filters
                foreach ($this->getFilterCriteria() as $criteria) {
                    foreach ($relationSchema->filters() as $filter) {
                        if ($criteria->shouldFilter($filter, $scope)) {
                            $criteria->filter($relationBuilder, $filter, $this);
                        }
                    }
                }

                // apply sorts
                foreach ($this->getSortCriteria() as $sortCriterion) {
                    foreach ($relationSchema->sorts() as $sort) {
                        if ($sortCriterion->shouldSort($sort, $scope)) {
                            $sortCriterion->sort($relationBuilder, $sort);
                        }
                    }
                }
            };
        }

        return $constrainedEagerLoads;
    }

    /**
     * Prepare models for sear.
     *
     * @param  PaginationStrategy  $paginationStrategy
     * @return BaseCollection
     *
     * @throws RuntimeException
     */
    public function search(PaginationStrategy $paginationStrategy): BaseCollection
    {
        $search = static::getSearch();

        if ($search !== null && $search->shouldSearch($this)) {
            return $search->search($this, $paginationStrategy);
        }

        // Let developer know why search can't be performed
        $driver = Config::get('scout.driver');
        $term = $this->getSearchCriteria()?->getTerm();
        throw new RuntimeException("Can't search for term '$term' with driver '$driver'. Please configure supported driver.");
    }

    /**
     * Get the search instance.
     *
     * @return Search|null
     */
    protected static function getSearch(): ?Search
    {
        return match (Config::get('scout.driver')) {
            'elastic' => App::make(Elasticsearch::class),
            default => null,
        };
    }

    /**
     * Get the resource schema.
     *
     * @return EloquentSchema
     */
    abstract public function schema(): EloquentSchema;

    /**
     * Get the query builder of the resource.
     *
     * @return Builder
     */
    abstract public function builder(): Builder;

    /**
     * Get the json resource.
     *
     * @param  mixed  $resource
     * @return BaseResource
     */
    abstract public function resource(mixed $resource): BaseResource;

    /**
     * Get the resource collection.
     *
     * @param  mixed  $resource
     * @return BaseCollection
     */
    abstract public function collection(mixed $resource): BaseCollection;
}
