<?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\InventoryBusinessType;
use App\Models\DberpInventoryRule;
use App\Models\DberpProduct;
use App\Models\DberpWarehouse;
use Carbon\Carbon;
use Illuminate\Validation\ValidationException;

final class InventoryRefinementService
{
    public function __construct(
        private readonly InventoryBatchService $batchService,
        private readonly InventorySerialNumberService $serialNumberService,
        private readonly InventoryReplenishmentSuggestionService $replenishmentSuggestionService,
    ) {}

    /**
     * 单据过账前校验：启用序列号管理的商品，过账数量须为正整数且与规范化后的序列号条数一致。
     *
     * @param  array<int, string>|string|null  $serialNumbers
     */
    public function assertDocumentLineSerialNumbersMatchQuantity(
        DberpProduct $product,
        string $quantity,
        array|string|null $serialNumbers,
    ): void {
        if (! $product->manages_serial_number) {
            return;
        }

        $formattedQuantity = number_format((float) $quantity, 4, '.', '');

        if (bccomp($formattedQuantity, '0', 4) !== 1) {
            return;
        }

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

        $expectedCount = (int) (float) $formattedQuantity;
        $serialCount = count($this->serialNumberService->normalizeSerialNumbers($serialNumbers));

        if ($serialCount !== $expectedCount) {
            throw ValidationException::withMessages([
                'serial_numbers' => __('dberp.errors.inventory_serial_number_count_must_match_quantity', [
                    'product_name' => $product->name,
                    'quantity' => (string) $expectedCount,
                    'serial_count' => (string) $serialCount,
                ]),
            ]);
        }
    }

    /**
     * @param  array<string, mixed>  $detail
     * @return array{batch_id:int|null,batch_no:string|null,batch_allocations:array<int, array<string, mixed>>,serial_numbers:array<int, string>}
     */
    public function applyInbound(
        DberpProduct $product,
        DberpWarehouse $warehouse,
        string $quantity,
        InventoryBusinessType $businessType,
        string $businessNumber,
        string $sourceType,
        int $sourceId,
        ?string $remark,
        \DateTimeInterface $businessTime,
        array $detail = [],
    ): array {
        $this->assertDocumentLineSerialNumbersMatchQuantity(
            $product,
            $quantity,
            $detail['serial_numbers'] ?? [],
        );

        $batch = $this->batchService->receive(
            product: $product,
            warehouse: $warehouse,
            quantity: $quantity,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
            batchNo: $detail['batch_no'] ?? null,
            productionDate: $detail['production_date'] ?? null,
            expiryDate: $detail['expiry_date'] ?? null,
            shelfLifeDays: (int) ($detail['shelf_life_days'] ?? 0),
        );

        $serialNumbers = $this->serialNumberService->registerInbound(
            product: $product,
            warehouse: $warehouse,
            batch: $batch,
            serialNumbers: $detail['serial_numbers'] ?? [],
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
        );

        $this->refreshReplenishmentSuggestion($product, $warehouse);

        return [
            'batch_id' => $batch?->getKey(),
            'batch_no' => $batch?->batch_no,
            'batch_allocations' => $batch !== null ? [[
                'batch_id' => $batch->getKey(),
                'batch_no' => $batch->batch_no,
                'quantity' => number_format((float) $quantity, 4, '.', ''),
                'production_date' => $this->snapshotDate($batch->getAttribute('production_date')),
                'expiry_date' => $this->snapshotDate($batch->getAttribute('expiry_date')),
                'shelf_life_days' => (int) $batch->shelf_life_days,
            ]] : [],
            'serial_numbers' => $serialNumbers,
        ];
    }

    /**
     * @param  array<string, mixed>  $detail
     * @return array{batch_id:int|null,batch_no:string|null,batch_allocations:array<int, array<string, mixed>>,serial_numbers:array<int, string>}
     */
    public function applyOutbound(
        DberpProduct $product,
        DberpWarehouse $warehouse,
        string $quantity,
        InventoryBusinessType $businessType,
        string $businessNumber,
        string $sourceType,
        int $sourceId,
        ?string $remark,
        \DateTimeInterface $businessTime,
        array $detail = [],
        bool $enforceSaleExpiry = false,
    ): array {
        $this->assertDocumentLineSerialNumbersMatchQuantity(
            $product,
            $quantity,
            $detail['serial_numbers'] ?? [],
        );

        $allocations = $this->batchService->issue(
            product: $product,
            warehouse: $warehouse,
            quantity: $quantity,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
            sourceBatchId: isset($detail['source_batch_id']) ? (int) $detail['source_batch_id'] : null,
            batchNo: $detail['batch_no'] ?? null,
            batchAllocations: $detail['batch_allocations'] ?? [],
            issuePolicy: (string) ($product->inventory_issue_policy ?? 'fifo'),
            enforceSaleExpiry: $enforceSaleExpiry,
        );

        $serialNumbers = $this->serialNumberService->issueOutbound(
            product: $product,
            warehouse: $warehouse,
            serialNumbers: $detail['serial_numbers'] ?? [],
            batchIds: array_values(array_filter(array_map(fn (array $allocation): ?int => isset($allocation['batch_id']) ? (int) $allocation['batch_id'] : null, $allocations))),
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
        );

        $this->refreshReplenishmentSuggestion($product, $warehouse);

        return [
            'batch_id' => count($allocations) === 1 ? (int) ($allocations[0]['batch_id'] ?? 0) ?: null : null,
            'batch_no' => count($allocations) === 1 ? (($allocations[0]['batch_no'] ?? null) ?: null) : null,
            'batch_allocations' => $allocations,
            'serial_numbers' => $serialNumbers,
        ];
    }

    /**
     * 调拨调入：总账已由 {@see InventoryLedgerService::inbound} 过账；此处补写调入仓批次，并把序列号迁入调入仓在库态（不创建新序列号档案）。
     *
     * @param  array<string, mixed>  $detail  需含从调出分配快照推导的 batch_no、production_date、expiry_date、shelf_life_days；含 serial_numbers（若商品启用序列号）。
     * @return array{batch_id:int|null,batch_no:string|null,batch_allocations:array<int, array<string, mixed>>,serial_numbers:array<int, string>}
     */
    public function applyTransferInbound(
        DberpProduct $product,
        DberpWarehouse $warehouse,
        string $quantity,
        InventoryBusinessType $businessType,
        string $businessNumber,
        string $sourceType,
        int $sourceId,
        ?string $remark,
        \DateTimeInterface $businessTime,
        array $detail = [],
    ): array {
        if (! $product->manages_batch && ! $product->manages_serial_number) {
            return [
                'batch_id' => null,
                'batch_no' => null,
                'batch_allocations' => [],
                'serial_numbers' => [],
            ];
        }

        $batchNo = $detail['batch_no'] ?? null;
        $batch = $this->batchService->receive(
            product: $product,
            warehouse: $warehouse,
            quantity: $quantity,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
            batchNo: is_string($batchNo) ? $batchNo : null,
            productionDate: $detail['production_date'] ?? null,
            expiryDate: $detail['expiry_date'] ?? null,
            shelfLifeDays: (int) ($detail['shelf_life_days'] ?? 0),
        );

        $serialNumbers = [];
        if ($product->manages_serial_number) {
            $this->assertDocumentLineSerialNumbersMatchQuantity(
                $product,
                $quantity,
                $detail['serial_numbers'] ?? [],
            );

            $serialNumbers = $this->serialNumberService->normalizeSerialNumbers($detail['serial_numbers'] ?? []);

            $this->serialNumberService->restoreInbound(
                product: $product,
                warehouse: $warehouse,
                serialNumbers: $serialNumbers,
                batchId: $batch?->getKey(),
                businessType: $businessType,
                businessNumber: $businessNumber,
                sourceType: $sourceType,
                sourceId: $sourceId,
                remark: $remark,
                businessTime: $businessTime,
            );
        }

        $allocations = [];
        if ($batch !== null) {
            $allocations = [[
                'batch_id' => $batch->getKey(),
                'batch_no' => $batch->batch_no,
                'quantity' => number_format((float) $quantity, 4, '.', ''),
                'production_date' => $this->snapshotDate($batch->getAttribute('production_date')),
                'expiry_date' => $this->snapshotDate($batch->getAttribute('expiry_date')),
                'shelf_life_days' => (int) $batch->shelf_life_days,
            ]];
        }

        $this->refreshReplenishmentSuggestion($product, $warehouse);

        return [
            'batch_id' => $batch?->getKey(),
            'batch_no' => $batch?->batch_no,
            'batch_allocations' => $allocations,
            'serial_numbers' => $serialNumbers,
        ];
    }

    /**
     * @param  array<string, mixed>  $detail
     */
    public function rollbackInbound(
        DberpProduct $product,
        DberpWarehouse $warehouse,
        string $quantity,
        InventoryBusinessType $businessType,
        string $businessNumber,
        string $sourceType,
        int $sourceId,
        ?string $remark,
        \DateTimeInterface $businessTime,
        array $detail = [],
    ): void {
        $this->batchService->issue(
            product: $product,
            warehouse: $warehouse,
            quantity: $quantity,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
            sourceBatchId: isset($detail['source_batch_id']) ? (int) $detail['source_batch_id'] : null,
            batchNo: $detail['batch_no'] ?? null,
            batchAllocations: $detail['batch_allocations'] ?? [],
            issuePolicy: (string) ($product->inventory_issue_policy ?? 'fifo'),
            enforceSaleExpiry: false,
        );

        $this->serialNumberService->voidInbound(
            product: $product,
            serialNumbers: $detail['serial_numbers'] ?? [],
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
        );

        $this->refreshReplenishmentSuggestion($product, $warehouse);
    }

    /**
     * @param  array<string, mixed>  $detail
     */
    public function rollbackOutbound(
        DberpProduct $product,
        DberpWarehouse $warehouse,
        string $quantity,
        InventoryBusinessType $businessType,
        string $businessNumber,
        string $sourceType,
        int $sourceId,
        ?string $remark,
        \DateTimeInterface $businessTime,
        array $detail = [],
    ): void {
        $allocations = $detail['batch_allocations'] ?? [];

        if ($product->manages_batch && empty($allocations) && (($detail['batch_no'] ?? null) !== null || ($detail['source_batch_id'] ?? null) !== null)) {
            $allocations = [[
                'batch_id' => $detail['source_batch_id'] ?? null,
                'batch_no' => $detail['batch_no'] ?? null,
                'quantity' => number_format((float) $quantity, 4, '.', ''),
                'production_date' => $detail['production_date'] ?? null,
                'expiry_date' => $detail['expiry_date'] ?? null,
                'shelf_life_days' => (int) ($detail['shelf_life_days'] ?? 0),
            ]];
        }

        $this->batchService->restoreFromAllocations(
            product: $product,
            warehouse: $warehouse,
            allocations: $allocations,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
        );

        $this->serialNumberService->restoreInbound(
            product: $product,
            warehouse: $warehouse,
            serialNumbers: $detail['serial_numbers'] ?? [],
            batchId: count($allocations) === 1 ? (int) ($allocations[0]['batch_id'] ?? 0) ?: null : null,
            businessType: $businessType,
            businessNumber: $businessNumber,
            sourceType: $sourceType,
            sourceId: $sourceId,
            remark: $remark,
            businessTime: $businessTime,
        );

        $this->refreshReplenishmentSuggestion($product, $warehouse);
    }

    private function refreshReplenishmentSuggestion(DberpProduct $product, DberpWarehouse $warehouse): void
    {
        $rule = DberpInventoryRule::query()
            ->where('product_id', $product->getKey())
            ->where('warehouse_id', $warehouse->getKey())
            ->where('is_enabled', true)
            ->first();

        if ($rule !== null) {
            $this->replenishmentSuggestionService->refreshForRule($rule);
        }
    }

    private function snapshotDate(mixed $value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        return Carbon::parse((string) $value)->format('Y-m-d');
    }
}
