<?php

namespace App\Modules\Procurement\Services;

use App\Modules\Procurement\Exceptions\ReceivingOperationalException;
use App\Modules\Procurement\Models\GoodsReceipt;
use App\Modules\Procurement\Models\GoodsReceiptItem;
use App\Modules\Procurement\Models\PurchaseOrder;
use App\Modules\Procurement\Models\PurchaseOrderItem;
use App\Modules\Procurement\Support\GoodsReceiptLifecycleStatus;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;

final class ReceivingValidationService
{
    public function __construct(
        private readonly PurchaseOrderLifecycleService $purchaseOrderLifecycle,
        private readonly ProcurementReceivingObservability $obs,
    ) {}

    /**
     * Full pipeline before marking validated or posting stock.
     *
     * @param  \Illuminate\Support\Collection<int, GoodsReceiptItem>  $lines
     */
    public function assertReceiptLinesReceivable(
        GoodsReceipt $receipt,
        PurchaseOrder $purchaseOrder,
        $lines,
    ): void {
        $ids = $lines->pluck('purchase_order_item_id')->unique()->sort()->values();
        foreach ($ids as $pid) {
            PurchaseOrderItem::withoutGlobalScopes()
                ->where('tenant_id', $receipt->tenant_id)
                ->whereKey($pid)
                ->lockForUpdate()
                ->first();
        }

        $this->purchaseOrderLifecycle->assertPurchaseOrderAllowsReceiving($purchaseOrder);

        if ($receipt->warehouse_id === null) {
            throw new ReceivingOperationalException('Receipt warehouse is required.', 'receipt_warehouse_missing');
        }

        if ($lines->isEmpty()) {
            throw new ReceivingOperationalException('Receipt has no lines.', 'receipt_no_lines');
        }

        foreach ($lines as $line) {
            $this->assertLine($receipt, $purchaseOrder, $line);
        }
    }

    private function assertLine(GoodsReceipt $receipt, PurchaseOrder $purchaseOrder, GoodsReceiptItem $line): void
    {
        if ($line->quarantined_at !== null) {
            $this->obs->quarantineExcluded((int) $line->id, (int) $receipt->id);
            throw new ReceivingOperationalException('Quarantined line cannot receive.', 'line_quarantined');
        }

        $poi = PurchaseOrderItem::withoutGlobalScopes()->find((int) $line->purchase_order_item_id);
        if ($poi === null || $poi->trashed()) {
            throw new ReceivingOperationalException('Invalid or deleted PO line.', 'po_line_invalid');
        }
        if ((int) $poi->purchase_order_id !== (int) $purchaseOrder->id) {
            throw new ReceivingOperationalException('Cross-PO receipt line.', 'cross_po');
        }
        if ((int) $poi->tenant_id !== (int) $receipt->tenant_id) {
            throw new ReceivingOperationalException('Tenant mismatch on PO line.', 'tenant_mismatch');
        }

        $qty = trim((string) $line->quantity_received);
        if (! DecimalQuantity::gtZero($qty)) {
            throw new ReceivingOperationalException('Line quantity must be positive.', 'line_qty_invalid');
        }

        $ordered = (string) $poi->quantity_ordered;
        $posted = (string) $poi->quantity_received_posted;
        $remaining = DecimalQuantity::add($ordered, DecimalQuantity::negate($posted));
        if (! DecimalQuantity::gte($remaining, $qty)) {
            $this->obs->overReceiveAttempt([
                'tenant_id' => (int) $receipt->tenant_id,
                'goods_receipt_id' => $receipt->id,
                'purchase_order_id' => (int) $purchaseOrder->id,
                'purchase_order_item_id' => $poi->id,
                'remaining' => $remaining,
                'attempted' => $qty,
            ]);
            throw new ReceivingOperationalException('Over-receive not allowed.', 'over_receive');
        }
    }

    public function assertReceiptStatus(GoodsReceipt $receipt, GoodsReceiptLifecycleStatus $expected): void
    {
        $actual = GoodsReceiptLifecycleStatus::tryFrom((string) $receipt->status);
        if ($actual !== $expected) {
            throw new ReceivingOperationalException('Invalid receipt status for operation.', 'receipt_status_invalid');
        }
    }
}
