<?php

declare(strict_types=1);

namespace Builder\Core\Http\Controllers;

use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Builder\Core\Foundation\Contracts\CommandPaletteSearchable;
use Builder\Core\Foundation\Contracts\Translatable;
use Builder\Core\Foundation\Registry;
use Builder\Core\Foundation\Resources\ResourceDefinition;
use Builder\Core\Foundation\Tables\Column;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class GlobalSearchController
{
    private const int MIN_QUERY_LENGTH = 2;

    public function __invoke(Request $request, Registry $registry): JsonResponse
    {
        $query = trim((string) $request->query('q', ''));

        if (strlen($query) < self::MIN_QUERY_LENGTH) {
            return response()->json(['groups' => []]);
        }

        /** @var BuilderAccessible $user */
        $user = $request->user();
        $permissions = $user->builderPermissions();

        $groups = collect($registry->resources())
            ->filter(fn (string $resourceClass) => is_a($resourceClass, CommandPaletteSearchable::class, true))
            ->mapWithKeys(fn (string $resourceClass, string $slug) => [
                $slug => [$resourceClass, $registry->resolveResource($resourceClass)],
            ])
            ->filter(fn (array $pair) => $this->userCanRead($permissions, $pair[1]))
            ->map(function (array $pair, string $slug) use ($query): ?array {
                [$resourceClass, $definition] = $pair;
                $searchKeys = $this->resolveSearchKeys($definition);

                if ($searchKeys === []) {
                    return null;
                }

                $records = rescue(fn () => $this->runSearch($definition, $query, $searchKeys), report: false);

                if ($records === null || $records->isEmpty()) {
                    return null;
                }

                $resource = new $resourceClass;

                return [
                    'label' => $definition->getLabel(),
                    'slug' => $slug,
                    'items' => $records->map(fn (Model $record): array => [
                        'id' => $record->getKey(),
                        'title' => $resource->searchResultTitle($record),
                        'subtitle' => $resource->searchResultSubtitle($record),
                        'route' => $this->buildRoute($slug, $record),
                    ])->values()->all(),
                ];
            })
            ->filter()
            ->values()
            ->all();

        return response()->json(['groups' => $groups]);
    }

    /** @return list<string> */
    private function resolveSearchKeys(ResourceDefinition $definition): array
    {
        return array_values(
            collect($definition->getColumns())
                ->filter(fn (Column $c): bool => $c->isSearchable())
                ->map(fn (Column $c): string => $c->getKey())
                ->merge($definition->getSearchable())
                ->unique()
                ->values()
                ->all(),
        );
    }

    /**
     * @param  list<string>  $searchKeys
     * @return Collection<int, Model>
     */
    private function runSearch(ResourceDefinition $definition, string $query, array $searchKeys): Collection
    {
        $model = $definition->getModel();
        $modelInstance = new $model;
        $table = $modelInstance->getTable();
        $isTranslatable = $modelInstance instanceof Translatable;

        $builder = $model::query();

        $transTable = null;
        $fk = null;
        $translatableNames = [];

        if ($isTranslatable) {
            $transTable = $modelInstance->getTranslationTable();
            $fk = $modelInstance->getTranslationForeignKey();
            $translatableNames = $modelInstance->getTranslatableAttributes();
        }

        $builder->where(function (Builder $q) use ($query, $searchKeys, $table, $transTable, $fk, $translatableNames): void {
            foreach ($searchKeys as $key) {
                if ($transTable && $fk && in_array($key, $translatableNames, true)) {
                    $q->orWhereExists(
                        function (QueryBuilder $sub) use ($transTable, $fk, $table, $key, $query): void {
                            $sub->selectRaw('1')
                                ->from($transTable)
                                ->whereColumn("{$transTable}.{$fk}", "{$table}.id")
                                ->where("{$transTable}.{$key}", 'like', "%{$query}%");
                        }
                    );
                } else {
                    $q->orWhere("{$table}.{$key}", 'like', "%{$query}%");
                }
            }
        });

        $maxResults = (int) config('builder.search.max_results_per_resource', 5);

        /** @var Collection<int, Model> $records */
        $records = $builder->limit($maxResults)->get();

        if ($isTranslatable && $records->isNotEmpty()) {
            $records->loadMissing('translations');
            $records->each(fn (Model $record) => $this->tagMatchedLocale($record, $query, $searchKeys, $translatableNames));
        }

        return $records;
    }

    /**
     * Sets a `_matched_locale` attribute on the record so the route and title
     * can reflect the language that actually matched the search query.
     *
     * @param  list<string>  $searchKeys
     * @param  list<string>  $translatableNames
     */
    private function tagMatchedLocale(Model $record, string $query, array $searchKeys, array $translatableNames): void
    {
        $translatableKeys = collect($searchKeys)
            ->filter(fn (string $k): bool => in_array($k, $translatableNames, true));

        if ($translatableKeys->isEmpty()) {
            return;
        }

        /** @var iterable<int, Model> $rawTranslations */
        $rawTranslations = $record->getAttribute('translations') ?? [];

        /** @var Collection<int, Model> $translations */
        $translations = collect([...$rawTranslations]);

        $matched = $translations->first(
            fn (Model $translation): bool => $translatableKeys->contains(function (string $key) use ($translation, $query): bool {
                $val = $translation->{$key} ?? null;

                return $val !== null && mb_stripos((string) $val, $query) !== false;
            })
        );

        if ($matched !== null) {
            $record->setAttribute('_matched_locale', $matched->getAttribute('locale'));
        }
    }

    private function buildRoute(string $slug, Model $record): string
    {
        $base = route('builder.resource.edit', [$slug, $record->getKey()]);
        $locale = $record->getAttribute('_matched_locale');

        return $locale ? "{$base}?locale={$locale}" : $base;
    }

    /** @param list<string> $permissions */
    private function userCanRead(array $permissions, ResourceDefinition $definition): bool
    {
        $required = $definition->resolvePermissionForAction('index');

        if ($required === null) {
            return true;
        }

        return in_array('*', $permissions, true) || in_array($required, $permissions, true);
    }
}
