<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Support;

use App\Business\Inventory\InventorySerialNumberService;
use App\Filament\Support\DberpFilamentRelationshipSelectLimits;
use App\Models\DberpInventorySerialNumber;
use App\Models\DberpProduct;
use App\Models\DberpWarehouse;
use Closure;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;

/**
 * 出库类单据明细：在库序列号选择 + 已选序列号手工维护（仍写入 serial_numbers JSON）。
 */
final class DberpDocumentLineOutboundSerialNumberFormFields
{
    public const PICKER_FIELD = '_in_stock_serial_picker';

    /**
     * @return list<Field>
     */
    public static function outboundSerialNumberFields(
        Closure $resolveProduct,
        Closure $resolveWarehouseId,
        Closure $resolveSourceBatchId,
        string $quantityField,
    ): array {
        return [
            self::warehouseRequiredPlaceholder($resolveProduct, $resolveWarehouseId),
            self::inStockSerialPicker($resolveProduct, $resolveWarehouseId, $resolveSourceBatchId, $quantityField),
            self::selectedSerialNumbersInput($resolveProduct, $quantityField),
        ];
    }

    /**
     * @param  array<int, string>|string|null  $includeSerialNumbers
     * @return array<string, string>
     */
    public static function searchAvailableSerialOptions(
        ?DberpProduct $product,
        ?int $warehouseId,
        ?int $batchId,
        ?string $search,
        array|string|null $includeSerialNumbers = [],
    ): array {
        if (! $product instanceof DberpProduct || ! $product->manages_serial_number) {
            return [];
        }

        if ($warehouseId === null || $warehouseId <= 0) {
            return [];
        }

        $warehouse = DberpWarehouse::query()->find($warehouseId);

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

        $service = app(InventorySerialNumberService::class);
        $query = $service->availableQuery(
            $product,
            $warehouse,
            $batchId !== null && $batchId > 0 ? $batchId : null,
        );

        if (filled($search)) {
            $query->where($query->qualifyColumn('serial_no'), 'like', '%'.trim($search).'%');
        } else {
            DberpFilamentRelationshipSelectLimits::limitInventorySerialNumberOptionsQuery($query, null);
        }

        $options = self::mapSerialsToOptions($query->get());

        foreach (app(InventorySerialNumberService::class)->normalizeSerialNumbers($includeSerialNumbers) as $serialNo) {
            if (! array_key_exists($serialNo, $options)) {
                $serial = DberpInventorySerialNumber::query()
                    ->where('product_id', $product->getKey())
                    ->where('warehouse_id', $warehouse->getKey())
                    ->where('serial_no', $serialNo)
                    ->first();

                $options[$serialNo] = $serial instanceof DberpInventorySerialNumber
                    ? self::labelForSerial($serial)
                    : $serialNo;
            }
        }

        $selected = app(InventorySerialNumberService::class)->normalizeSerialNumbers($includeSerialNumbers);

        if ($selected !== []) {
            $options = array_diff_key($options, array_flip($selected));
        }

        return $options;
    }

    public static function resetLineSerialState(Set $set): void
    {
        $set('serial_numbers', []);
        $set(self::PICKER_FIELD, []);
    }

    /**
     * 保存明细时合并 Tags 与在库多选器中的序列号（多选器本身不入库）。
     *
     * @return list<string>
     */
    public static function resolveSerialNumbersFormState(Get $get, mixed $tagsState): array
    {
        return self::mergePickerAndTagsSerialNumbers(
            $tagsState,
            $get(self::PICKER_FIELD),
        );
    }

    /**
     * @param  array<int, string>|string|null  $tagsState
     * @param  array<int, string>|string|null  $pickerState
     * @return list<string>
     */
    public static function mergePickerAndTagsSerialNumbers(array|string|null $tagsState, array|string|null $pickerState): array
    {
        $fromTags = DberpDocumentLineSerialNumberFormFields::expandSerialNumberEntries(
            is_array($tagsState) ? $tagsState : [],
        );

        $fromPicker = is_array($pickerState)
            ? DberpDocumentLineSerialNumberFormFields::expandSerialNumberEntries($pickerState)
            : [];

        return app(InventorySerialNumberService::class)->normalizeSerialNumbers(
            array_merge($fromTags, $fromPicker),
        );
    }

    /**
     * 来源批次变更时：仅清空多选器，并按批次裁剪已选序列号（避免先选序列号后选批次时被整表清空）。
     */
    public static function pruneSerialNumbersForSourceBatch(
        Set $set,
        Get $get,
        Closure $resolveProduct,
        Closure $resolveWarehouseId,
    ): void {
        $set(self::PICKER_FIELD, []);

        $serials = $get('serial_numbers');

        if (! is_array($serials) || $serials === []) {
            return;
        }

        $product = $resolveProduct($get);

        if (! $product instanceof DberpProduct || ! DberpProductInventoryRefinementFormFields::productManagesSerial($product)) {
            return;
        }

        $warehouseId = (int) $resolveWarehouseId();

        if ($warehouseId <= 0) {
            return;
        }

        $sourceBatchId = (int) ($get('source_batch_id') ?? 0);

        $set(
            'serial_numbers',
            self::filterSerialNumbersForSourceBatch(
                $product,
                $warehouseId,
                $sourceBatchId,
                DberpDocumentLineSerialNumberFormFields::expandSerialNumberEntries($serials),
            ),
        );
    }

    /**
     * @param  list<string>  $serialNumbers
     * @return list<string>
     */
    public static function filterSerialNumbersForSourceBatch(
        DberpProduct $product,
        int $warehouseId,
        int $sourceBatchId,
        array $serialNumbers,
    ): array {
        $normalized = app(InventorySerialNumberService::class)->normalizeSerialNumbers($serialNumbers);

        if ($normalized === []) {
            return [];
        }

        if ($sourceBatchId <= 0) {
            return $normalized;
        }

        $filtered = [];

        foreach ($normalized as $serialNo) {
            $serial = DberpInventorySerialNumber::query()
                ->where('product_id', $product->getKey())
                ->where('warehouse_id', $warehouseId)
                ->where('serial_no', $serialNo)
                ->first();

            if ($serial instanceof DberpInventorySerialNumber && (int) ($serial->batch_id ?? 0) === $sourceBatchId) {
                $filtered[] = $serialNo;
            }
        }

        return $filtered;
    }

    public static function documentLineQuantityTrimsSerialNumbers(
        TextInput $quantityField,
        Closure $resolveProduct,
    ): TextInput {
        $quantityFieldName = $quantityField->getName();

        return $quantityField->afterStateUpdated(function (mixed $state, Set $set, Get $get) use ($resolveProduct, $quantityFieldName): void {
            self::syncSerialNumbersToQuantity($set, $get, $resolveProduct, $quantityFieldName);
        });
    }

    public static function syncSerialNumbersToQuantity(
        Set $set,
        Get $get,
        Closure $resolveProduct,
        string $quantityField,
    ): void {
        $serials = $get('serial_numbers');

        if (! is_array($serials) || $serials === []) {
            return;
        }

        $maxCount = DberpDocumentLineSerialNumberFormFields::maxSerialCountForQuantity(
            $resolveProduct,
            $quantityField,
        )($get);

        $expanded = DberpDocumentLineSerialNumberFormFields::expandSerialNumberEntries($serials);

        $set(
            'serial_numbers',
            DberpDocumentLineSerialNumberFormFields::trimSerialsToMaxCount($expanded, $maxCount),
        );
    }

    private static function warehouseRequiredPlaceholder(Closure $resolveProduct, Closure $resolveWarehouseId): Placeholder
    {
        return Placeholder::make('_outbound_serial_warehouse_hint')
            ->label(__('dberp.document_line.available_serial_numbers'))
            ->content(__('dberp.document_line.serial_number_no_stock_in_warehouse'))
            ->visible(function (Get $get) use ($resolveProduct, $resolveWarehouseId): bool {
                $product = $resolveProduct($get);

                if (! DberpProductInventoryRefinementFormFields::productManagesSerial($product)) {
                    return false;
                }

                return (int) $resolveWarehouseId() <= 0;
            })
            ->columnSpanFull();
    }

    private static function inStockSerialPicker(
        Closure $resolveProduct,
        Closure $resolveWarehouseId,
        Closure $resolveSourceBatchId,
        string $quantityField,
    ): Select {
        return DberpProductInventoryRefinementFormFields::constrainSerialField(
            Select::make(self::PICKER_FIELD)
                ->label(__('dberp.document_line.select_in_stock_serial_numbers'))
                ->multiple()
                ->dehydrated(false)
                ->searchable()
                ->preload()
                ->options(fn (Get $get): array => self::resolvePickerOptions($get, $resolveProduct, $resolveWarehouseId, $resolveSourceBatchId, ''))
                ->getSearchResultsUsing(fn (string $search, Get $get): array => self::resolvePickerOptions(
                    $get,
                    $resolveProduct,
                    $resolveWarehouseId,
                    $resolveSourceBatchId,
                    $search,
                ))
                ->getOptionLabelsUsing(function (array $values): array {
                    $labels = [];

                    foreach ($values as $serialNo) {
                        $serialNo = (string) $serialNo;
                        $serial = DberpInventorySerialNumber::query()
                            ->where('serial_no', $serialNo)
                            ->first();

                        $labels[$serialNo] = $serial instanceof DberpInventorySerialNumber
                            ? self::labelForSerial($serial)
                            : $serialNo;
                    }

                    return $labels;
                })
                ->disabled(function (Get $get) use ($resolveProduct, $resolveWarehouseId): bool {
                    return self::pickerDisabled($get, $resolveProduct, $resolveWarehouseId);
                })
                ->live()
                ->afterStateUpdated(function (?array $state, Set $set, Get $get) use ($resolveProduct, $quantityField): void {
                    if (! is_array($state) || $state === []) {
                        return;
                    }

                    $product = $resolveProduct($get);

                    if (! DberpProductInventoryRefinementFormFields::productManagesSerial($product)) {
                        return;
                    }

                    $maxCount = DberpDocumentLineSerialNumberFormFields::maxSerialCountForQuantity(
                        $resolveProduct,
                        $quantityField,
                    )($get);

                    $merged = DberpDocumentLineSerialNumberFormFields::trimSerialsToMaxCount(
                        array_merge($get('serial_numbers') ?? [], $state),
                        $maxCount,
                    );

                    $set('serial_numbers', $merged);
                    $set(self::PICKER_FIELD, []);
                })
                ->helperText(function (Get $get) use ($resolveProduct, $resolveWarehouseId, $quantityField): ?string {
                    if (self::pickerDisabled($get, $resolveProduct, $resolveWarehouseId)) {
                        return null;
                    }

                    $product = $resolveProduct($get);

                    if (! DberpProductInventoryRefinementFormFields::productManagesSerial($product)) {
                        return null;
                    }

                    $maxCount = DberpDocumentLineSerialNumberFormFields::maxSerialCountForQuantity(
                        $resolveProduct,
                        $quantityField,
                    )($get);

                    if ($maxCount === null) {
                        return (string) __('dberp.document_line.select_in_stock_serial_numbers_enter_quantity_first');
                    }

                    return (string) __('dberp.document_line.select_in_stock_serial_numbers_helper');
                })
                ->columnSpanFull(),
            $resolveProduct,
        );
    }

    private static function selectedSerialNumbersInput(Closure $resolveProduct, string $quantityField): TagsInput
    {
        return DberpProductInventoryRefinementFormFields::constrainSerialField(
            DberpDocumentLineSerialNumberFormFields::configureTagsInput(
                TagsInput::make('serial_numbers')
                    ->label(__('dberp.document_line.selected_serial_numbers'))
                    ->helperText(__('dberp.document_line.selected_serial_numbers_helper'))
                    ->separator(',')
                    ->splitKeys(['Enter', 'Tab'])
                    ->dehydrated(true)
                    ->dehydrateStateUsing(
                        fn (mixed $state, Get $get): array => self::resolveSerialNumbersFormState($get, $state),
                    )
                    ->columnSpanFull(),
                $resolveProduct,
                $quantityField,
            ),
            $resolveProduct,
        );
    }

    /**
     * @return array<string, string>
     */
    private static function resolvePickerOptions(
        Get $get,
        Closure $resolveProduct,
        Closure $resolveWarehouseId,
        Closure $resolveSourceBatchId,
        string $search,
    ): array {
        if (self::pickerDisabled($get, $resolveProduct, $resolveWarehouseId)) {
            return [];
        }

        $batchId = (int) $resolveSourceBatchId($get);

        return self::searchAvailableSerialOptions(
            $resolveProduct($get),
            (int) $resolveWarehouseId(),
            $batchId > 0 ? $batchId : null,
            $search,
            $get('serial_numbers'),
        );
    }

    private static function pickerDisabled(Get $get, Closure $resolveProduct, Closure $resolveWarehouseId): bool
    {
        $product = $resolveProduct($get);

        if (! DberpProductInventoryRefinementFormFields::productManagesSerial($product)) {
            return true;
        }

        return (int) $resolveWarehouseId() <= 0;
    }

    /**
     * @param  iterable<int, DberpInventorySerialNumber>  $serials
     * @return array<string, string>
     */
    private static function mapSerialsToOptions(iterable $serials): array
    {
        $options = [];

        foreach ($serials as $serial) {
            if ($serial instanceof DberpInventorySerialNumber) {
                $options[(string) $serial->serial_no] = self::labelForSerial($serial);
            }
        }

        return $options;
    }

    public static function labelForSerial(DberpInventorySerialNumber $serial): string
    {
        $serial->loadMissing('batch');

        $batchNo = trim((string) ($serial->batch?->batch_no ?? ''));

        if ($batchNo !== '') {
            return sprintf('%s / %s', (string) $serial->serial_no, $batchNo);
        }

        return (string) $serial->serial_no;
    }
}
