<?php

declare(strict_types=1);

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

namespace App\Business\Purchase;

use App\Business\Inventory\Enums\InventoryBusinessType;
use App\Business\Inventory\Enums\InventoryDocumentStatus;
use App\Business\Inventory\Exceptions\InsufficientStockException;
use App\Business\Inventory\InventoryLedgerLineTrace;
use App\Business\Inventory\InventoryLedgerService;
use App\Business\Inventory\InventoryRefinementService;
use App\Business\Inventory\InventoryRuleService;
use App\Business\Product\DberpProductUnitConversionService;
use App\Business\Product\ProductSkuOperationalGuard;
use App\Business\Purchase\Enums\PurchaseOrderStatus;
use App\Models\DberpInventoryBalance;
use App\Models\DberpPurchaseReceiptOrder;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PurchaseReceiptService
{
    public function __construct(
        private readonly InventoryLedgerService $ledgerService,
        private readonly InventoryRefinementService $refinementService,
        private readonly InventoryRuleService $ruleService,
        private readonly PurchaseOrderService $purchaseOrderService,
        private readonly PurchasePriceReferenceService $purchasePriceReferenceService,
        private readonly PurchaseOrderExecutionService $purchaseOrderExecutionService,
        private readonly PurchaseExceptionService $purchaseExceptionService,
        private readonly DberpAuditService $auditService,
    ) {}

    public function apply(DberpPurchaseReceiptOrder $order): void
    {
        DB::transaction(function () use ($order): void {
            $locked = DberpPurchaseReceiptOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
            $locked->loadMissing('purchaseOrder', 'warehouse');

            if ($locked->status !== InventoryDocumentStatus::Draft->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.inventory_doc_not_draft'),
                ]);
            }

            if (! in_array($locked->purchaseOrder->status, [
                PurchaseOrderStatus::Confirmed->value,
                PurchaseOrderStatus::PartiallyReceived->value,
                PurchaseOrderStatus::FullyReceived->value,
            ], true)) {
                throw ValidationException::withMessages([
                    'purchase_order_id' => __('dberp.errors.purchase_order_not_receivable'),
                ]);
            }

            if ((int) $locked->purchaseOrder->supplier_id !== (int) $locked->supplier_id) {
                throw ValidationException::withMessages([
                    'supplier_id' => __('dberp.errors.purchase_supplier_mismatch'),
                ]);
            }

            $items = $locked->items()->with(['product', 'productSku', 'purchaseOrderItem.productSku'])->orderBy('sort_order')->orderBy('id')->get();

            if ($items->isEmpty()) {
                throw ValidationException::withMessages([
                    'items' => __('dberp.errors.purchase_receipt_no_items'),
                ]);
            }

            foreach ($items as $item) {
                if ((int) $item->purchaseOrderItem->order_id !== (int) $locked->purchase_order_id) {
                    throw ValidationException::withMessages([
                        'purchase_order_item_id' => __('dberp.errors.purchase_receipt_item_order_mismatch'),
                    ]);
                }

                if ((int) $item->purchaseOrderItem->product_sku_id !== (int) $item->product_sku_id) {
                    throw ValidationException::withMessages([
                        'product_sku_id' => __('dberp.errors.purchase_item_sku_mismatch'),
                    ]);
                }

                if ((int) $item->purchaseOrderItem->product_id !== (int) $item->product_id) {
                    throw ValidationException::withMessages([
                        'product_id' => __('dberp.errors.purchase_item_product_mismatch'),
                    ]);
                }

                $poLine = $item->purchaseOrderItem;

                $remainingBase = bcsub(
                    (string) ($poLine->base_quantity ?? '0'),
                    (string) ($poLine->base_received_quantity ?? '0'),
                    4
                );

                if ((float) $item->arrival_quantity < (float) $item->received_quantity) {
                    throw ValidationException::withMessages([
                        'arrival_quantity' => __('dberp.errors.purchase_receipt_arrival_less_than_received'),
                    ]);
                }

                if ((float) $item->arrival_quantity <= 0) {
                    throw ValidationException::withMessages([
                        'arrival_quantity' => __('dberp.errors.purchase_receipt_arrival_invalid'),
                    ]);
                }

                if ((float) $item->received_quantity < 0) {
                    throw ValidationException::withMessages([
                        'received_quantity' => __('dberp.errors.purchase_receipt_quantity_invalid'),
                    ]);
                }

                if (bccomp((string) $item->base_received_quantity, $remainingBase, 4) === 1) {
                    $this->auditService->log(
                        'dberp.purchase_receipt.quantity_overflow_blocked',
                        $locked,
                        null,
                        [
                            'purchase_order_item_id' => $item->purchase_order_item_id,
                            'product_id' => $item->product_id,
                            'remaining_base' => $remainingBase,
                            'arrival_quantity' => (string) $item->arrival_quantity,
                            'received_quantity' => (string) $item->received_quantity,
                            'base_received_quantity' => (string) $item->base_received_quantity,
                        ],
                    );

                    throw ValidationException::withMessages([
                        'received_quantity' => __('dberp.errors.purchase_receipt_quantity_exceeds_remaining'),
                    ]);
                }

                $ledgerReplayedForReceiptLine = false;

                if (bccomp((string) $item->base_received_quantity, '0', 4) === 1) {
                    $sku = $item->productSku;
                    if ($sku === null) {
                        throw ValidationException::withMessages([
                            'product_sku_id' => __('dberp.errors.purchase_receipt_item_sku_required'),
                        ]);
                    }

                    ProductSkuOperationalGuard::assertEnabledForPurchase($sku);

                    $receiptLineKey = sprintf('purchase_receipt:%d:confirm:%d', (int) $locked->getKey(), (int) $item->getKey());
                    $ledgerQty = (string) $item->base_received_quantity;
                    $lineTrace = InventoryLedgerLineTrace::fromDocumentLine($item, 'received_quantity');

                    $post = $this->ledgerService->inbound(
                        productSku: $sku,
                        warehouse: $locked->warehouse,
                        quantity: $ledgerQty,
                        businessType: InventoryBusinessType::PurchaseReceipt,
                        businessNumber: (string) $locked->code,
                        sourceType: $locked->getMorphClass(),
                        sourceId: (int) $locked->getKey(),
                        remark: $item->remark ?? $locked->remark,
                        businessTime: $locked->business_time ?? now(),
                        idempotencyKey: $receiptLineKey,
                        lineTrace: $lineTrace,
                    );

                    $ledgerReplayedForReceiptLine = $post->wasIdempotentReplay;

                    if (! $post->wasIdempotentReplay) {
                        $result = $this->refinementService->applyInbound(
                            product: $sku->product,
                            warehouse: $locked->warehouse,
                            quantity: $ledgerQty,
                            businessType: InventoryBusinessType::PurchaseReceipt,
                            businessNumber: (string) $locked->code,
                            sourceType: $locked->getMorphClass(),
                            sourceId: (int) $locked->getKey(),
                            remark: $item->remark ?? $locked->remark,
                            businessTime: $locked->business_time ?? now(),
                            detail: [
                                'batch_no' => $item->batch_no,
                                'production_date' => $item->production_date?->format('Y-m-d'),
                                'expiry_date' => $item->expiry_date?->format('Y-m-d'),
                                'shelf_life_days' => (int) $item->shelf_life_days,
                                'serial_numbers' => $item->serial_numbers,
                            ],
                        );

                        $item->forceFill([
                            'source_batch_id' => $result['batch_id'],
                            'batch_no' => $result['batch_no'] ?? $item->batch_no,
                            'batch_allocations' => $result['batch_allocations'],
                            'serial_numbers' => $result['serial_numbers'],
                        ])->save();
                    }
                }

                if (! $ledgerReplayedForReceiptLine) {
                    $deltaReceivedPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                        (string) $item->base_received_quantity,
                        $poLine,
                    );
                    $deltaArrivalPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                        (string) $item->base_arrival_quantity,
                        $poLine,
                    );
                    $rejectBase = bcsub((string) $item->base_arrival_quantity, (string) $item->base_received_quantity, 4);
                    if (bccomp($rejectBase, '0', 4) === -1) {
                        $rejectBase = '0.0000';
                    }
                    $deltaRejectedPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                        $rejectBase,
                        $poLine,
                    );

                    $newReceived = bcadd((string) $poLine->received_quantity, $deltaReceivedPo, 4);
                    $newBaseReceived = bcadd((string) ($poLine->base_received_quantity ?? '0'), (string) $item->base_received_quantity, 4);
                    $newArrived = bcadd((string) $poLine->arrived_quantity, $deltaArrivalPo, 4);
                    $newRejected = bcadd((string) $poLine->rejected_quantity, $deltaRejectedPo, 4);

                    $poLine->forceFill([
                        'received_quantity' => $newReceived,
                        'base_received_quantity' => $newBaseReceived,
                        'arrived_quantity' => $newArrived,
                        'rejected_quantity' => $newRejected,
                        'last_arrival_date' => $locked->arrival_date ?? $locked->business_time?->toDateString(),
                    ])->save();

                    $this->purchaseExceptionService->syncFromReceiptItem($locked, $item, (float) $remainingBase);
                }
            }

            $this->refreshTotals($locked);

            $before = ['status' => $locked->status, 'effective_at' => $locked->effective_at];

            $this->auditService->logSemanticSave(
                'dberp.purchase_receipt.applied',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => InventoryDocumentStatus::Effective->value,
                        'effective_at' => now(),
                    ])->save();
                },
                $before,
                ['status' => $locked->status, 'effective_at' => $locked->effective_at],
            );

            $this->purchaseOrderService->refreshTotals($locked->purchaseOrder);
            $this->purchaseOrderExecutionService->syncOrder($locked->purchaseOrder);

            foreach ($items as $item) {
                $this->purchasePriceReferenceService->refreshForProduct((int) $item->product_id, (int) $locked->supplier_id);
            }

        });
    }

    public function void(DberpPurchaseReceiptOrder $order): void
    {
        try {
            DB::transaction(function () use ($order): void {
                $locked = DberpPurchaseReceiptOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('purchaseOrder', 'warehouse');

                if ($locked->status !== InventoryDocumentStatus::Effective->value) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.inventory_doc_not_effective'),
                    ]);
                }

                if (! $this->ruleService->allowsEffectiveInventoryVoid()) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.inventory_void_policy_blocked'),
                    ]);
                }

                $rollbackInventoryEffects = $this->ruleService->allowVoidRollback();

                if ($rollbackInventoryEffects) {
                    $items = $locked->items()->with(['product', 'productSku', 'purchaseOrderItem.productSku'])->orderBy('sort_order')->orderBy('id')->get();

                    $preflightVoidLines = [];

                    foreach ($items as $item) {
                        $ledgerQty = (string) $item->base_received_quantity;

                        if (bccomp($ledgerQty, '0', 4) !== 1) {
                            continue;
                        }

                        $sku = $item->productSku;
                        if ($sku === null) {
                            continue;
                        }

                        $preflightVoidLines[] = [
                            'sku' => $sku,
                            'warehouse' => $locked->warehouse,
                            'quantity' => $ledgerQty,
                        ];
                    }

                    if ($preflightVoidLines !== []) {
                        $this->ledgerService->assertNoOutboundShortagesPreflight(
                            $preflightVoidLines,
                            InventoryBusinessType::PurchaseReceiptVoid,
                            (string) $locked->code,
                        );
                    }

                    foreach ($items as $item) {
                        if ((float) $item->returned_quantity > 0) {
                            throw ValidationException::withMessages([
                                'status' => __('dberp.errors.purchase_receipt_has_returns'),
                            ]);
                        }

                        $poLine = $item->purchaseOrderItem;
                        $ledgerQty = (string) $item->base_received_quantity;
                        $lineTrace = InventoryLedgerLineTrace::fromDocumentLine($item, 'received_quantity');

                        $ledgerReplayed = false;

                        if (bccomp($ledgerQty, '0', 4) === 1) {
                            $sku = $item->productSku;
                            if ($sku === null) {
                                throw ValidationException::withMessages([
                                    'product_sku_id' => __('dberp.errors.purchase_receipt_item_sku_required'),
                                ]);
                            }

                            $voidLineKey = sprintf('purchase_receipt:%d:void:%d', (int) $locked->getKey(), (int) $item->getKey());

                            $post = $this->ledgerService->outbound(
                                productSku: $sku,
                                warehouse: $locked->warehouse,
                                quantity: $ledgerQty,
                                businessType: InventoryBusinessType::PurchaseReceiptVoid,
                                businessNumber: (string) $locked->code,
                                sourceType: $locked->getMorphClass(),
                                sourceId: (int) $locked->getKey(),
                                remark: $item->remark ?? $locked->remark,
                                businessTime: now(),
                                idempotencyKey: $voidLineKey,
                                lineTrace: $lineTrace,
                            );

                            $ledgerReplayed = $post->wasIdempotentReplay;

                            if (! $post->wasIdempotentReplay) {
                                $this->refinementService->rollbackInbound(
                                    product: $sku->product,
                                    warehouse: $locked->warehouse,
                                    quantity: $ledgerQty,
                                    businessType: InventoryBusinessType::PurchaseReceiptVoid,
                                    businessNumber: (string) $locked->code,
                                    sourceType: $locked->getMorphClass(),
                                    sourceId: (int) $locked->getKey(),
                                    remark: $item->remark ?? $locked->remark,
                                    businessTime: now(),
                                    detail: [
                                        'source_batch_id' => $item->source_batch_id,
                                        'batch_no' => $item->batch_no,
                                        'batch_allocations' => $item->batch_allocations,
                                        'serial_numbers' => $item->serial_numbers,
                                    ],
                                );
                            }
                        }

                        if (! $ledgerReplayed) {
                            $deltaReceivedPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                                (string) $item->base_received_quantity,
                                $poLine,
                            );
                            $deltaArrivalPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                                (string) $item->base_arrival_quantity,
                                $poLine,
                            );
                            $rejectBase = bcsub((string) $item->base_arrival_quantity, (string) $item->base_received_quantity, 4);
                            if (bccomp($rejectBase, '0', 4) === -1) {
                                $rejectBase = '0.0000';
                            }
                            $deltaRejectedPo = DberpProductUnitConversionService::baseQuantityToLineEnteredString(
                                $rejectBase,
                                $poLine,
                            );

                            $poLine->forceFill([
                                'received_quantity' => bcsub((string) $poLine->received_quantity, $deltaReceivedPo, 4),
                                'base_received_quantity' => bcsub((string) ($poLine->base_received_quantity ?? '0'), (string) $item->base_received_quantity, 4),
                                'arrived_quantity' => bcsub((string) $poLine->arrived_quantity, $deltaArrivalPo, 4),
                                'rejected_quantity' => bcsub((string) $poLine->rejected_quantity, $deltaRejectedPo, 4),
                            ])->save();
                        }
                    }

                    $this->purchaseOrderService->refreshTotals($locked->purchaseOrder);
                    $this->purchaseOrderExecutionService->syncOrder($locked->purchaseOrder);

                    foreach ($items as $item) {
                        $this->purchasePriceReferenceService->refreshForProduct((int) $item->product_id, (int) $locked->supplier_id);
                    }
                }

                $before = ['status' => $locked->status, 'voided_at' => $locked->voided_at];

                $this->auditService->logSemanticSave(
                    'dberp.purchase_receipt.voided',
                    $locked,
                    function () use ($locked): void {
                        $locked->forceFill([
                            'status' => InventoryDocumentStatus::Voided->value,
                            'voided_at' => now(),
                        ])->save();
                    },
                    $before,
                    ['status' => $locked->status, 'voided_at' => $locked->voided_at],
                );
            });
        } catch (InsufficientStockException $exception) {
            $this->auditInsufficientStock($exception);

            throw $exception->validationException();
        }
    }

    public function refreshTotals(DberpPurchaseReceiptOrder $order): void
    {
        $totals = $order->items()->selectRaw('COALESCE(SUM(arrival_quantity), 0) as total_arrival_quantity, COALESCE(SUM(received_quantity), 0) as total_received_quantity, COALESCE(SUM(line_amount), 0) as total_amount')->first();

        DberpAuditTrailGuard::withoutAuditTrail(function () use ($order, $totals): void {
            $order->forceFill([
                'total_arrival_quantity' => (string) ($totals?->total_arrival_quantity ?? '0.0000'),
                'total_received_quantity' => (string) ($totals?->total_received_quantity ?? '0.0000'),
                'total_amount' => (string) ($totals?->total_amount ?? '0.0000'),
            ])->save();
        });
    }

    private function auditInsufficientStock(InsufficientStockException $exception): void
    {
        $balanceQuery = DberpInventoryBalance::query()->where('warehouse_id', $exception->warehouseId);

        if ($exception->productSkuId !== null) {
            $balanceQuery->where('product_sku_id', $exception->productSkuId);
        } else {
            $balanceQuery->where('product_id', $exception->productId);
        }

        $balance = $balanceQuery->first();

        $this->auditService->log(
            'dberp.inventory.insufficient_stock_blocked',
            $balance,
            null,
            $exception->auditContext(),
        );
    }
}
