<?php

namespace Filament\Forms\Components;

use BackedEnum;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\View\FormsIconAlias;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\StateCasts\Contracts\StateCast;
use Filament\Schemas\Components\StateCasts\EnumArrayStateCast;
use Filament\Schemas\Components\StateCasts\EnumStateCast;
use Filament\Schemas\Components\StateCasts\OptionsArrayStateCast;
use Filament\Schemas\Components\StateCasts\OptionStateCast;
use Filament\Schemas\Schema;
use Filament\Support\Components\Attributes\ExposedLivewireMethod;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\HasExtraAlpineAttributes;
use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Filament\Support\Services\RelationshipJoiner;
use Filament\Support\View\ComponentAttributeBag as FilamentComponentAttributeBag;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use Illuminate\Database\Eloquent\Relations\HasOneOrManyThrough;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Js;
use Illuminate\Support\Str;
use Livewire\Attributes\Renderless;
use LogicException;
use Znck\Eloquent\Relations\BelongsToThrough;

use function Filament\Support\generate_search_column_expression;
use function Filament\Support\generate_search_term_expression;

class Select extends Field implements Contracts\CanDisableOptions, Contracts\HasAffixes, Contracts\HasNestedRecursiveValidationRules, HasEmbeddedView
{
    use Concerns\CanAllowHtml;
    use Concerns\CanBeNative;
    use Concerns\CanBePreloaded;
    use Concerns\CanBeSearchable;
    use Concerns\CanDisableOptions;
    use Concerns\CanDisableOptionsWhenSelectedInSiblingRepeaterItems;
    use Concerns\CanFixIndistinctState;
    use Concerns\CanLimitItemsLength;
    use Concerns\CanSelectPlaceholder;
    use Concerns\HasAffixes;
    use Concerns\HasExtraInputAttributes;
    use Concerns\HasLoadingMessage;
    use Concerns\HasNestedRecursiveValidationRules;
    use Concerns\HasOptions;
    use Concerns\HasPivotData;
    use Concerns\HasPlaceholder;
    use HasExtraAlpineAttributes;

    protected ?string $publishedViewOverrideCheckPath = 'filament-forms::components.select';

    /**
     * @var array<Component | Action | ActionGroup> | Closure | null
     */
    protected array | Closure | null $createOptionActionForm = null;

    protected ?Closure $createOptionUsing = null;

    protected string | Closure | null $createOptionModalHeading = null;

    protected string | Closure | null $editOptionModalHeading = null;

    protected ?Closure $modifyCreateOptionActionUsing = null;

    protected ?Closure $modifyManageOptionActionsUsing = null;

    /**
     * @var array<Component | Action | ActionGroup> | Closure | null
     */
    protected array | Closure | null $editOptionActionForm = null;

    protected ?Closure $fillEditOptionActionFormUsing = null;

    protected ?Closure $updateOptionUsing = null;

    protected ?Closure $modifyEditOptionActionUsing = null;

    protected ?Model $cachedSelectedRecord = null;

    protected bool | Closure $isMultiple = false;

    protected ?Closure $getOptionLabelUsing = null;

    protected ?Closure $getOptionLabelsUsing = null;

    protected ?Closure $getSearchResultsUsing = null;

    protected ?Closure $getSelectedRecordUsing = null;

    protected ?Closure $transformOptionsForJsUsing = null;

    /**
     * @var array<string> | null
     */
    protected ?array $searchColumns = null;

    protected string | Closure | null $maxItemsMessage = null;

    protected string | Closure | null $relationshipTitleAttribute = null;

    protected string | Closure | null $position = null;

    protected ?Closure $getOptionLabelFromRecordUsing = null;

    protected string | Closure | null $relationship = null;

    protected ?Closure $modifyRelationshipQueryUsing = null;

    protected bool $shouldIgnoreRelationshipRecord = false;

    protected ?Collection $cachedRelationshipRecords = null;

    /**
     * @var array<string | int, string> | null
     */
    protected ?array $cachedRelationshipOptions = null;

    protected int | Closure $optionsLimit = 50;

    protected bool | Closure | null $isSearchForcedCaseInsensitive = null;

    protected bool | Closure $canOptionLabelsWrap = true;

    protected bool | Closure $isReorderable = false;

    protected bool | Closure | null $hasDynamicOptions = null;

    protected function setUp(): void
    {
        parent::setUp();

        $this->transformOptionsForJsUsing(static function (Select $component, array $options): array {
            return collect($options)
                ->map(fn ($label, $value): array => is_array($label)
                    ? ['label' => $value, 'options' => $component->transformOptionsForJs($label)]
                    : ['label' => $label, 'value' => strval($value), 'isDisabled' => $component->isOptionDisabled($value, $label)])
                ->values()
                ->all();
        });

        $this->placeholder(static fn (Select $component): ?string => $component->isDisabled() ? null : __('filament-forms::components.select.placeholder'));

        $this->suffixActions([
            static fn (Select $component): ?Action => $component->getCreateOptionAction(),
            static fn (Select $component): ?Action => $component->getEditOptionAction(),
        ]);
    }

    public function boolean(?string $trueLabel = null, ?string $falseLabel = null, ?string $placeholder = null): static
    {
        $this->options([
            1 => $trueLabel ?? __('filament-forms::components.select.boolean.true'),
            0 => $falseLabel ?? __('filament-forms::components.select.boolean.false'),
        ]);

        $this->placeholder($placeholder ?? '-');

        $this->stateCast(app(BooleanStateCast::class, ['isStoredAsInt' => true]));

        return $this;
    }

    public function createOptionAction(?Closure $callback): static
    {
        $this->modifyCreateOptionActionUsing = $callback;

        return $this;
    }

    public function manageOptionActions(?Closure $callback): static
    {
        $this->modifyManageOptionActionsUsing = $callback;

        return $this;
    }

    /**
     * @param  array<Component | Action | ActionGroup> | Closure | null  $schema
     */
    public function manageOptionForm(array | Closure | null $schema): static
    {
        $this->createOptionForm($schema);
        $this->editOptionForm($schema);

        return $this;
    }

    /**
     * @param  array<Component | Action | ActionGroup> | Closure | null  $schema
     */
    public function createOptionForm(array | Closure | null $schema): static
    {
        $this->createOptionActionForm = $schema;

        return $this;
    }

    public function createOptionUsing(?Closure $callback): static
    {
        $this->createOptionUsing = $callback;

        return $this;
    }

    public function getCreateOptionUsing(): ?Closure
    {
        return $this->createOptionUsing;
    }

    public function getCreateOptionActionName(): string
    {
        return 'createOption';
    }

    public function getCreateOptionAction(): ?Action
    {
        if ($this->isDisabled()) {
            return null;
        }

        if (! $this->hasCreateOptionActionFormSchema()) {
            return null;
        }

        $action = Action::make($this->getCreateOptionActionName())
            ->label(__('filament-forms::components.select.actions.create_option.label'))
            ->schema(static function (Select $component, Schema $schema): array | Schema | null {
                return $component->getCreateOptionActionForm($schema->model(
                    $component->hasRelationship() ? $component->getRelationship()->getModel()::class : $component->getActionSchemaModel(),
                ));
            })
            ->action(static function (Action $action, array $arguments, Select $component, array $data, Schema $schema): void {
                if (! $component->getCreateOptionUsing()) {
                    throw new LogicException("Select field [{$component->getStatePath()}] must have a [createOptionUsing()] closure set.");
                }

                $createdOptionKey = $component->evaluate($component->getCreateOptionUsing(), [
                    'data' => $data,
                    'form' => $schema,
                    'schema' => $schema,
                ]);

                $state = $component->isMultiple()
                    ? [
                        ...$component->getState(),
                        $createdOptionKey,
                    ]
                    : $createdOptionKey;

                $component->state($state);
                $component->callAfterStateUpdated();

                $component->refreshSelectedOptionLabel();

                if (! ($arguments['another'] ?? false)) {
                    return;
                }

                $action->callAfter();

                $schema->fill();

                $action->halt();
            })
            ->color('gray')
            ->icon(FilamentIcon::resolve(FormsIconAlias::COMPONENTS_SELECT_ACTIONS_CREATE_OPTION) ?? Heroicon::Plus)
            ->iconButton()
            ->modalHeading($this->getCreateOptionModalHeading() ?? __('filament-forms::components.select.actions.create_option.modal.heading'))
            ->modalSubmitActionLabel(__('filament-forms::components.select.actions.create_option.modal.actions.create.label'))
            ->extraModalFooterActions(fn (Action $action, Select $component): array => $component->isMultiple() ? [
                $action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
                    ->label(__('filament-forms::components.select.actions.create_option.modal.actions.create_another.label')),
            ] : []);

        if ($this->modifyManageOptionActionsUsing) {
            $action = $this->evaluate($this->modifyManageOptionActionsUsing, [
                'action' => $action,
            ]) ?? $action;
        }

        if ($this->modifyCreateOptionActionUsing) {
            $action = $this->evaluate($this->modifyCreateOptionActionUsing, [
                'action' => $action,
            ]) ?? $action;
        }

        return $action;
    }

    public function createOptionModalHeading(string | Closure | null $heading): static
    {
        $this->createOptionModalHeading = $heading;

        return $this;
    }

    public function editOptionModalHeading(string | Closure | null $heading): static
    {
        $this->editOptionModalHeading = $heading;

        return $this;
    }

    public function editOptionAction(?Closure $callback): static
    {
        $this->modifyEditOptionActionUsing = $callback;

        return $this;
    }

    /**
     * @return array<Component | Action | ActionGroup> | Schema | null
     */
    public function getCreateOptionActionForm(Schema $schema): array | Schema | null
    {
        return $this->evaluate($this->createOptionActionForm, ['form' => $schema, 'schema' => $schema]);
    }

    public function hasCreateOptionActionFormSchema(): bool
    {
        return (bool) $this->createOptionActionForm;
    }

    /**
     * @return array<Component | Action | ActionGroup> | Schema | null
     */
    public function getEditOptionActionForm(Schema $schema): array | Schema | null
    {
        return $this->evaluate($this->editOptionActionForm, ['form' => $schema, 'schema' => $schema]);
    }

    public function hasEditOptionActionFormSchema(): bool
    {
        return (bool) $this->editOptionActionForm;
    }

    /**
     * @param  array<Component | Action | ActionGroup> | Closure | null  $schema
     */
    public function editOptionForm(array | Closure | null $schema): static
    {
        $this->editOptionActionForm = $schema;
        $this->live();

        return $this;
    }

    public function updateOptionUsing(?Closure $callback): static
    {
        $this->updateOptionUsing = $callback;

        return $this;
    }

    public function getUpdateOptionUsing(): ?Closure
    {
        return $this->updateOptionUsing;
    }

    public function getEditOptionActionName(): string
    {
        return 'editOption';
    }

    public function getEditOptionAction(): ?Action
    {
        if ($this->isDisabled()) {
            return null;
        }

        if ($this->isMultiple()) {
            return null;
        }

        if (! $this->hasEditOptionActionFormSchema()) {
            return null;
        }

        $action = Action::make($this->getEditOptionActionName())
            ->label(__('filament-forms::components.select.actions.edit_option.label'))
            ->schema(static function (Select $component, Schema $schema): array | Schema | null {
                return $component->getEditOptionActionForm(
                    $schema->model($component->getSelectedRecord()),
                );
            })
            ->fillForm(static fn (Select $component): ?array => $component->getEditOptionActionFormData())
            ->action(static function (Action $action, array $arguments, Select $component, array $data, Schema $schema): void {
                if (! $component->getUpdateOptionUsing()) {
                    throw new LogicException("Select field [{$component->getStatePath()}] must have a [updateOptionUsing()] closure set.");
                }

                $component->evaluate($component->getUpdateOptionUsing(), [
                    'data' => $data,
                    'form' => $schema,
                    'schema' => $schema,
                ]);

                $component->refreshSelectedOptionLabel();
            })
            ->color('gray')
            ->icon(FilamentIcon::resolve(FormsIconAlias::COMPONENTS_SELECT_ACTIONS_EDIT_OPTION) ?? Heroicon::PencilSquare)
            ->iconButton()
            ->modalHeading($this->getEditOptionModalHeading() ?? __('filament-forms::components.select.actions.edit_option.modal.heading'))
            ->modalSubmitActionLabel(__('filament-forms::components.select.actions.edit_option.modal.actions.save.label'))
            ->visible(fn (): bool => filled($this->getState()));

        if ($this->modifyManageOptionActionsUsing) {
            $action = $this->evaluate($this->modifyManageOptionActionsUsing, [
                'action' => $action,
            ]) ?? $action;
        }

        if ($this->modifyEditOptionActionUsing) {
            $action = $this->evaluate($this->modifyEditOptionActionUsing, [
                'action' => $action,
            ]) ?? $action;
        }

        return $action;
    }

    /**
     * @return ?array<string, mixed>
     */
    public function getEditOptionActionFormData(): ?array
    {
        return $this->evaluate($this->fillEditOptionActionFormUsing);
    }

    public function fillEditOptionActionFormUsing(?Closure $callback): static
    {
        $this->fillEditOptionActionFormUsing = $callback;

        return $this;
    }

    public function getCreateOptionModalHeading(): ?string
    {
        return $this->evaluate($this->createOptionModalHeading);
    }

    public function getEditOptionModalHeading(): ?string
    {
        return $this->evaluate($this->editOptionModalHeading);
    }

    public function getOptionLabelUsing(?Closure $callback): static
    {
        $this->getOptionLabelUsing = $callback;

        return $this;
    }

    public function getSelectedRecordUsing(?Closure $callback): static
    {
        $this->getSelectedRecordUsing = $callback;

        return $this;
    }

    public function getOptionLabelsUsing(?Closure $callback): static
    {
        $this->getOptionLabelsUsing = $callback;

        return $this;
    }

    public function getSearchResultsUsing(?Closure $callback): static
    {
        $this->getSearchResultsUsing = $callback;

        return $this;
    }

    public function transformOptionsForJsUsing(?Closure $callback): static
    {
        $this->transformOptionsForJsUsing = $callback;

        return $this;
    }

    /**
     * @param  bool | array<string> | Closure  $condition
     */
    public function searchable(bool | array | Closure $condition = true): static
    {
        if (is_array($condition)) {
            $this->isSearchable = true;
            $this->searchColumns = $condition;
        } else {
            $this->isSearchable = $condition;
            $this->searchColumns = null;
        }

        return $this;
    }

    public function multiple(bool | Closure $condition = true): static
    {
        $this->isMultiple = $condition;

        return $this;
    }

    public function reorderable(bool | Closure $condition = true): static
    {
        $this->isReorderable = $condition;

        return $this;
    }

    public function isReorderable(): bool
    {
        return (bool) $this->evaluate($this->isReorderable);
    }

    public function position(string | Closure | null $position): static
    {
        $this->position = $position;

        return $this;
    }

    public function maxItemsMessage(string | Closure | null $message): static
    {
        $this->maxItemsMessage = $message;

        return $this;
    }

    public function optionsLimit(int | Closure $limit): static
    {
        $this->optionsLimit = $limit;

        return $this;
    }

    public function getPosition(): ?string
    {
        return $this->evaluate($this->position);
    }

    #[ExposedLivewireMethod]
    #[Renderless]
    public function getOptionLabel(bool $withDefault = true): ?string
    {
        return $this->resolveOptionLabel($withDefault);
    }

    /**
     * @param  array<string | array<string>> | null  $options
     */
    protected function resolveOptionLabel(bool $withDefault = true, ?array $options = null): ?string
    {
        if (! $this->getOptionLabelUsing) {
            $state = $this->getState();
            $options ??= $this->getOptions();

            if ($state instanceof BackedEnum) {
                $state = $state->value;
            }

            foreach ($options as $groupedOptions) {
                if (! is_array($groupedOptions)) {
                    continue;
                }

                if (blank($groupedOptions[$state] ?? null)) {
                    continue;
                }

                return $groupedOptions[$state];
            }

            if (filled($options[$state] ?? null) && (! is_array($options[$state]))) {
                return $options[$state];
            }

            if ($withDefault) {
                return $state;
            }

            return null;
        }

        $state = null;

        $label = $this->evaluate($this->getOptionLabelUsing, [
            'value' => function () use (&$state): mixed {
                return $state = $this->getState();
            },
        ]);

        if ($withDefault) {
            $label ??= ($state ?? $this->getState());
        }

        return $label;
    }

    /**
     * @param  array<string | array<string>> | null  $options
     * @return array<string>
     */
    public function getOptionLabels(bool $withDefaults = true, ?array $options = null): array
    {
        if (! $this->getOptionLabelsUsing) {
            $state = $this->getState();
            $options ??= $this->getOptions();

            $labels = [];

            foreach ($state ?? [] as $value) {
                if ($value instanceof BackedEnum) {
                    $value = $value->value;
                }

                foreach ($options as $groupedOptions) {
                    if (! is_array($groupedOptions)) {
                        continue;
                    }

                    if (blank($groupedOptions[$value] ?? null)) {
                        continue;
                    }

                    $labels[$value] = $groupedOptions[$value];

                    continue 2;
                }

                if (
                    filled($options[$value] ?? null)
                    && (! is_array($options[$value]))
                ) {
                    $labels[$value] = $options[$value];

                    continue;
                }

                if ($withDefaults) {
                    $labels[$value] = $value;
                }
            }

            return $labels;
        }

        $labels = $this->evaluate($this->getOptionLabelsUsing, [
            'values' => fn (): array => $this->getState(),
        ]);

        if ($labels instanceof Arrayable) {
            $labels = $labels->toArray();
        }

        foreach ($labels as $value => $label) {
            if (filled($label)) {
                continue;
            }

            if ($withDefaults) {
                $labels[$value] = $value;

                continue;
            }

            unset($labels[$value]);
        }

        return $labels;
    }

    /**
     * @return array<string>
     */
    public function getSearchColumns(): ?array
    {
        $columns = $this->searchColumns;

        if ($this->hasRelationship() && (filled($relationshipTitleAttribute = $this->getRelationshipTitleAttribute()))) {
            $columns ??= [$relationshipTitleAttribute];
        }

        return $columns;
    }

    /**
     * @return array<string>
     */
    public function getSearchResults(string $search): array
    {
        if (! $this->getSearchResultsUsing) {
            return [];
        }

        $results = $this->evaluate($this->getSearchResultsUsing, [
            'query' => $search,
            'search' => $search,
            'searchQuery' => $search,
        ]);

        if ($results instanceof Arrayable) {
            $results = $results->toArray();
        }

        return $results;
    }

    /**
     * @return array<array{'label': string, 'value': string}>
     */
    #[ExposedLivewireMethod]
    #[Renderless]
    public function getSearchResultsForJs(string $search): array
    {
        return $this->transformOptionsForJs($this->getSearchResults($search));
    }

    /**
     * @return array<array{'label': string, 'value': string}>
     */
    #[ExposedLivewireMethod]
    #[Renderless]
    public function getOptionsForJs(): array
    {
        return $this->transformOptionsForJs($this->getOptions());
    }

    /**
     * @return array<array{'label': string, 'value': string}>
     */
    #[ExposedLivewireMethod]
    #[Renderless]
    public function getOptionLabelsForJs(): array
    {
        return $this->transformOptionsForJs($this->getOptionLabels());
    }

    /**
     * @param  array<string | array<string>>  $options
     * @return array<array<string, mixed>>
     */
    protected function transformOptionsForJs(array $options): array
    {
        if (empty($options)) {
            return [];
        }

        $transformedOptions = $this->evaluate($this->transformOptionsForJsUsing, [
            'options' => $options,
        ]);

        if ($transformedOptions instanceof Arrayable) {
            return $transformedOptions->toArray();
        }

        return $transformedOptions;
    }

    public function isMultiple(): bool
    {
        return (bool) $this->evaluate($this->isMultiple);
    }

    public function isSearchable(): bool
    {
        return $this->evaluate($this->isSearchable) ?? $this->isMultiple();
    }

    public function relationship(string | Closure | null $name = null, string | Closure | null $titleAttribute = null, ?Closure $modifyQueryUsing = null, bool $ignoreRecord = false): static
    {
        $this->relationship = $name ?? $this->getName();
        $this->relationshipTitleAttribute = $titleAttribute;
        $this->modifyRelationshipQueryUsing = $modifyQueryUsing;
        $this->shouldIgnoreRelationshipRecord = $ignoreRecord;
        $this->cachedRelationshipRecords = null;
        $this->cachedRelationshipOptions = null;

        $this->getSearchResultsUsing(static function (Select $component, ?string $search): array {
            return $component->getSearchResultsFromRelationship($search);
        });

        $this->options(static function (Select $component): ?array {
            return $component->getOptionsFromRelationship();
        });

        $this->loadStateFromRelationshipsUsing(static function (Select $component): void {
            $component->fillStateFromRelationship();
        });

        $this->getOptionLabelUsing(static function (Select $component) {
            $record = $component->getSelectedRecord();

            if (! $record) {
                return null;
            }

            if ($component->hasOptionLabelFromRecordUsingCallback()) {
                return $component->getOptionLabelFromRecord($record);
            }

            $relationshipTitleAttribute = $component->getRelationshipTitleAttribute();

            if (str_contains($relationshipTitleAttribute, '->')) {
                $relationshipTitleAttribute = str_replace('->', '.', $relationshipTitleAttribute);
            }

            return data_get($record, $relationshipTitleAttribute);
        });

        $this->getSelectedRecordUsing(static function (Select $component, $state) use ($modifyQueryUsing): ?Model {
            $relationship = $component->getRelationship();

            if (
                (! $modifyQueryUsing) &&
                ($relationship instanceof BelongsTo)
            ) {
                $record = $component->getRecord();

                if (
                    ($record instanceof Model) &&
                    $record->relationLoaded($component->getRelationshipName())
                ) {
                    $relatedRecord = $record->getRelationValue($component->getRelationshipName());

                    if (
                        ($relatedRecord instanceof Model) &&
                        ((string) $relatedRecord->getAttribute($relationship->getOwnerKeyName()) === (string) $state)
                    ) {
                        return $relatedRecord;
                    }
                }
            }

            $relationship = Relation::noConstraints(fn () => $component->getRelationship());

            $relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);

            $relationshipQuery->where($component->getQualifiedRelatedKeyNameForRelationship($relationship), $state);

            if ($modifyQueryUsing) {
                $relationshipQuery = $component->evaluate($modifyQueryUsing, [
                    'query' => $relationshipQuery,
                    'search' => null,
                ]) ?? $relationshipQuery;
            }

            return $relationshipQuery->first();
        });

        $this->getOptionLabelsUsing(static function (Select $component, array $values) use ($modifyQueryUsing): array {
            $relationship = $component->getRelationship();
            $record = $component->getRecord();
            $relationshipName = $component->getRelationshipName();

            if (
                (! $modifyQueryUsing) &&
                ($record instanceof Model) &&
                $record->relationLoaded($relationshipName) &&
                (
                    ($relationship instanceof BelongsToMany) ||
                    ($relationship instanceof HasOneOrMany)
                )
            ) {
                $relatedRecords = $record->getRelationValue($relationshipName);

                if ($relatedRecords instanceof Collection) {
                    $relatedKeyName = ($relationship instanceof BelongsToMany)
                        ? $relationship->getRelatedKeyName()
                        : $relationship->getRelated()->getKeyName();

                    $loadedKeys = $relatedRecords->pluck($relatedKeyName)->map(fn ($key) => (string) $key)->all();
                    $requestedKeys = array_map(fn ($value) => (string) $value, $values);

                    if (empty(array_diff($requestedKeys, $loadedKeys))) {
                        $relationshipTitleAttribute = $component->getRelationshipTitleAttribute();

                        if (str_contains($relationshipTitleAttribute, '->')) {
                            $relationshipTitleAttribute = str_replace('->', '.', $relationshipTitleAttribute);
                        }

                        $filteredRecords = $relatedRecords->filter(
                            fn (Model $relatedRecord): bool => in_array(
                                (string) $relatedRecord->getAttribute($relatedKeyName),
                                $requestedKeys,
                                strict: true,
                            ),
                        );

                        if ($component->hasOptionLabelFromRecordUsingCallback()) {
                            return $filteredRecords
                                ->mapWithKeys(static fn (Model $relatedRecord) => [
                                    $relatedRecord->getAttribute($relatedKeyName) => $component->getOptionLabelFromRecord($relatedRecord),
                                ])
                                ->toArray();
                        }

                        return $filteredRecords
                            ->pluck($relationshipTitleAttribute, $relatedKeyName)
                            ->toArray();
                    }
                }
            }

            $relationship = Relation::noConstraints(fn () => $component->getRelationship());

            $relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);

            $qualifiedRelatedKeyName = $component->getQualifiedRelatedKeyNameForRelationship($relationship);

            $relationshipQuery->whereIn($qualifiedRelatedKeyName, $values);

            if ($modifyQueryUsing) {
                $relationshipQuery = $component->evaluate($modifyQueryUsing, [
                    'query' => $relationshipQuery,
                    'search' => null,
                ]) ?? $relationshipQuery;
            }

            if ($component->hasOptionLabelFromRecordUsingCallback()) {
                return $relationshipQuery
                    ->get()
                    ->mapWithKeys(static fn (Model $record) => [
                        $record->{Str::afterLast($qualifiedRelatedKeyName, '.')} => $component->getOptionLabelFromRecord($record),
                    ])
                    ->toArray();
            }

            $relationshipTitleAttribute = $component->getRelationshipTitleAttribute();

            if (str_contains($relationshipTitleAttribute, '->')) {
                if (! str_contains($relationshipTitleAttribute, ' as ')) {
                    $relationshipTitleAttribute .= " as {$relationshipTitleAttribute}";
                }
            } else {
                $relationshipTitleAttribute = $relationshipQuery->qualifyColumn($relationshipTitleAttribute);
            }

            return $relationshipQuery
                ->pluck($relationshipTitleAttribute, $qualifiedRelatedKeyName)
                ->toArray();
        });

        $this->saveRelationshipsUsing(static function (Select $component): void {
            $component->saveStateToRelationship();
        });

        $this->createOptionUsing(static function (Select $component, array $data, Schema $schema) {
            $record = $component->getRelationship()->newModelInstance();
            $record->fill($data);
            $record->save();

            $schema->model($record)->saveRelationships();

            return $record->getKey();
        });

        $this->fillEditOptionActionFormUsing(static function (Select $component): ?array {
            return $component->getSelectedRecord()?->attributesToArray();
        });

        $this->updateOptionUsing(static function (array $data, Schema $schema): void {
            $schema->getRecord()?->update($data);
        });

        $this->dehydrated(fn (Select $component): bool => (! $component->isMultiple()) && $component->isSaved());

        return $this;
    }

    /**
     * @return array<string | int, string>
     */
    public function getSearchResultsFromRelationship(?string $search): array
    {
        $relationship = Relation::noConstraints(fn () => $this->getRelationship());

        $relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);

        if ($this->shouldIgnoreRelationshipRecord && ($record = $this->getRecord())) {
            $relationshipQuery->where($record->getQualifiedKeyName(), '!=', $record->getKey());
        }

        if ($this->modifyRelationshipQueryUsing) {
            $relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [
                'query' => $relationshipQuery,
                'search' => $search,
            ]) ?? $relationshipQuery;
        }

        $this->applySearchConstraint(
            $relationshipQuery,
            generate_search_term_expression($search, $this->isSearchForcedCaseInsensitive(), $relationshipQuery->getConnection()),
        );

        $baseRelationshipQuery = $relationshipQuery->getQuery();

        if (isset($baseRelationshipQuery->limit)) {
            $this->optionsLimit($baseRelationshipQuery->limit);
        } else {
            $relationshipQuery->limit($this->getOptionsLimit());
        }

        $qualifiedRelatedKeyName = $this->getQualifiedRelatedKeyNameForRelationship($relationship);

        if ($this->hasOptionLabelFromRecordUsingCallback()) {
            return $relationshipQuery
                ->get()
                ->mapWithKeys(fn (Model $record) => [
                    $record->{Str::afterLast($qualifiedRelatedKeyName, '.')} => $this->getOptionLabelFromRecord($record),
                ])
                ->toArray();
        }

        $relationshipTitleAttribute = $this->getRelationshipTitleAttribute();

        if (empty($relationshipQuery->getQuery()->orders)) {
            $relationshipOrderByAttribute = $relationshipTitleAttribute;

            if (str_contains($relationshipOrderByAttribute, ' as ')) {
                $relationshipOrderByAttribute = (string) str($relationshipOrderByAttribute)->before(' as ');
            }

            $relationshipQuery->orderBy($relationshipQuery->qualifyColumn($relationshipOrderByAttribute));
        }

        if (str_contains($relationshipTitleAttribute, '->')) {
            if (! str_contains($relationshipTitleAttribute, ' as ')) {
                $relationshipTitleAttribute .= " as {$relationshipTitleAttribute}";
            }
        } else {
            $relationshipTitleAttribute = $relationshipQuery->qualifyColumn($relationshipTitleAttribute);
        }

        return $relationshipQuery
            ->pluck($relationshipTitleAttribute, $qualifiedRelatedKeyName)
            ->toArray();
    }

    /**
     * @return array<string | int, string> | null
     */
    public function getOptionsFromRelationship(): ?array
    {
        if ($this->isSearchable() && (! $this->isPreloaded())) {
            return null;
        }

        $relationship = Relation::noConstraints(fn () => $this->getRelationship());

        $qualifiedRelatedKeyName = $this->getQualifiedRelatedKeyNameForRelationship($relationship);

        if ($this->hasOptionLabelFromRecordUsingCallback()) {
            if (
                (! $this->modifyRelationshipQueryUsing) &&
                (! $this->shouldIgnoreRelationshipRecord) &&
                ($this->cachedRelationshipRecords !== null)
            ) {
                return $this->cachedRelationshipRecords
                    ->mapWithKeys(fn (Model $record) => [
                        $record->{Str::afterLast($qualifiedRelatedKeyName, '.')} => $this->getOptionLabelFromRecord($record),
                    ])
                    ->toArray();
            }

            $relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);

            if ($this->shouldIgnoreRelationshipRecord && ($record = $this->getRecord())) {
                $relationshipQuery->where($record->getQualifiedKeyName(), '!=', $record->getKey());
            }

            if ($this->modifyRelationshipQueryUsing) {
                $relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [
                    'query' => $relationshipQuery,
                    'search' => null,
                ]) ?? $relationshipQuery;
            }

            $baseRelationshipQuery = $relationshipQuery->getQuery();

            if (isset($baseRelationshipQuery->limit)) {
                $this->optionsLimit($baseRelationshipQuery->limit);
            } elseif ($this->isSearchable() && filled($this->getSearchColumns())) {
                $relationshipQuery->limit($this->getOptionsLimit());
            }

            $records = $relationshipQuery->get();

            if ((! $this->modifyRelationshipQueryUsing) && (! $this->shouldIgnoreRelationshipRecord)) {
                $this->cachedRelationshipRecords = $records;
            }

            return $records
                ->mapWithKeys(fn (Model $record) => [
                    $record->{Str::afterLast($qualifiedRelatedKeyName, '.')} => $this->getOptionLabelFromRecord($record),
                ])
                ->toArray();
        }

        if (
            (! $this->modifyRelationshipQueryUsing) &&
            (! $this->shouldIgnoreRelationshipRecord) &&
            ($this->cachedRelationshipOptions !== null)
        ) {
            return $this->cachedRelationshipOptions;
        }

        $relationshipQuery = app(RelationshipJoiner::class)->prepareQueryForNoConstraints($relationship);

        if ($this->shouldIgnoreRelationshipRecord && ($record = $this->getRecord())) {
            $relationshipQuery->where($record->getQualifiedKeyName(), '!=', $record->getKey());
        }

        if ($this->modifyRelationshipQueryUsing) {
            $relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [
                'query' => $relationshipQuery,
                'search' => null,
            ]) ?? $relationshipQuery;
        }

        $baseRelationshipQuery = $relationshipQuery->getQuery();

        if (isset($baseRelationshipQuery->limit)) {
            $this->optionsLimit($baseRelationshipQuery->limit);
        } elseif ($this->isSearchable() && filled($this->getSearchColumns())) {
            $relationshipQuery->limit($this->getOptionsLimit());
        }

        $relationshipTitleAttribute = $this->getRelationshipTitleAttribute();

        if (empty($relationshipQuery->getQuery()->orders)) {
            $relationshipOrderByAttribute = $relationshipTitleAttribute;

            if (str_contains($relationshipOrderByAttribute, ' as ')) {
                $relationshipOrderByAttribute = (string) str($relationshipOrderByAttribute)->before(' as ');
            }

            $relationshipQuery->orderBy($relationshipQuery->qualifyColumn($relationshipOrderByAttribute));
        }

        if (str_contains($relationshipTitleAttribute, '->')) {
            if (! str_contains($relationshipTitleAttribute, ' as ')) {
                $relationshipTitleAttribute .= " as {$relationshipTitleAttribute}";
            }
        } else {
            $relationshipTitleAttribute = $relationshipQuery->qualifyColumn($relationshipTitleAttribute);
        }

        $options = $relationshipQuery
            ->pluck($relationshipTitleAttribute, $qualifiedRelatedKeyName)
            ->toArray();

        if ((! $this->modifyRelationshipQueryUsing) && (! $this->shouldIgnoreRelationshipRecord)) {
            $this->cachedRelationshipOptions = $options;
        }

        return $options;
    }

    public function fillStateFromRelationship(): void
    {
        if (filled($this->getState())) {
            return;
        }

        $relationship = $this->getRelationship();
        $relationshipName = $this->getRelationshipName();

        if (
            (! $this->modifyRelationshipQueryUsing) &&
            (! str_contains($relationshipName, '.')) &&
            ($record = $this->getRecord()) instanceof Model &&
            $record->relationLoaded($relationshipName)
        ) {
            $relatedRecords = $record->getRelationValue($relationshipName);

            if (
                ($relationship instanceof BelongsToMany) ||
                ($relationship instanceof HasOneOrManyThrough)
            ) {
                $relatedKeys = $relatedRecords
                    ->pluck(($relationship instanceof BelongsToMany) ? $relationship->getRelatedKeyName() : $relationship->getRelated()->getKeyName())
                    ->map(static fn ($key): string => strval($key));

                $this->state(
                    $this->isMultiple()
                        ? $relatedKeys->all()
                        : $relatedKeys->first(),
                );

                return;
            }

            if ($relationship instanceof BelongsToThrough) {
                $this->state(
                    $relatedRecords?->getAttribute(
                        $relationship->getRelated()->getKeyName(),
                    ),
                );

                return;
            }

            if ($relationship instanceof HasMany) {
                $this->state(
                    $relatedRecords
                        ->pluck($relationship->getLocalKeyName())
                        ->all(),
                );

                return;
            }

            if ($relationship instanceof HasOne) {
                $this->state(
                    $relatedRecords?->getAttribute(
                        $relationship->getLocalKeyName(),
                    ),
                );

                return;
            }

            /** @var BelongsTo $relationship */
            $this->state(
                $relatedRecords?->getAttribute(
                    $relationship->getOwnerKeyName(),
                ),
            );

            return;
        }

        if (
            ($relationship instanceof BelongsToMany) ||
            ($relationship instanceof HasOneOrManyThrough)
        ) {
            if ($this->modifyRelationshipQueryUsing) {
                $this->evaluate($this->modifyRelationshipQueryUsing, [
                    'query' => $relationship->getQuery(),
                    'search' => null,
                ]);
            }

            /** @var Collection $relatedRecords */
            $relatedRecords = $relationship->getResults();

            // Cast the related keys to a string, otherwise
            // JavaScript can't handle deselection.
            //
            // https://github.com/filamentphp/filament/issues/1111
            $relatedKeys = $relatedRecords
                ->pluck(($relationship instanceof BelongsToMany) ? $relationship->getRelatedKeyName() : $relationship->getRelated()->getKeyName())
                ->map(static fn ($key): string => strval($key));

            $this->state(
                $this->isMultiple()
                    ? $relatedKeys->all()
                    : $relatedKeys->first(),
            );

            return;
        }

        if ($relationship instanceof BelongsToThrough) {
            /** @var ?Model $relatedModel */
            $relatedModel = $relationship->getResults();

            $this->state(
                $relatedModel?->getAttribute(
                    $relationship->getRelated()->getKeyName(),
                ),
            );

            return;
        }

        if ($relationship instanceof HasMany) {
            /** @var Collection $relatedRecords */
            $relatedRecords = $relationship->getResults();

            $this->state(
                $relatedRecords
                    ->pluck($relationship->getLocalKeyName())
                    ->all(),
            );

            return;
        }

        if ($relationship instanceof HasOne) {
            $relatedModel = $relationship->getResults();

            $this->state(
                $relatedModel?->getAttribute(
                    $relationship->getLocalKeyName(),
                ),
            );

            return;
        }

        /** @var BelongsTo $relationship */
        $relatedModel = $relationship->getResults();

        $this->state(
            $relatedModel?->getAttribute(
                $relationship->getOwnerKeyName(),
            ),
        );
    }

    public function saveStateToRelationship(): void
    {
        $relationship = $this->getRelationship();
        $record = $this->getRecord();
        $relationshipName = $this->getRelationshipName();
        $state = $this->getState();

        if (($relationship instanceof HasOne) || ($relationship instanceof HasMany)) {
            $query = $relationship->getQuery();

            if ($this->modifyRelationshipQueryUsing) {
                $this->evaluate($this->modifyRelationshipQueryUsing, [
                    'query' => $query,
                    'search' => null,
                ]);
            }

            $query->update([
                $relationship->getForeignKeyName() => null,
            ]);

            if (! empty($state)) {
                $relationship::noConstraints(function () use ($record, $state): void {
                    $relationship = $this->getRelationship();

                    $query = $relationship->getQuery()->whereIn($relationship->getLocalKeyName(), Arr::wrap($state));

                    if ($this->modifyRelationshipQueryUsing) {
                        $this->evaluate($this->modifyRelationshipQueryUsing, [
                            'query' => $query,
                            'search' => null,
                        ]);
                    }

                    $query->update([
                        $relationship->getForeignKeyName() => $record->getAttribute($relationship->getLocalKeyName()),
                    ]);
                });
            }

            $record->unsetRelation($relationshipName);

            return;
        }

        if (
            ($relationship instanceof HasOneOrMany) ||
            ($relationship instanceof HasOneOrManyThrough) ||
            ($relationship instanceof BelongsToThrough)
        ) {
            return;
        }

        if (! $relationship instanceof BelongsToMany) {
            // Security: If the model is new and the foreign key is already
            // filled, don't overwrite it — the key may have been set by
            // authorization logic or event listeners before save.
            if (
                $record->wasRecentlyCreated &&
                filled($record->getAttributeValue($relationship->getForeignKeyName()))
            ) {
                return;
            }

            $relationship->associate($state);
            $record->wasRecentlyCreated && $record->save();

            return;
        }

        if ($this->modifyRelationshipQueryUsing) {
            $this->evaluate($this->modifyRelationshipQueryUsing, [
                'query' => $relationship->getQuery(),
                'search' => null,
            ]);
        }

        /** @var Collection $relatedRecords */
        $relatedRecords = $relationship->getResults();

        $state = Arr::wrap($state ?? []);

        $recordsToDetach = array_diff(
            $relatedRecords
                ->pluck($relationship->getRelatedKeyName())
                ->map(static fn ($key): string => strval($key))
                ->all(),
            $state,
        );

        if (count($recordsToDetach) > 0) {
            $relationship->detach($recordsToDetach);
        }

        $pivotData = $this->getPivotData();

        if ($pivotData === []) {
            $relationship->sync($state, detaching: false);
            $record->unsetRelation($relationshipName);

            return;
        }

        $relationship->syncWithPivotValues($state, $pivotData, detaching: false);
        $record->unsetRelation($relationshipName);
    }

    /**
     * @internal Do not use this method outside the internals of Filament. It is subject to breaking changes in minor and patch releases.
     */
    public function applySearchConstraint(Builder $query, string $search): Builder
    {
        /** @var Connection $databaseConnection */
        $databaseConnection = $query->getConnection();

        $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive();

        $query->where(function (Builder $query) use ($databaseConnection, $isForcedCaseInsensitive, $search): Builder {
            $isFirst = true;

            foreach ($this->getSearchColumns() ?? [] as $searchColumn) {
                $whereClause = $isFirst ? 'where' : 'orWhere';

                $query->{$whereClause}(
                    generate_search_column_expression($searchColumn, $isForcedCaseInsensitive, $databaseConnection),
                    'like',
                    "%{$search}%",
                );

                $isFirst = false;
            }

            return $query;
        });

        return $query;
    }

    public function getOptionLabelFromRecordUsing(?Closure $callback): static
    {
        $this->getOptionLabelFromRecordUsing = $callback;

        return $this;
    }

    public function hasOptionLabelFromRecordUsingCallback(): bool
    {
        return $this->getOptionLabelFromRecordUsing !== null;
    }

    public function getOptionLabelFromRecord(Model $record): string
    {
        return $this->evaluate(
            $this->getOptionLabelFromRecordUsing,
            namedInjections: [
                'record' => $record,
            ],
            typedInjections: [
                Model::class => $record,
                $record::class => $record,
            ],
        );
    }

    public function getRelationshipTitleAttribute(): ?string
    {
        return $this->evaluate($this->relationshipTitleAttribute);
    }

    public function getLabel(): string | Htmlable | null
    {
        if ($this->label === null && $this->hasRelationship()) {
            $label = (string) str($this->getRelationshipName())
                ->before('.')
                ->kebab()
                ->replace(['-', '_'], ' ')
                ->ucfirst();

            return ($this->shouldTranslateLabel) ? __($label) : $label;
        }

        return parent::getLabel();
    }

    public function getRelationship(): BelongsTo | BelongsToMany | HasOneOrMany | HasOneOrManyThrough | BelongsToThrough | null
    {
        if (! $this->hasRelationship()) {
            return null;
        }

        $record = $this->getModelInstance();

        $relationship = null;

        $relationshipName = $this->getRelationshipName();

        foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
            if ($record->hasAttribute($nestedRelationshipName)) {
                $relationship = null;

                break;
            }

            if (! $record->isRelation($nestedRelationshipName)) {
                $relationship = null;

                break;
            }

            $relationship = $record->{$nestedRelationshipName}();
            $record = $relationship->getRelated();
        }

        if (! $relationship) {
            throw new LogicException("The relationship [{$relationshipName}] does not exist on the model [{$this->getModel()}].");
        }

        return $relationship;
    }

    public function getRelationshipName(): ?string
    {
        return $this->evaluate($this->relationship);
    }

    public function getSelectedRecord(): ?Model
    {
        if ($this->cachedSelectedRecord) {
            return $this->cachedSelectedRecord;
        }

        if (blank($this->getState())) {
            return null;
        }

        return $this->cachedSelectedRecord = $this->evaluate($this->getSelectedRecordUsing);
    }

    public function hasRelationship(): bool
    {
        return filled($this->getRelationshipName());
    }

    public function dynamicOptions(bool | Closure | null $condition = true): static
    {
        $this->hasDynamicOptions = $condition;

        return $this;
    }

    public function hasDynamicOptions(): bool
    {
        if (($condition = $this->evaluate($this->hasDynamicOptions)) !== null) {
            return $condition;
        }

        if ($this->hasDynamicDisabledOptions()) {
            return true;
        }

        if ($this->hasRelationship()) {
            return (! $this->isSearchable()) || $this->isPreloaded();
        }

        if ($this->isPreloaded()) {
            return true;
        }

        return $this->options instanceof Closure;
    }

    public function hasDynamicSearchResults(): bool
    {
        if ($this->hasRelationship() && blank($this->getSearchColumns())) {
            return false;
        }

        return $this->getSearchResultsUsing instanceof Closure;
    }

    public function hasInitialNoOptionsMessage(): bool
    {
        if ($this->hasRelationship()) {
            return (! $this->isSearchable()) || $this->isPreloaded();
        }

        if ($this->isPreloaded()) {
            return true;
        }

        return ! $this->hasDynamicSearchResults();
    }

    /**
     * @return Model | array<string, mixed> | class-string<Model> | null
     */
    public function getActionSchemaModel(): Model | array | string | null
    {
        if ($this->hasRelationship()) {
            return $this->getRelationship()->getModel()::class;
        }

        return parent::getActionSchemaModel();
    }

    public function getOptionsLimit(): int
    {
        return $this->evaluate($this->optionsLimit);
    }

    public function getMaxItemsMessage(): string
    {
        $maxItems = $this->getMaxItems();

        return $this->evaluate($this->maxItemsMessage) ?? trans_choice('filament-forms::components.select.max_items_message', $maxItems, [
            ':count' => $maxItems,
        ]);
    }

    public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static
    {
        $this->isSearchForcedCaseInsensitive = $condition;

        return $this;
    }

    public function isSearchForcedCaseInsensitive(): ?bool
    {
        return $this->evaluate($this->isSearchForcedCaseInsensitive);
    }

    public function wrapOptionLabels(bool | Closure $condition = true): static
    {
        $this->canOptionLabelsWrap = $condition;

        return $this;
    }

    public function canOptionLabelsWrap(): bool
    {
        return (bool) $this->evaluate($this->canOptionLabelsWrap);
    }

    public function getQualifiedRelatedKeyNameForRelationship(Relation $relationship): string
    {
        if ($relationship instanceof BelongsToMany) {
            return $relationship->getQualifiedRelatedKeyName();
        }

        if ($relationship instanceof HasOneOrManyThrough) {
            return $relationship->getQualifiedForeignKeyName();
        }

        if (
            ($relationship instanceof HasOneOrMany) ||
            ($relationship instanceof BelongsToThrough)
        ) {
            return $relationship->getRelated()->getQualifiedKeyName();
        }

        /** @var BelongsTo $relationship */

        return $relationship->getQualifiedOwnerKeyName();
    }

    public function refreshSelectedOptionLabel(): void
    {
        $livewire = $this->getLivewire();

        $livewire->dispatch(
            'filament-forms::select.refreshSelectedOptionLabel',
            livewireId: $livewire->getId(),
            statePath: $this->getStatePath(),
        );
    }

    public function getEnumDefaultStateCast(): ?StateCast
    {
        $enum = $this->getEnum();

        if (blank($enum)) {
            return null;
        }

        return app(
            $this->isMultiple() ? EnumArrayStateCast::class : EnumStateCast::class,
            ['enum' => $enum],
        );
    }

    /**
     * @return array<StateCast>
     */
    public function getDefaultStateCasts(): array
    {
        if ($this->hasCustomStateCasts() || filled($this->getEnum())) {
            return parent::getDefaultStateCasts();
        }

        if ($this->isMultiple()) {
            return [app(OptionsArrayStateCast::class)];
        }

        return [app(OptionStateCast::class, ['isNullable' => true])];
    }

    /**
     * @return ?array<string>
     */
    public function getInValidationRuleValues(): ?array
    {
        $values = parent::getInValidationRuleValues();

        if ($values !== null) {
            return $values;
        }

        if ($this->isMultiple()) {
            if ((! $this->getOptionLabelsUsing) && ($this->options === null)) {
                throw new LogicException("Filament failed to validate the [{$this->getStatePath()}] field\'s selected options because it did not have an [options()] or [getOptionLabelsUsing()] configuration. Please use one of these methods to inform Filament which options are valid for this field.");
            }

            $state = $this->getState();

            if (blank($state)) {
                return null;
            }

            $optionLabels = $this->getOptionLabels(withDefaults: false);

            if (count($state) > count($optionLabels)) {
                return [];
            }

            $state = array_map(
                static fn (mixed $value): mixed => ($value instanceof BackedEnum) ? $value->value : $value,
                $state,
            );

            if (count(array_diff($state, array_keys($optionLabels)))) {
                return [];
            }

            if ($this->hasDisabledOptions()) {
                foreach ($optionLabels as $optionValue => $optionLabel) {
                    if ($this->isOptionDisabled($optionValue, $optionLabel)) {
                        return [];
                    }
                }
            }

            return null;
        }

        if ((! $this->getOptionLabelUsing) && ($this->options === null)) {
            throw new LogicException("Filament failed to validate the [{$this->getStatePath()}] field\'s selected options because it did not have an [options()] or [getOptionLabelUsing()] configuration. Please use one of these methods to inform Filament which options are valid for this field.");
        }

        $state = $this->getState();

        if (blank($state)) {
            return null;
        }

        $optionLabel = $this->getOptionLabel(withDefault: false);

        if (blank($optionLabel)) {
            return [];
        }

        if ($state instanceof BackedEnum) {
            $state = $state->value;
        }

        if ($this->hasDisabledOptions() && $this->isOptionDisabled($state, $optionLabel)) {
            return [];
        }

        return null;
    }

    public function hasInValidationOnMultipleValues(): bool
    {
        return $this->isMultiple();
    }

    public function hasNullableBooleanState(): bool
    {
        return true;
    }

    public function toEmbeddedHtml(): string
    {
        $extraInputAttributeBag = $this->getExtraInputAttributeBag();
        $canSelectPlaceholder = $this->canSelectPlaceholder();
        $isAutofocused = $this->isAutofocused();
        $isDisabled = $this->isDisabled();
        $isMultiple = $this->isMultiple();
        $isReorderable = $this->isReorderable();
        $isSearchable = $this->isSearchable();
        $hasInitialNoOptionsMessage = $this->hasInitialNoOptionsMessage();
        $canOptionLabelsWrap = $this->canOptionLabelsWrap();
        $isRequired = $this->isRequired();
        $isHtmlAllowed = $this->isHtmlAllowed();
        $isNative = (! ($isSearchable || $isMultiple || $isHtmlAllowed) && $this->isNative());
        $isPrefixInline = $this->isPrefixInline();
        $isSuffixInline = $this->isSuffixInline();
        $key = $this->getKey();
        $id = $this->getId();
        $prefixActions = $this->getPrefixActions();
        $prefixIcon = $this->getPrefixIcon();
        $prefixIconColor = $this->getPrefixIconColor();
        $prefixLabel = $this->getPrefixLabel();
        $suffixActions = $this->getSuffixActions();
        $suffixIcon = $this->getSuffixIcon();
        $suffixIconColor = $this->getSuffixIconColor();
        $suffixLabel = $this->getSuffixLabel();
        $statePath = $this->getStatePath();
        $state = $this->getRawState();
        $livewireKey = $this->getLivewireKey();
        $hasDynamicOptions = $this->hasDynamicOptions();

        // Filter visible prefix/suffix actions
        $prefixActions = array_filter(
            $prefixActions,
            static fn (Action $action): bool => $action->isVisible(),
        );
        $suffixActions = array_filter(
            $suffixActions,
            static fn (Action $action): bool => $action->isVisible(),
        );

        $hasPrefix = count($prefixActions) || $prefixIcon || filled($prefixLabel);

        $wrapperAttributes = $this->getExtraAttributeBag()
            ->merge([
                'x-on:focus-input.stop' => $isNative
                    ? "\$el.querySelector('select')?.focus()"
                    : "\$el.querySelector('.fi-select-input-btn')?.focus()",
            ], escape: false)
            ->class([
                'fi-fo-select',
                'fi-fo-select-has-inline-prefix' => $isPrefixInline && $hasPrefix,
                'fi-fo-select-native' => $isNative,
            ]);

        ob_start(); ?>


                <?php if ($isNative) { ?>
                    <?php $options = $this->getOptions(); ?>

                    <select
                        <?= $extraInputAttributeBag
                            ->merge([
                                'autofocus' => $isAutofocused,
                                'disabled' => $isDisabled,
                                'id' => $id,
                                'required' => $isRequired,
                                'wire:key' => $hasDynamicOptions ? ($livewireKey . '.' . substr(md5(serialize($options)), 0, 64)) : null,
                                $this->applyStateBindingModifiers('wire:model') => $statePath,
                            ], escape: false)
                            ->class([
                                'fi-select-input',
                                'fi-select-input-has-inline-prefix' => $isPrefixInline && $hasPrefix,
                            ])
                            ->toHtml() ?>
                    >
                        <?php if ($canSelectPlaceholder) { ?>
                            <option value="">
                                <?php if (! $isDisabled) { ?>
                                    <?= e($this->getPlaceholder()) ?>
                                <?php } ?>
                            </option>
                        <?php } ?>

                        <?php foreach ($options as $value => $label) { ?>
                            <?php if (is_array($label)) { ?>
                                <optgroup label="<?= e($value) ?>">
                                    <?php foreach ($label as $groupedValue => $groupedLabel) { ?>
                                        <option
                                            <?php if ($this->isOptionDisabled($groupedValue, $groupedLabel)) { ?> disabled <?php } ?>
                                            value="<?= e($groupedValue) ?>"
                                        >
                                            <?= e($groupedLabel) ?>
                                        </option>
                                    <?php } ?>
                                </optgroup>
                            <?php } else { ?>
                                <option
                                    <?php if ($this->isOptionDisabled($value, $label)) { ?> disabled <?php } ?>
                                    value="<?= e($value) ?>"
                                >
                                    <?= e($label) ?>
                                </option>
                            <?php } ?>
                        <?php } ?>
                    </select>
                <?php } else { ?>
                    <?php $options = $this->getOptions(); ?>

                    <div
                        class="fi-hidden"
                        x-data="{
                            isDisabled: <?= Js::from($isDisabled) ?>,
                            init() {
                                const container = $el.nextElementSibling
                                container.dispatchEvent(
                                    new CustomEvent('set-select-property', {
                                        detail: { isDisabled: this.isDisabled },
                                    }),
                                )
                            },
                        }"
                    ></div>
                    <div
                        x-load
                        x-load-src="<?= e(FilamentAsset::getAlpineComponentSrc('select', 'filament/forms')) ?>"
                        x-data="selectFormComponent({
                                    canOptionLabelsWrap: <?= Js::from($canOptionLabelsWrap) ?>,
                                    canSelectPlaceholder: <?= Js::from($canSelectPlaceholder) ?>,
                                    clearButtonLabel: <?= Js::from(__('filament-forms::components.select.actions.clear.label')) ?>,
                                    getOptionLabelUsing: async () => {
                                        return await Livewire.fireAction(
                                            $wire.__instance,
                                            'callSchemaComponentMethod',
                                            [<?= Js::from($key) ?>, 'getOptionLabel'],
                                            { async: true },
                                        )
                                    },
                                    getOptionLabelsUsing: async () => {
                                        return await Livewire.fireAction(
                                            $wire.__instance,
                                            'callSchemaComponentMethod',
                                            [<?= Js::from($key) ?>, 'getOptionLabelsForJs'],
                                            { async: true },
                                        )
                                    },
                                    getOptionsUsing: async () => {
                                        return await Livewire.fireAction(
                                            $wire.__instance,
                                            'callSchemaComponentMethod',
                                            [<?= Js::from($key) ?>, 'getOptionsForJs'],
                                            { async: true },
                                        )
                                    },
                                    getSearchResultsUsing: async (search) => {
                                        return await Livewire.fireAction(
                                            $wire.__instance,
                                            'callSchemaComponentMethod',
                                            [<?= Js::from($key) ?>, 'getSearchResultsForJs', { search }],
                                            { async: true },
                                        )
                                    },
                                    hasDynamicOptions: <?= Js::from($hasDynamicOptions) ?>,
                                    hasDynamicSearchResults: <?= Js::from($this->hasDynamicSearchResults()) ?>,
                                    hasInitialNoOptionsMessage: <?= Js::from($hasInitialNoOptionsMessage) ?>,
                                    id: <?= Js::from($id) ?>,
                                    initialOptionLabel: <?= Js::from((blank($state) || $isMultiple) ? null : $this->resolveOptionLabel(options: $options)) ?>,
                                    initialOptionLabels: <?= Js::from((filled($state) && $isMultiple) ? $this->transformOptionsForJs($this->getOptionLabels(options: $options)) : []) ?>,
                                    initialState: <?= Js::from($state) ?>,
                                    isAutofocused: <?= Js::from($isAutofocused) ?>,
                                    isDisabled: <?= Js::from($isDisabled) ?>,
                                    isHtmlAllowed: <?= Js::from($isHtmlAllowed) ?>,
                                    isMultiple: <?= Js::from($isMultiple) ?>,
                                    isReorderable: <?= Js::from($isReorderable) ?>,
                                    isSearchable: <?= Js::from($isSearchable) ?>,
                                    livewireId: <?= Js::from($this->getLivewire()->getId()) ?>,
                                    loadingMessage: <?= Js::from($this->getLoadingMessage()) ?>,
                                    maxItems: <?= Js::from($this->getMaxItems()) ?>,
                                    maxItemsMessage: <?= Js::from($this->getMaxItemsMessage()) ?>,
                                    noOptionsMessage: <?= Js::from($this->getNoOptionsMessage()) ?>,
                                    noSearchResultsMessage: <?= Js::from($this->getNoSearchResultsMessage()) ?>,
                                    options: <?= Js::from($this->transformOptionsForJs($options)) ?>,
                                    optionsLimit: <?= Js::from($this->getOptionsLimit()) ?>,
                                    placeholder: <?= Js::from($this->getPlaceholder()) ?>,
                                    position: <?= Js::from($this->getPosition()) ?>,
                                    removeButtonLabel: <?= Js::from(__('filament-forms::components.select.actions.remove_option.label')) ?>,
                                    searchDebounce: <?= Js::from($this->getSearchDebounce()) ?>,
                                    searchingMessage: <?= Js::from($this->getSearchingMessage()) ?>,
                                    searchLabel: <?= Js::from(__('filament-forms::components.select.search_label')) ?>,
                                    searchPrompt: <?= Js::from($this->getSearchPrompt()) ?>,
                                    searchableOptionFields: <?= Js::from($this->getSearchableOptionFields()) ?>,
                                    state: $wire.<?= $this->applyStateBindingModifiers("\$entangle('{$statePath}')") ?>,
                                    statePath: <?= Js::from($statePath) ?>,
                                })"
                        wire:ignore
                        wire:key="<?= e($livewireKey) ?>.<?= substr(md5(serialize([$isDisabled, $isReorderable])), 0, 64) ?>"
                        x-on:keydown.esc="select.dropdown.isActive && $event.stopPropagation()"
                        x-on:set-select-property="$event.detail.isDisabled ? select.disable() : select.enable()"
                        <?= (new FilamentComponentAttributeBag)
                            ->merge($this->getExtraAlpineAttributes(), escape: false)
                            ->class(['fi-select-input'])
                            ->toHtml() ?>
                    >
                        <div x-ref="select"></div>
                    </div>
                <?php } ?>

        <?php $slotHtml = ob_get_clean();

        return $this->wrapEmbeddedHtml(
            $this->wrapInputHtml(
                $slotHtml,
                attributes: $wrapperAttributes,
            ),
            extraWrapperAttributes: ['class' => 'fi-fo-select-wrp'],
        );
    }
}
