<?php

declare(strict_types=1);

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

namespace App\Business\Inventory;

use App\Business\Inventory\Enums\InventorySerialNumberStatus;
use App\Business\Inventory\Enums\StocktakeDifferenceStatus;
use App\Business\Inventory\Enums\StocktakeSerialNumberDifferenceType;
use App\Models\DberpInventorySerialNumber;
use App\Models\DberpProduct;
use App\Models\DberpStocktakeOrder;
use App\Models\DberpStocktakeOrderItem;
use App\Models\DberpStocktakeSerialNumberDifference;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

/**
 * 盘点序列号差异：对比实盘序列号与仓库在库序列号，记录差异供后续治理。
 */
final class StocktakeSerialNumberDifferenceService
{
    public function __construct(
        private readonly InventorySerialNumberService $serialNumberService,
        private readonly InventoryRefinementService $refinementService,
    ) {}

    public function validateStocktakeItemSerialNumbers(DberpStocktakeOrderItem $item): void
    {
        $product = $item->product;

        if (! $product instanceof DberpProduct || ! $product->manages_serial_number) {
            return;
        }

        $quantity = (string) ($item->base_actual_quantity ?? $item->actual_quantity);
        $formattedQuantity = number_format((float) $quantity, 4, '.', '');
        $normalizedSerials = $this->serialNumberService->normalizeSerialNumbers($item->serial_numbers);

        if (bccomp($formattedQuantity, '0', 4) === 0) {
            if ($normalizedSerials !== []) {
                throw ValidationException::withMessages([
                    'serial_numbers' => __('dberp.errors.stocktake_serial_numbers_must_be_empty_when_zero_quantity', [
                        'product_name' => $product->name,
                    ]),
                ]);
            }

            return;
        }

        if (bccomp($formattedQuantity, '0', 4) === -1) {
            throw ValidationException::withMessages([
                'actual_quantity' => __('dberp.errors.stocktake_actual_negative'),
            ]);
        }

        $this->refinementService->assertDocumentLineSerialNumbersMatchQuantity(
            $product,
            $quantity,
            $item->serial_numbers,
        );
    }

    public function syncFromStocktakeItem(DberpStocktakeOrder $order, DberpStocktakeOrderItem $item): void
    {
        $product = $item->product;

        if (! $product instanceof DberpProduct || ! $product->manages_serial_number) {
            DberpStocktakeSerialNumberDifference::query()
                ->where('stocktake_order_item_id', $item->getKey())
                ->delete();

            return;
        }

        $warehouseId = (int) $order->warehouse_id;
        $actualSerials = $this->serialNumberService->normalizeSerialNumbers($item->serial_numbers);

        $recordsBySerial = $this->loadSerialRecordsForActual($actualSerials);

        $expectedInStock = $this->expectedInStockSerials($product, $warehouseId);

        $payloads = [];

        foreach ($expectedInStock as $serialNo) {
            if (in_array($serialNo, $actualSerials, true)) {
                continue;
            }

            $record = DberpInventorySerialNumber::query()
                ->where('product_id', $product->getKey())
                ->where('warehouse_id', $warehouseId)
                ->where('serial_no', $serialNo)
                ->first();

            $payloads[] = $this->buildPayload(
                order: $order,
                item: $item,
                serialNo: $serialNo,
                type: StocktakeSerialNumberDifferenceType::MissingInActual,
                record: $record,
                countedInActual: false,
            );
        }

        foreach ($actualSerials as $serialNo) {
            if (in_array($serialNo, $expectedInStock, true)) {
                continue;
            }

            $record = $recordsBySerial->get($serialNo);

            if ($record === null) {
                $payloads[] = $this->buildPayload(
                    order: $order,
                    item: $item,
                    serialNo: $serialNo,
                    type: StocktakeSerialNumberDifferenceType::ExtraInActual,
                    record: null,
                    countedInActual: true,
                );

                continue;
            }

            if ((int) $record->product_id !== (int) $product->getKey()) {
                $payloads[] = $this->buildPayload(
                    order: $order,
                    item: $item,
                    serialNo: $serialNo,
                    type: StocktakeSerialNumberDifferenceType::ExtraInActual,
                    record: $record,
                    countedInActual: true,
                );

                continue;
            }

            if ($this->isDisabledOrVoided($record)) {
                $payloads[] = $this->buildPayload(
                    order: $order,
                    item: $item,
                    serialNo: $serialNo,
                    type: StocktakeSerialNumberDifferenceType::DisabledOrVoided,
                    record: $record,
                    countedInActual: true,
                );

                continue;
            }

            if ((int) $record->warehouse_id !== $warehouseId) {
                $payloads[] = $this->buildPayload(
                    order: $order,
                    item: $item,
                    serialNo: $serialNo,
                    type: StocktakeSerialNumberDifferenceType::WrongWarehouse,
                    record: $record,
                    countedInActual: true,
                );

                continue;
            }

            if ($record->status !== InventorySerialNumberStatus::InStock->value) {
                $payloads[] = $this->buildPayload(
                    order: $order,
                    item: $item,
                    serialNo: $serialNo,
                    type: StocktakeSerialNumberDifferenceType::NotInStock,
                    record: $record,
                    countedInActual: true,
                );
            }
        }

        DberpStocktakeSerialNumberDifference::query()
            ->where('stocktake_order_item_id', $item->getKey())
            ->delete();

        foreach ($payloads as $index => $payload) {
            DberpStocktakeSerialNumberDifference::query()->create([
                ...$payload,
                'sort_order' => $index,
            ]);
        }
    }

    public function resolveVoidedOrder(DberpStocktakeOrder $order): void
    {
        DberpStocktakeSerialNumberDifference::query()
            ->where('stocktake_order_id', $order->getKey())
            ->update([
                'status' => StocktakeDifferenceStatus::Resolved->value,
            ]);
    }

    /**
     * @param  list<string>  $actualSerials
     * @return Collection<string, DberpInventorySerialNumber>
     */
    private function loadSerialRecordsForActual(array $actualSerials): Collection
    {
        if ($actualSerials === []) {
            return collect();
        }

        return DberpInventorySerialNumber::query()
            ->whereIn('serial_no', $actualSerials)
            ->get()
            ->keyBy(fn (DberpInventorySerialNumber $record): string => Str::upper((string) $record->serial_no));
    }

    /**
     * @return list<string>
     */
    private function expectedInStockSerials(DberpProduct $product, int $warehouseId): array
    {
        return DberpInventorySerialNumber::query()
            ->where('product_id', $product->getKey())
            ->where('warehouse_id', $warehouseId)
            ->where('status', InventorySerialNumberStatus::InStock->value)
            ->where('is_enabled', true)
            ->orderBy('sort_order')
            ->orderBy('id')
            ->pluck('serial_no')
            ->map(fn (mixed $serialNo): string => Str::upper(trim((string) $serialNo)))
            ->all();
    }

    private function isDisabledOrVoided(DberpInventorySerialNumber $record): bool
    {
        if (! $record->is_enabled) {
            return true;
        }

        return in_array((string) $record->status, [
            InventorySerialNumberStatus::Disabled->value,
            InventorySerialNumberStatus::Voided->value,
        ], true);
    }

    /**
     * @return array<string, mixed>
     */
    private function buildPayload(
        DberpStocktakeOrder $order,
        DberpStocktakeOrderItem $item,
        string $serialNo,
        StocktakeSerialNumberDifferenceType $type,
        ?DberpInventorySerialNumber $record,
        bool $countedInActual,
    ): array {
        return [
            'stocktake_order_id' => $order->getKey(),
            'stocktake_order_item_id' => $item->getKey(),
            'product_id' => $item->product_id,
            'product_sku_id' => $item->product_sku_id,
            'warehouse_id' => $order->warehouse_id,
            'serial_no' => $serialNo,
            'difference_type' => $type->value,
            'system_status' => $record?->status,
            'system_warehouse_id' => $record?->warehouse_id,
            'system_is_enabled' => $record?->is_enabled,
            'counted_in_actual' => $countedInActual,
            'status' => StocktakeDifferenceStatus::Pending->value,
        ];
    }
}
