<?php

namespace App\Domain\Inventory\Services;

use App\Domain\Inventory\Enums\MovementType;
use App\Domain\Inventory\Exceptions\InsufficientStockException;
use App\Domain\Inventory\Models\InventoryLevel;
use App\Domain\Inventory\Models\Kit;
use App\Domain\Inventory\Models\KitComponent;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\Warehouse;
use App\Models\User;
use Illuminate\Support\Facades\DB;

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

    /**
     * Create a new kit with its components.
     */
    public function create(array $data, array $components, User $creator): Kit
    {
        $this->assertComponentsAreUsable($components);

        return DB::transaction(function () use ($data, $components, $creator) {
            $kit = Kit::create([
                'academy_id' => $data['academy_id'],
                'name_ar' => $data['name_ar'],
                'name' => $data['name'] ?? null,
                'sku' => $data['sku'],
                'description_ar' => $data['description_ar'] ?? null,
                'selling_price' => (int) $data['selling_price'],
                'is_active' => $data['is_active'] ?? true,
                'assembly_instructions' => $data['assembly_instructions'] ?? null,
                'photo_path' => $data['photo_path'] ?? null,
                'created_by' => $creator->id,
                'metadata' => $data['metadata'] ?? [],
            ]);

            foreach ($components as $sortOrder => $component) {
                KitComponent::create([
                    'kit_id' => $kit->id,
                    'product_id' => $component['product_id'],
                    'quantity' => (int) ($component['quantity'] ?? 1),
                    'sort_order' => $component['sort_order'] ?? $sortOrder,
                ]);
            }

            return $kit->load('components.product');
        });
    }

    /**
     * Update a kit and its components.
     */
    public function update(Kit $kit, array $data, array $components): Kit
    {
        $this->assertComponentsAreUsable($components);

        return DB::transaction(function () use ($kit, $data, $components) {
            $kit->update([
                'name_ar' => $data['name_ar'] ?? $kit->name_ar,
                'name' => array_key_exists('name', $data) ? $data['name'] : $kit->name,
                'sku' => $data['sku'] ?? $kit->sku,
                'description_ar' => array_key_exists('description_ar', $data) ? $data['description_ar'] : $kit->description_ar,
                'selling_price' => isset($data['selling_price']) ? (int) $data['selling_price'] : $kit->selling_price,
                'is_active' => $data['is_active'] ?? $kit->is_active,
                'assembly_instructions' => array_key_exists('assembly_instructions', $data) ? $data['assembly_instructions'] : $kit->assembly_instructions,
                'photo_path' => array_key_exists('photo_path', $data) ? $data['photo_path'] : $kit->photo_path,
                'metadata' => $data['metadata'] ?? $kit->metadata,
            ]);

            // Replace components
            $kit->components()->delete();

            foreach ($components as $sortOrder => $component) {
                KitComponent::create([
                    'kit_id' => $kit->id,
                    'product_id' => $component['product_id'],
                    'quantity' => (int) ($component['quantity'] ?? 1),
                    'sort_order' => $component['sort_order'] ?? $sortOrder,
                ]);
            }

            return $kit->fresh('components.product');
        });
    }

    /**
     * Every component must be a product this branch may actually sell.
     *
     * The ids arrive from the kit form and were written straight into
     * kit_components. A kit may only bundle products this branch stocks, which
     * the Product scope already enforces: another branch's product resolves to
     * nothing and is refused here, rather than becoming a component of a kit
     * this branch can sell but cannot assemble.
     *
     * @param  array<int, array{product_id?: int|string}>  $components
     */
    private function assertComponentsAreUsable(array $components): void
    {
        $ids = collect($components)
            ->pluck('product_id')
            ->filter()
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values();

        if ($ids->isEmpty()) {
            return;
        }

        $resolved = Product::whereIn('id', $ids)->pluck('id')->map(fn ($id) => (int) $id);

        if ($resolved->count() !== $ids->count()) {
            throw new \App\Domain\Shared\Exceptions\DomainException(
                'بعض المنتجات المحددة غير متاحة لهذا الفرع'
            );
        }
    }

    /**
     * Assemble a kit: deduct component stock (kit_assembly movements).
     * Checks ALL components have sufficient stock BEFORE deducting any.
     */
    public function assemble(Kit $kit, Warehouse $warehouse, int $quantity, User $actor): void
    {
        if ($quantity <= 0) {
            throw new \App\Domain\Shared\Exceptions\DomainException('الكمية يجب أن تكون أكبر من صفر');
        }

        $kit->load('components.product');

        DB::transaction(function () use ($kit, $warehouse, $quantity, $actor) {
            // First pass: verify all components have sufficient stock
            foreach ($kit->components as $component) {
                $required = $component->quantity * $quantity;
                // Read the same row createMovement() will lock a moment later:
                // keyed by the warehouse, not by the reader's branch. Reading
                // them differently is how a pre-check passes and the movement
                // then fails, or the reverse.
                $level = InventoryLevel::withoutBranchScope()
                    ->where('product_id', $component->product_id)
                    ->where('warehouse_id', $warehouse->id)
                    ->lockForUpdate()
                    ->first();

                $available = $level ? $level->quantity_on_hand : 0;

                if ($available < $required) {
                    $productName = $component->product->name_ar ?? $component->product->name;
                    throw new InsufficientStockException(
                        "المخزون غير كافٍ للمنتج '{$productName}': متاح {$available}، مطلوب {$required}"
                    );
                }
            }

            // Second pass: deduct stock for each component
            foreach ($kit->components as $component) {
                $required = $component->quantity * $quantity;
                $product = $component->product;

                $this->inventoryService->createMovement(
                    product: $product,
                    warehouse: $warehouse,
                    type: MovementType::KitAssembly,
                    quantity: $required,
                    actor: $actor,
                    reference: $kit,
                    reason: "تجميع طقم '{$kit->name_ar}' × {$quantity}",
                );
            }
        });
    }

    /**
     * Disassemble a kit: return component stock (kit_disassembly movements).
     */
    public function disassemble(Kit $kit, Warehouse $warehouse, int $quantity, User $actor): void
    {
        if ($quantity <= 0) {
            throw new \App\Domain\Shared\Exceptions\DomainException('الكمية يجب أن تكون أكبر من صفر');
        }

        $kit->load('components.product');

        DB::transaction(function () use ($kit, $warehouse, $quantity, $actor) {
            foreach ($kit->components as $component) {
                $returnQty = $component->quantity * $quantity;
                $product = $component->product;

                $this->inventoryService->createMovement(
                    product: $product,
                    warehouse: $warehouse,
                    type: MovementType::KitDisassembly,
                    quantity: $returnQty,
                    actor: $actor,
                    reference: $kit,
                    reason: "تفكيك طقم '{$kit->name_ar}' × {$quantity}",
                );
            }
        });
    }

    /**
     * Check how many complete kits can be assembled from current stock.
     * Returns the minimum available kits based on the most constrained component.
     */
    public function checkAvailability(Kit $kit, Warehouse $warehouse): int
    {
        $kit->load('components');

        $minKits = PHP_INT_MAX;

        foreach ($kit->components as $component) {
            if ($component->quantity <= 0) {
                continue;
            }

            // Warehouse-keyed, like every other level read here.
            $level = InventoryLevel::withoutBranchScope()
                ->where('product_id', $component->product_id)
                ->where('warehouse_id', $warehouse->id)
                ->first();

            $available = $level ? $level->quantity_available : 0;
            $possibleKits = intdiv($available, $component->quantity);

            $minKits = min($minKits, $possibleKits);
        }

        return $minKits === PHP_INT_MAX ? 0 : $minKits;
    }
}
