<?php

namespace App\Modules\Procurement\Services;

use App\Modules\Inventory\Stock\DTOs\ApplyMovementInput;
use App\Modules\Inventory\Stock\Services\InventoryMovementApplyService;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use App\Modules\Inventory\Stock\Support\InventoryStockMovementType;
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\Repositories\GoodsReceiptPostIdempotencyRepository;
use App\Modules\Procurement\Support\GoodsReceiptLifecycleStatus;
use Illuminate\Support\Str;

/**
 * Lock ordering: FOR UPDATE goods_receipt → purchase_orders → purchase_order_items (via validation locks)
 * → stock apply (GET_LOCK + projection inside InventoryMovementApplyService).
 */
final class GoodsReceiptPostingExecutor
{
    public function __construct(
        private readonly GoodsReceiptPostIdempotencyRepository $postIdempotency,
        private readonly ReceivingValidationService $validation,
        private readonly PurchaseOrderReceivingProgressService $progress,
        private readonly InventoryMovementApplyService $stockApply,
        private readonly ProcurementReceivingObservability $obs,
    ) {}

    /**
     * @return array<string, mixed>
     */
    public function executePost(
        int $tenantId,
        int $goodsReceiptId,
        string $idempotencyKey,
        string $idempotencySource,
        ?int $actorUserId,
        ?string $correlationId,
    ): array {
        $idem = $this->postIdempotency->findLocked($tenantId, $idempotencyKey, $idempotencySource);
        if ($idem !== null) {
            $this->obs->duplicatePostingAttempt([
                'tenant_id' => $tenantId,
                'goods_receipt_id' => $goodsReceiptId,
            ]);

            return $idem->result_payload;
        }

        $receipt = GoodsReceipt::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->whereKey($goodsReceiptId)
            ->lockForUpdate()
            ->first();
        if ($receipt === null) {
            throw new ReceivingOperationalException('Goods receipt not found.', 'receipt_not_found');
        }

        $this->validation->assertReceiptStatus($receipt, GoodsReceiptLifecycleStatus::Validated);

        $po = PurchaseOrder::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->whereKey($receipt->purchase_order_id)
            ->lockForUpdate()
            ->first();
        if ($po === null) {
            throw new ReceivingOperationalException('Purchase order not found.', 'po_not_found');
        }

        $lines = GoodsReceiptItem::withoutGlobalScopes()
            ->where('tenant_id', $tenantId)
            ->where('goods_receipt_id', $goodsReceiptId)
            ->orderBy('id')
            ->get();

        $this->validation->assertReceiptLinesReceivable($receipt, $po, $lines);

        $runCorrelation = $correlationId ?? (string) Str::uuid();
        $movementIds = [];
        foreach ($lines as $line) {
            $poi = PurchaseOrderItem::withoutGlobalScopes()
                ->whereKey($line->purchase_order_item_id)
                ->lockForUpdate()
                ->first();
            if ($poi === null) {
                throw new ReceivingOperationalException('PO line missing.', 'po_line_missing');
            }

            $qty = trim((string) $line->quantity_received);
            $stockKey = 'gr:'.$goodsReceiptId.':line:'.$line->id;

            $r = $this->stockApply->apply(new ApplyMovementInput(
                tenantId: $tenantId,
                warehouseId: (int) $receipt->warehouse_id,
                itemId: (int) $poi->inventory_item_id,
                type: InventoryStockMovementType::StockIn,
                quantityInput: $qty,
                actorUserId: $actorUserId,
                correlationId: $runCorrelation,
                idempotencyKey: $stockKey,
                idempotencySource: 'procurement_receive',
                unitId: null,
                occurredAt: null,
                referenceType: 'goods_receipt_item',
                referenceId: (int) $line->id,
                metadata: [
                    'goods_receipt_id' => (int) $receipt->id,
                    'purchase_order_id' => (int) $po->id,
                    'purchase_order_item_id' => (int) $poi->id,
                ],
            ));

            $movementIds[] = $r->movementId;

            $newPosted = DecimalQuantity::add((string) $poi->quantity_received_posted, $qty);
            $poi->forceFill(['quantity_received_posted' => $newPosted])->save();
        }

        $receipt->forceFill([
            'status' => GoodsReceiptLifecycleStatus::Posted->value,
            'posted_at' => now(),
            'correlation_id' => $runCorrelation,
            'actor_user_id' => $actorUserId,
        ])->save();

        $this->progress->refreshPurchaseOrderStatus($po);

        $payload = [
            'goods_receipt_id' => $goodsReceiptId,
            'movement_ids' => $movementIds,
            'correlation_id' => $runCorrelation,
        ];
        $this->postIdempotency->store($tenantId, $idempotencyKey, $idempotencySource, $goodsReceiptId, $payload);

        return $payload;
    }
}
