<?php

declare(strict_types=1);

namespace Builder\Core\Http\Controllers\Concerns;

use Builder\Core\Foundation\Contracts\Translatable;
use Builder\Core\Foundation\Definition\Field;
use Builder\Core\Foundation\Resources\ResourceDefinition;
use Builder\Core\Foundation\Tables\Column;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;

/**
 * Requires: HasResourceResolving (resolveContentLocale)
 */
trait HasIndexQuery
{
    /** @return Builder<Model> */
    protected function buildIndexQuery(Request $request, ResourceDefinition $definition): Builder
    {
        $model = $definition->getModel();
        $modelInstance = new $model;
        $table = $modelInstance->getTable();
        $isTranslatable = $modelInstance instanceof Translatable;
        $translatableNames = [];
        $transTable = null;

        $columnEagerLoad = [];

        foreach ($definition->getColumns() as $column) {
            $columnEagerLoad = array_merge($columnEagerLoad, $column->getEagerLoad());
        }

        $query = $model::with(array_merge($definition->getEagerLoad(), $columnEagerLoad));

        if ($isTranslatable) {
            $locale = $this->resolveContentLocale($request);
            $transTable = $modelInstance->getTranslationTable();
            $fk = $modelInstance->getTranslationForeignKey();
            $translatableNames = $definition->getTranslatableFieldNames();

            $query->getQuery()->leftJoin(
                $transTable,
                function (JoinClause $join) use ($transTable, $fk, $table, $locale): void {
                    $join->on("{$transTable}.{$fk}", '=', "{$table}.id")
                        ->where("{$transTable}.locale", '=', $locale);
                }
            );

            $noSelectNames = array_map(
                fn (Field $f): string => $f->getName(),
                array_filter(
                    $definition->getAllFields(),
                    fn (Field $f): bool => (bool) $f->getMeta('no_index_select', false),
                ),
            );
            $selectableNames = array_values(array_diff($translatableNames, $noSelectNames));

            $query->select(
                "{$table}.*",
                ...array_map(fn (string $c): string => "{$transTable}.{$c}", $selectableNames)
            );
        }

        $columnJoins = array_values(array_filter(
            $definition->getColumns(),
            fn (Column $c): bool => $c->getJoinConfig() !== null,
        ));

        if ($columnJoins !== []) {
            if (! $isTranslatable) {
                $query->select("{$table}.*");
            }

            foreach ($columnJoins as $column) {
                $jc = $column->getJoinConfig();
                assert($jc !== null);
                $joinMethod = $jc['type'] === 'inner' ? 'join' : 'leftJoin';

                $query->{$joinMethod}(
                    $jc['table'],
                    "{$table}.{$jc['foreignKey']}",
                    '=',
                    "{$jc['table']}.{$jc['ownerKey']}",
                );

                $query->addSelect("{$jc['table']}.{$jc['column']} as {$column->getKey()}");
            }
        }

        $search = $request->string('search')->value();
        $searchableColumns = array_filter(
            $definition->getColumns(),
            fn (Column $c): bool => $c->isSearchable(),
        );
        $searchableFields = $definition->getSearchable();
        $searchKeys = array_unique([
            ...array_map(fn (Column $c): string => $c->getKey(), array_values($searchableColumns)),
            ...$searchableFields,
        ]);

        if ($search && $searchKeys) {
            $query->where(function (Builder $q) use ($search, $searchKeys, $translatableNames, $transTable): void {
                foreach ($searchKeys as $key) {
                    if ($transTable && in_array($key, $translatableNames, true)) {
                        $q->orWhere("{$transTable}.{$key}", 'like', "%{$search}%");
                    } else {
                        $q->orWhere($key, 'like', "%{$search}%");
                    }
                }
            });
        }

        /** @var array<string, mixed> $bodyFilters */
        $bodyFilters = (array) $request->input('filters', []);

        foreach ($definition->getFilters() as $filter) {
            $value = $request->query($filter->getKey()) ?? ($bodyFilters[$filter->getKey()] ?? null);

            if ($value !== null && $value !== '') {
                $applyFn = $filter->getApply();
                if ($applyFn) {
                    $applyFn($query, $value);
                } else {
                    $query->where($filter->getKey(), $value);
                }
            }
        }

        $sortColumn = $request->query('column');
        $sortDirection = $request->query('direction', 'asc');

        if ($sortColumn) {
            $sortableColumn = null;
            foreach ($definition->getColumns() as $col) {
                if ($col->getKey() === $sortColumn && $col->isSortable()) {
                    $sortableColumn = $col;
                    break;
                }
            }

            if ($sortableColumn) {
                $direction = $sortDirection === 'desc' ? 'desc' : 'asc';

                if ($sortableColumn->getJoinConfig() !== null) {
                    $jc = $sortableColumn->getJoinConfig();
                    $query->orderBy("{$jc['table']}.{$jc['column']}", $direction);
                } elseif ($transTable && in_array($sortColumn, $translatableNames, true)) {
                    $query->orderBy("{$transTable}.{$sortColumn}", $direction);
                } else {
                    $query->orderBy($sortColumn, $direction);
                }
            }
        }

        return $query;
    }
}
