<?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.
            //
            // Read through the warehouse, not through the branch scope. A
            // level row is one warehouse's count of one product; if the scope
            // hid an existing row this method would insert a second one for the
            // same product/warehouse pair — a duplicate count, or a unique-key
            // failure that stops the sale. The caller already holds the
            // Warehouse, which is where the branch check belongs.
            $level = InventoryLevel::withoutBranchScope()
                ->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,
                    // The stock sits in the warehouse, so it belongs to the
                    // warehouse's branch. Null (an academy-wide warehouse) falls
                    // through to the creating hook.
                    'branch_id' => $warehouse->branch_id,
                    'product_id' => $product->id,
                    'warehouse_id' => $warehouse->id,
                    'quantity_on_hand' => 0,
                    'quantity_reserved' => 0,
                    'quantity_available' => 0,
                ]);
                $level = InventoryLevel::withoutBranchScope()->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,
                // Same reading as the level: the movement happened in the
                // warehouse's branch, whoever was looking at the time.
                'branch_id' => $warehouse->branch_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) {
            // Keyed by the warehouse the caller handed in — see createMovement
            // for why the level row is not read through the branch scope.
            $level = InventoryLevel::withoutBranchScope()
                ->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) {
            // Must find the same row reserve() locked, or the reservation is
            // never released and the stock stays unavailable forever.
            $level = InventoryLevel::withoutBranchScope()
                ->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.
     *
     * With no warehouse this is "stock in the branch I am working in", which is
     * what the branch scope gives and what a till or a product screen should
     * show. Naming a warehouse asks a different question — how much is in THAT
     * warehouse — and the answer must not change with who is looking, or a
     * shared, academy-wide warehouse would read as empty from every branch but
     * one.
     */
    public function getStockLevel(Product $product, ?Warehouse $warehouse = null): int
    {
        $query = $warehouse
            ? InventoryLevel::withoutBranchScope()->where('warehouse_id', $warehouse->id)
            : InventoryLevel::query();

        return $query->where('product_id', $product->id)->sum('quantity_on_hand');
    }

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

        return $query->where('product_id', $product->id)->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();
    }
}
