<?php

namespace App\Filament\Support;

use App\Services\CurrentContext;
use Closure;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Component;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;

/**
 * Standard "pick a related record, or create one inline without leaving the
 * page" field used across every Resource with a required relationship
 * (Product → Category, Invoice → Customer, PurchaseOrder → Supplier, etc.).
 *
 * Built entirely from Filament's own native Select relationship/searchable/
 * createOptionForm support (see docs/migration-plan.md) — no custom
 * search/modal machinery. The inline "+ create" button is hidden whenever
 * the acting user lacks create permission on the related module, via the
 * same HasModulePermissions::hasPermissionTo() call every other permission
 * check in this app uses — a user who can't create branches, for instance,
 * never sees an option to create one from inside another form.
 */
class RelationSelect
{
    /**
     * @param  string  $name  the form field name (usually the FK column, e.g. "main_category_id")
     * @param  string  $relationship  the Eloquent relationship method name on the owning model
     * @param  string  $titleAttribute  the column shown in the search results / selected label
     * @param  array<int, Component>  $createForm  fields for the inline-create modal
     * @param  ?string  $createPermission  "{module_key}.create" for the related resource; null disables inline create entirely
     * @param  ?Closure  $modifyQueryUsing  same signature as Select::relationship()'s third argument — e.g. to restrict options to active parents
     * @param  ?array<int, string>  $searchColumns  extra columns a search also matches against, alongside
     *                                              $titleAttribute — e.g. a product's SKU and barcode, so a
     *                                              scanned code finds it as readily as its name does. Every
     *                                              existing caller that omits this keeps searching
     *                                              $titleAttribute only.
     */
    public static function make(
        string $name,
        string $relationship,
        string $titleAttribute,
        array $createForm = [],
        ?string $createPermission = null,
        ?Closure $modifyQueryUsing = null,
        ?array $searchColumns = null,
    ): Select {
        $select = Select::make($name)
            ->relationship($relationship, $titleAttribute, $modifyQueryUsing)
            // Select::searchable() accepts either no argument (title
            // attribute only) or an explicit column list — passing our own
            // list only when the caller actually asked for one keeps every
            // other RelationSelect::make() call site (categories, branches,
            // units, …) searching exactly as it always has.
            ->searchable($searchColumns ?? true)
            ->preload();

        // Single accessible branch → required branch selects auto-fill and lock.
        // Only required fields are locked — optional branch picks (Voucher,
        // ProductBundle, Transaction) stay nullable so a user can still create
        // a global (all-branch) record. `to_branch_id` is deliberately
        // excluded: StockTransfer's destination must stay selectable to any
        // branch even for a single-branch user (the source is what is locked).
        if ($name !== 'to_branch_id' && str_contains($name, 'branch')) {
            $select
                ->default(function (Select $component) {
                    if (! $component->isRequired()) {
                        return null;
                    }

                    return app(CurrentContext::class)->soleBranchId();
                })
                ->disabled(function (Select $component) {
                    return $component->isRequired() && app(CurrentContext::class)->soleBranchId() !== null;
                })
                ->dehydrated();
        }

        if ($createForm === [] || $createPermission === null) {
            return $select;
        }

        return $select
            ->createOptionForm($createForm)
            ->createOptionAction(
                fn (Action $action): Action => $action->visible(
                    fn (): bool => Auth::user()?->hasPermissionTo($createPermission) ?? false,
                ),
            );
    }

    /**
     * Same search/create-inline/permission-gate behavior as make(), but for
     * fields with no real parent relationship to resolve against — e.g. a
     * product/unit picker inside a plain (non ->relationship()) Repeater row,
     * such as a purchase order or GRN line item. Select::relationship()
     * always tries to resolve $relationship against the surrounding form's
     * model, which fails inside an unbound Repeater ("model doesn't have
     * that relationship") — this queries $modelClass directly instead.
     *
     * @param  class-string<Model>  $modelClass
     * @param  array<int, Component>  $createForm
     * @param  ?array<int, string>  $searchColumns  extra columns a search also matches against, alongside
     *                                              $titleAttribute — e.g. a product's SKU and barcode, so a
     *                                              scanned code finds it as readily as its name does. Every
     *                                              existing caller that omits this keeps searching
     *                                              $titleAttribute only.
     */
    public static function standalone(
        string $name,
        string $modelClass,
        string $titleAttribute,
        ?Closure $modifyQueryUsing = null,
        array $createForm = [],
        ?string $createPermission = null,
        ?array $searchColumns = null,
    ): Select {
        // $modifyQueryUsing must be invoked through the component's own
        // evaluate() (like Select::relationship() does internally), not
        // called directly — that's what lets callers type-hint Get $get,
        // $record, etc. in their closures and have Filament auto-inject
        // them. A plain direct call only supplies the query arg positionally
        // and throws ArgumentCountError the moment a caller adds a second
        // typed parameter (hit this building StockAdjustmentResource's
        // batch picker, which filters by the row's own product_id via Get).
        $baseQuery = function (Select $component) use ($modelClass, $modifyQueryUsing) {
            $query = $modelClass::query();

            return $modifyQueryUsing
                ? ($component->evaluate($modifyQueryUsing, ['query' => $query]) ?? $query)
                : $query;
        };

        $columns = $searchColumns ?? [$titleAttribute];

        $select = Select::make($name)
            ->searchable()
            ->getSearchResultsUsing(fn (Select $component, string $search) => $baseQuery($component)
                // Grouped into one WHERE (…), never bare orWhere() calls at
                // the top level — a caller's own scoping (a category filter,
                // "only what this customer bought") sits in a WHERE the
                // query builder already has, and an un-grouped OR would widen
                // that instead of searching within it.
                ->where(function ($query) use ($columns, $search) {
                    foreach ($columns as $i => $column) {
                        $i === 0
                            ? $query->where($column, 'like', "%{$search}%")
                            : $query->orWhere($column, 'like', "%{$search}%");
                    }
                })
                ->limit(50)
                ->pluck($titleAttribute, (new $modelClass)->getKeyName()))
            // Resolved through the same filtered query as the options, not a
            // fresh find(): a global scope (e.g. BranchScope) that hides the
            // row from the option list must not make the selected value's
            // label — and therefore its validation — fail for rows that ARE
            // legitimately selectable in the current form state.
            ->getOptionLabelUsing(fn (Select $component, $value) => $baseQuery($component)
                ->whereKey($value)
                ->value($titleAttribute))
            ->options(fn (Select $component) => $baseQuery($component)->limit(50)->pluck($titleAttribute, (new $modelClass)->getKeyName()));

        if ($createForm === [] || $createPermission === null) {
            return $select;
        }

        return $select
            ->createOptionForm($createForm)
            ->createOptionUsing(fn (array $data) => $modelClass::create($data)->getKey())
            ->createOptionAction(
                fn (Action $action): Action => $action->visible(
                    fn (): bool => Auth::user()?->hasPermissionTo($createPermission) ?? false,
                ),
            );
    }
}
