<?php

namespace App\Domain\Inventory\Services;

use App\Domain\Inventory\Enums\MovementType;
use App\Domain\Inventory\Events\InventoryMovementCreated;
use App\Domain\Inventory\Events\ProductLowStock;
use App\Domain\Inventory\Exceptions\InsufficientStockException;
use App\Domain\Inventory\Models\InventoryLevel;
use App\Domain\Inventory\Models\InventoryMovement;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\Warehouse;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class InventoryService
{
    /**
     * Create a movement record and adjust inventory level.
     * This is THE ONLY way to modify inventory_levels.quantity_on_hand.
     *
     * Steps:
     * 1. Lock the level row (lockForUpdate)
     * 2. Record quantity_before
     * 3. Adjust level
     * 4. Record quantity_after
     * 5. Create the movement record
     */
    public function createMovement(
        Product $product,
        Warehouse $warehouse,
        MovementType $type,
        int $quantity,
        User $actor,
        ?int $unitCost = null,
        ?Model $reference = null,
        ?string $reason = null,
        ?string $batchNumber = null,
        ?string $expiryDate = null,
    ): InventoryMovement {
        $expectedDirection = $type->direction();

        if ($quantity <= 0) {
            throw new \App\Domain\Shared\Exceptions\DomainException('الكمية يجب أن تكون أكبر من صفر');
        }

        return DB::transaction(function () use ($product, $warehouse, $type, $quantity, $actor, $unitCost, $reference, $reason, $batchNumber, $expiryDate, $expectedDirection) {
            // 1. Lock the level row
            $level = InventoryLevel::where('product_id', $product->id)
                ->where('warehouse_id', $warehouse->id)
                ->lockForUpdate()
                ->first();

            if (! $level) {
                // Create level if doesn't exist (first movement for this product/warehouse)
                $level = InventoryLevel::create([
                    'academy_id' => $product->academy_id,
                    'product_id' => $product->id,
                    'warehouse_id' => $warehouse->id,
                    'quantity_on_hand' => 0,
                    'quantity_reserved' => 0,
                    'quantity_available' => 0,
                ]);
                $level = InventoryLevel::where('id', $level->id)->lockForUpdate()->first();
            }

            // 2. Record quantity_before
            $quantityBefore = $level->quantity_on_hand;

            // 3. Adjust level
            if ($expectedDirection === 'in') {
                $level->quantity_on_hand += $quantity;
            } else {
                // Check stock for 'out' movements
                if ($level->quantity_on_hand < $quantity) {
                    throw new InsufficientStockException(
                        "المخزون غير كافٍ: متاح {$level->quantity_on_hand}، مطلوب {$quantity}"
                    );
                }
                $level->quantity_on_hand -= $quantity;
            }

            // Update quantity_available
            $level->quantity_available = $level->quantity_on_hand - $level->quantity_reserved;
            $level->last_movement_at = now();
            $level->save();

            // 4. Record quantity_after
            $quantityAfter = $level->quantity_on_hand;

            // 5. Create movement record
            $movement = InventoryMovement::create([
                'academy_id' => $product->academy_id,
                'product_id' => $product->id,
                'warehouse_id' => $warehouse->id,
                'movement_type' => $type->value,
                'direction' => $expectedDirection,
                'quantity' => $quantity,
                'quantity_before' => $quantityBefore,
                'quantity_after' => $quantityAfter,
                'unit_cost' => $unitCost,
                'total_cost' => $unitCost ? $unitCost * $quantity : null,
                'reference_type' => $reference ? $reference->getMorphClass() : null,
                'reference_id' => $reference?->getKey(),
                'batch_number' => $batchNumber,
                'expiry_date' => $expiryDate,
                'reason' => $reason,
                'created_by' => $actor->id,
            ]);

            // Dispatch movement event for downstream listeners (e.g. low stock check)
            InventoryMovementCreated::dispatch($movement, $level);

            return $movement;
        });
    }

    /**
     * Transfer stock between warehouses (creates out + in movements atomically).
     */
    public function transfer(
        Product $product,
        Warehouse $fromWarehouse,
        Warehouse $toWarehouse,
        int $quantity,
        User $actor,
        ?string $reason = null,
    ): array {
        return DB::transaction(function () use ($product, $fromWarehouse, $toWarehouse, $quantity, $actor, $reason) {
            $outMovement = $this->createMovement(
                $product,
                $fromWarehouse,
                MovementType::TransferOut,
                $quantity,
                $actor,
                reason: $reason,
            );

            $inMovement = $this->createMovement(
                $product,
                $toWarehouse,
                MovementType::TransferIn,
                $quantity,
                $actor,
                reason: $reason,
            );

            return ['out' => $outMovement, 'in' => $inMovement];
        });
    }

    /**
     * Reserve stock (increase quantity_reserved, decrease quantity_available).
     */
    public function reserve(Product $product, Warehouse $warehouse, int $quantity): void
    {
        DB::transaction(function () use ($product, $warehouse, $quantity) {
            $level = InventoryLevel::where('product_id', $product->id)
                ->where('warehouse_id', $warehouse->id)
                ->lockForUpdate()
                ->first();

            if (! $level || $level->quantity_available < $quantity) {
                throw new InsufficientStockException('المخزون المتاح غير كافٍ للحجز');
            }

            $level->quantity_reserved += $quantity;
            $level->quantity_available = $level->quantity_on_hand - $level->quantity_reserved;
            $level->save();
        });
    }

    /**
     * Release previously reserved stock.
     */
    public function releaseReservation(Product $product, Warehouse $warehouse, int $quantity): void
    {
        DB::transaction(function () use ($product, $warehouse, $quantity) {
            $level = InventoryLevel::where('product_id', $product->id)
                ->where('warehouse_id', $warehouse->id)
                ->lockForUpdate()
                ->first();

            if (! $level) {
                return;
            }

            $level->quantity_reserved = max(0, $level->quantity_reserved - $quantity);
            $level->quantity_available = $level->quantity_on_hand - $level->quantity_reserved;
            $level->save();
        });
    }

    /**
     * Get total stock on hand for a product (optionally in a specific warehouse).
     */
    public function getStockLevel(Product $product, ?Warehouse $warehouse = null): int
    {
        $query = InventoryLevel::where('product_id', $product->id);

        if ($warehouse) {
            $query->where('warehouse_id', $warehouse->id);
        }

        return $query->sum('quantity_on_hand');
    }

    /**
     * Get available stock (on_hand minus reserved) for a product.
     */
    public function getAvailableStock(Product $product, ?Warehouse $warehouse = null): int
    {
        $query = InventoryLevel::where('product_id', $product->id);

        if ($warehouse) {
            $query->where('warehouse_id', $warehouse->id);
        }

        return $query->sum('quantity_available');
    }

    /**
     * Get products where quantity_on_hand is at or below min_stock_level.
     */
    public function getLowStockProducts(int $academyId): Collection
    {
        return Product::where('academy_id', $academyId)
            ->where('track_inventory', true)
            ->where('is_active', true)
            ->whereNotNull('min_stock_level')
            ->whereHas('inventoryLevels', function ($q) {
                $q->whereRaw('quantity_on_hand <= (SELECT min_stock_level FROM products WHERE products.id = inventory_levels.product_id)');
            })
            ->with(['inventoryLevels'])
            ->get();
    }
}
