<?php

namespace App\Domains\Cart\Models;

use App\Domains\Auth\Models\User;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

#[Fillable([
    'tenant_codigo',
    'user_id',
    'guest_token',
    'status',
])]
class Cart extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $table = 'carritos';

    protected function casts(): array
    {
        return [
            'user_id' => 'integer',
        ];
    }

    /**
     * @return BelongsTo<Tenant, $this>
     */
    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
    }

    /**
     * @return BelongsTo<User, $this>
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * @return HasMany<CartItem, $this>
     */
    public function items(): HasMany
    {
        return $this->hasMany(CartItem::class, 'cart_id');
    }

    public function getTotalAmount(): float
    {
        $items = $this->relationLoaded('items')
            ? $this->getRelation('items')
            : $this->items()->with(['catalogItem', 'variant'])->get();

        return (float) $items->reduce(
            fn (float $carry, CartItem $item): float => $carry
                + (($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad),
            0.0,
        );
    }

    public function addItem(int $catalogItemId, ?int $variantId, int $quantity): CartItem
    {
        if ($quantity <= 0) {
            throw ValidationException::withMessages([
                'cantidad' => __('api.cart.positive_quantity'),
            ]);
        }

        return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem {
            self::query()->whereKey($this->getKey())->lockForUpdate()->firstOrFail();
            $selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
            $inventoryService = app(CatalogInventoryService::class);
            $availableQuantity = $inventoryService->availableQuantity($selectedItem);

            if ($availableQuantity !== null && $availableQuantity < $quantity) {
                throw ValidationException::withMessages([
                    'cantidad' => __('api.cart.insufficient_stock', ['max' => $availableQuantity]),
                ]);
            }

            /** @var CartItem|null $item */
            $item = $this->items()
                ->where('catalog_item_id', $catalogItemId)
                ->where('variant_id', $variantId)
                ->lockForUpdate()
                ->first();

            if ($item === null) {
                $item = $this->items()->create([
                    'catalog_item_id' => $catalogItemId,
                    'variant_id' => $variantId,
                    'cantidad' => $quantity,
                ]);
            } else {
                $item->cantidad += $quantity;
                $item->save();
            }

            $inventoryService->reserve($selectedItem, $quantity);

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

    public function updateItem(int $cartItemId, int $quantity): CartItem
    {
        if ($quantity <= 0) {
            throw ValidationException::withMessages([
                'cantidad' => __('api.cart.positive_quantity'),
            ]);
        }

        return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
            /** @var CartItem $item */
            $item = $this->items()
                ->where('id', $cartItemId)
                ->lockForUpdate()
                ->firstOrFail();

            $selectedItem = $this->resolveScopedItem(
                $item->catalog_item_id,
                $item->variant_id,
                true,
            );
            $inventoryService = app(CatalogInventoryService::class);
            $delta = $quantity - $item->cantidad;
            $availableQuantity = $inventoryService->availableQuantity($selectedItem);

            if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
                $maxAvailable = $availableQuantity + $item->cantidad;
                throw ValidationException::withMessages([
                    'cantidad' => __('api.cart.max_quantity', ['max' => $maxAvailable]),
                ]);
            }

            $item->cantidad = $quantity;
            $item->save();

            if ($delta > 0) {
                $inventoryService->reserve($selectedItem, $delta);
            }

            if ($delta < 0) {
                $inventoryService->release($selectedItem, abs($delta));
            }

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

    public function removeItem(int $cartItemId): void
    {
        DB::transaction(function () use ($cartItemId): void {
            /** @var CartItem $item */
            $item = $this->items()
                ->where('id', $cartItemId)
                ->lockForUpdate()
                ->firstOrFail();

            $selectedItem = $this->resolveScopedItem(
                $item->catalog_item_id,
                $item->variant_id,
                true,
            );
            app(CatalogInventoryService::class)->release(
                $selectedItem,
                $item->cantidad,
            );
            $item->delete();
        });
    }

    protected function resolveScopedItem(
        int $catalogItemId,
        ?int $variantId,
        bool $lockForUpdate = false,
    ): CatalogItem|Variant {
        $catalogItemQuery = CatalogItem::query()
            ->whereKey($catalogItemId)
            ->where('tenant_code', $this->tenant_codigo);

        if ($lockForUpdate) {
            $catalogItemQuery->lockForUpdate();
        }

        $catalogItem = $catalogItemQuery->first();

        if ($catalogItem === null) {
            throw new NotFoundHttpException('Catalog item not found for tenant.');
        }

        if ($catalogItem->isBundle()) {
            if ($variantId !== null) {
                throw ValidationException::withMessages([
                    'variant_id' => __('api.cart.bundle_variant_forbidden'),
                ]);
            }

            if (! $catalogItem->bundleComponents()->exists()) {
                throw ValidationException::withMessages([
                    'catalog_item_id' => __('api.cart.empty_bundle'),
                ]);
            }

            return $catalogItem;
        }

        if ($variantId === null) {
            if ($catalogItem->inventory_id === null) {
                throw ValidationException::withMessages([
                    'variant_id' => __('api.cart.variant_required'),
                ]);
            }

            $inventory = $this->resolveInventory($catalogItem->inventory_id, $lockForUpdate);
            $catalogItem->setRelation('inventory', $inventory);

            return $catalogItem;
        }

        $variantQuery = Variant::query()
            ->whereKey($variantId)
            ->where('catalog_item_id', $catalogItem->id);

        if ($lockForUpdate) {
            $variantQuery->lockForUpdate();
        }

        $variant = $variantQuery->first();

        if ($variant === null) {
            throw new NotFoundHttpException('Variant not found for catalog item.');
        }

        $inventory = $this->resolveInventory($variant->inventory_id, $lockForUpdate);
        $variant->setRelation('catalogItem', $catalogItem);
        $variant->setRelation('inventory', $inventory);

        return $variant;
    }

    protected function resolveInventory(int $inventoryId, bool $lockForUpdate): Inventory
    {
        $query = Inventory::query()->whereKey($inventoryId);

        if ($lockForUpdate) {
            $query->lockForUpdate();
        }

        return $query->firstOrFail();
    }
}
