<?php

namespace App\Domain\Inventory\Services;

use App\Domain\Inventory\Enums\MovementType;
use App\Domain\Inventory\Enums\PurchaseOrderStatus;
use App\Domain\Inventory\Events\PurchaseOrderReceived;
use App\Domain\Inventory\Models\PurchaseOrder;
use App\Domain\Inventory\Models\PurchaseOrderItem;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class PurchaseOrderService
{
    private const VALID_TRANSITIONS = [
        'draft' => ['submitted', 'cancelled'],
        'submitted' => ['confirmed', 'cancelled'],
        'confirmed' => ['partially_received', 'received', 'cancelled'],
        'partially_received' => ['received'],
        'received' => [],
        'cancelled' => [],
    ];

    public function __construct(
        private readonly InventoryService $inventoryService,
    ) {}

    /**
     * Create a new purchase order with items.
     */
    public function create(array $data, array $items, User $creator): PurchaseOrder
    {
        // The order names both a branch and a warehouse and nothing checked
        // that the two belong together: goods ordered under one branch could be
        // received into another branch's warehouse, and the stock would land
        // there for real.
        $this->resolveWarehouse($data['warehouse_id'] ?? null, $data['branch_id'] ?? null);

        return DB::transaction(function () use ($data, $items, $creator) {
            // Calculate totals from items
            $subtotal = 0;
            foreach ($items as $item) {
                $subtotal += (int) $item['unit_cost'] * (int) $item['quantity_ordered'];
            }

            $taxAmount = (int) ($data['tax_amount'] ?? 0);
            $shippingCost = (int) ($data['shipping_cost'] ?? 0);
            $totalAmount = $subtotal + $taxAmount + $shippingCost;

            $po = PurchaseOrder::create([
                'academy_id' => $data['academy_id'],
                'branch_id' => $data['branch_id'],
                'warehouse_id' => $data['warehouse_id'],
                'order_number' => $data['order_number'],
                'supplier_name' => $data['supplier_name'],
                'supplier_contact' => $data['supplier_contact'] ?? null,
                'status' => PurchaseOrderStatus::Draft->value,
                'order_date' => $data['order_date'],
                'expected_delivery_date' => $data['expected_delivery_date'] ?? null,
                'subtotal' => $subtotal,
                'tax_amount' => $taxAmount,
                'shipping_cost' => $shippingCost,
                'total_amount' => $totalAmount,
                'notes' => $data['notes'] ?? null,
                'created_by' => $creator->id,
                'metadata' => $data['metadata'] ?? [],
            ]);

            foreach ($items as $item) {
                $lineTotal = (int) $item['unit_cost'] * (int) $item['quantity_ordered'];

                PurchaseOrderItem::create([
                    'purchase_order_id' => $po->id,
                    'product_id' => $item['product_id'],
                    'quantity_ordered' => (int) $item['quantity_ordered'],
                    'quantity_received' => 0,
                    'unit_cost' => (int) $item['unit_cost'],
                    'line_total' => $lineTotal,
                    'notes' => $item['notes'] ?? null,
                ]);
            }

            return $po->load('items');
        });
    }

    /**
     * The warehouse an order's goods may be received into.
     *
     * An order may only be received into a store this branch keeps. Read
     * without the branch scope deliberately: this has to be an explicit refusal
     * with a message a receptionist can act on, not a ModelNotFound thrown out
     * of a scoped findOrFail.
     */
    private function resolveWarehouse(?int $warehouseId, ?int $branchId): Warehouse
    {
        $warehouse = $warehouseId
            ? Warehouse::withoutBranchScope()->find($warehouseId)
            : null;

        if (! $warehouse) {
            throw new DomainException('المخزن غير موجود');
        }

        if ($branchId && $warehouse->branch_id && (int) $warehouse->branch_id !== (int) $branchId) {
            throw new DomainException('المخزن المحدد لا يخص هذا الفرع');
        }

        return $warehouse;
    }

    /**
     * Submit a draft purchase order for approval.
     */
    public function submit(PurchaseOrder $po, User $user): PurchaseOrder
    {
        $this->validateTransition($po, 'submitted');

        return DB::transaction(function () use ($po, $user) {
            $po->update([
                'status' => PurchaseOrderStatus::Submitted->value,
                'submitted_by' => $user->id,
            ]);

            return $po->fresh();
        });
    }

    /**
     * Confirm a submitted purchase order.
     */
    public function confirm(PurchaseOrder $po, User $user): PurchaseOrder
    {
        $this->validateTransition($po, 'confirmed');

        return DB::transaction(function () use ($po, $user) {
            $po->update([
                'status' => PurchaseOrderStatus::Confirmed->value,
                'confirmed_by' => $user->id,
            ]);

            return $po->fresh();
        });
    }

    /**
     * Receive items against a confirmed purchase order.
     * Creates purchase_received inventory movements for each received item.
     *
     * @param array $receivedItems Array of ['item_id' => X, 'quantity_received' => Y]
     */
    public function receive(PurchaseOrder $po, array $receivedItems, User $actor): PurchaseOrder
    {
        $currentStatus = $po->status instanceof PurchaseOrderStatus
            ? $po->status->value
            : $po->status;

        if (! in_array($currentStatus, ['confirmed', 'partially_received'])) {
            throw new InvalidStatusTransitionException(
                "لا يمكن استلام بضاعة لأمر شراء بحالة '{$currentStatus}'"
            );
        }

        return DB::transaction(function () use ($po, $receivedItems, $actor) {
            // Re-assert the pair at the moment stock actually moves, not only
            // when the order was typed.
            $warehouse = $this->resolveWarehouse($po->warehouse_id, $po->branch_id);

            foreach ($receivedItems as $received) {
                $item = PurchaseOrderItem::where('purchase_order_id', $po->id)
                    ->where('id', $received['item_id'])
                    ->lockForUpdate()
                    ->firstOrFail();

                $qtyToReceive = (int) $received['quantity_received'];
                $maxReceivable = $item->quantity_ordered - $item->quantity_received;

                if ($qtyToReceive <= 0) {
                    continue;
                }

                if ($qtyToReceive > $maxReceivable) {
                    throw new DomainException(
                        "الكمية المستلمة ({$qtyToReceive}) تتجاوز المتبقي ({$maxReceivable}) للمنتج"
                    );
                }

                // Update item received qty
                $item->update([
                    'quantity_received' => $item->quantity_received + $qtyToReceive,
                ]);

                // Create inventory movement
                $product = Product::findOrFail($item->product_id);
                $this->inventoryService->createMovement(
                    product: $product,
                    warehouse: $warehouse,
                    type: MovementType::PurchaseReceived,
                    quantity: $qtyToReceive,
                    actor: $actor,
                    unitCost: $item->unit_cost,
                    reference: $po,
                    reason: "استلام أمر شراء #{$po->order_number}",
                );
            }

            // Determine new PO status
            $po->refresh();
            $po->load('items');

            $allReceived = $po->items->every(fn ($item) => $item->quantity_received >= $item->quantity_ordered);
            $anyReceived = $po->items->contains(fn ($item) => $item->quantity_received > 0);

            if ($allReceived) {
                $po->update([
                    'status' => PurchaseOrderStatus::Received->value,
                    'received_date' => now(),
                ]);
                PurchaseOrderReceived::dispatch($po->fresh(), $actor);
            } elseif ($anyReceived) {
                $po->update([
                    'status' => PurchaseOrderStatus::PartiallyReceived->value,
                ]);
            }

            return $po->fresh('items');
        });
    }

    /**
     * Cancel a purchase order. Only allowed if no items have been received.
     */
    public function cancel(PurchaseOrder $po, User $user, ?string $reason = null): PurchaseOrder
    {
        $currentStatus = $po->status instanceof PurchaseOrderStatus
            ? $po->status->value
            : $po->status;

        $this->validateTransition($po, 'cancelled');

        // Cannot cancel if items have been received
        if (in_array($currentStatus, ['confirmed', 'partially_received'])) {
            $hasReceived = $po->items()->where('quantity_received', '>', 0)->exists();
            if ($hasReceived) {
                throw new DomainException('لا يمكن إلغاء أمر شراء تم استلام بضاعة منه');
            }
        }

        return DB::transaction(function () use ($po, $reason) {
            $po->update([
                'status' => PurchaseOrderStatus::Cancelled->value,
                'notes' => $reason ? ($po->notes ? $po->notes . "\n" . $reason : $reason) : $po->notes,
            ]);

            return $po->fresh();
        });
    }

    /**
     * Validate a status transition is allowed.
     */
    private function validateTransition(PurchaseOrder $po, string $newStatus): void
    {
        $currentStatus = $po->status instanceof PurchaseOrderStatus
            ? $po->status->value
            : $po->status;

        $allowed = self::VALID_TRANSITIONS[$currentStatus] ?? [];

        if (! in_array($newStatus, $allowed)) {
            throw new InvalidStatusTransitionException(
                "لا يمكن الانتقال من '{$currentStatus}' إلى '{$newStatus}'"
            );
        }
    }
}
