<?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\Catalog\Services\StockReservationService;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\UserPurchaseLimitService;
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',
    'origin',
    'current_purchase_id',
])]
class Cart extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $table = 'carritos';

    public const ORIGIN_USER = 'user';

    public const ORIGIN_DIRECT_CHECKOUT = 'direct_checkout';

    protected function casts(): array
    {
        return [
            'user_id' => 'integer',
            'current_purchase_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');
    }

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

    /** @return BelongsTo<Purchase, $this> */
    public function currentPurchase(): BelongsTo
    {
        return $this->belongsTo(Purchase::class, 'current_purchase_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 {
            $this->invalidateCurrentCheckout();
            $selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
            $cartQuantity = (int) $this->items()
                ->where('catalog_item_id', $catalogItemId)
                ->sum('cantidad');
            $inventoryService = app(CatalogInventoryService::class);
            $availableQuantity = $inventoryService->availableQuantity($selectedItem);
            $this->assertUserPurchaseLimit(
                $selectedItem,
                $cartQuantity + $quantity,
                heldQuantity: $cartQuantity,
                maximumAddableCeiling: $availableQuantity,
            );

            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 {
                app(StockReservationService::class)->ensure($item, $selectedItem);
                $item->cantidad += $quantity;
                $item->save();
            }

            app(StockReservationService::class)->reserve($item, $selectedItem, $quantity);

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

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

        return DB::transaction(function () use (
            $cartItemId,
            $quantity,
            $variantId,
            $updateVariant,
            $excludedPurchaseId,
        ): CartItem {
            $this->invalidateCurrentCheckout();

            /** @var CartItem $item */
            $item = $this->items()
                ->where('id', $cartItemId)
                ->lockForUpdate()
                ->firstOrFail();

            $currentSelection = $this->resolveScopedItem(
                $item->catalog_item_id,
                $item->variant_id,
                true,
            );
            $inventoryService = app(CatalogInventoryService::class);

            if ($updateVariant && $variantId !== $item->variant_id) {
                $nextSelection = $this->resolveScopedItem(
                    $item->catalog_item_id,
                    $variantId,
                    true,
                );
                $otherVariantsQuantity = (int) $this->items()
                    ->where('catalog_item_id', $item->catalog_item_id)
                    ->whereKeyNot($item->getKey())
                    ->sum('cantidad');
                $nextAvailableQuantity = $inventoryService->availableQuantity($nextSelection);
                $this->assertUserPurchaseLimit(
                    $nextSelection,
                    $otherVariantsQuantity + $quantity,
                    $excludedPurchaseId,
                    $otherVariantsQuantity + $item->cantidad,
                    $nextAvailableQuantity,
                );

                app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
                $availableQuantity = $inventoryService->availableQuantity($nextSelection);

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

                $targetItem = $this->items()
                    ->where('catalog_item_id', $item->catalog_item_id)
                    ->where('variant_id', $variantId)
                    ->whereKeyNot($item->getKey())
                    ->lockForUpdate()
                    ->first();

                if ($targetItem !== null) {
                    app(StockReservationService::class)->ensure($targetItem, $nextSelection);
                    $targetItem->cantidad += $quantity;
                    $targetItem->save();
                    app(StockReservationService::class)->reserve($targetItem, $nextSelection, $quantity);
                    $item->delete();

                    return $targetItem->fresh();
                }

                $item->variant_id = $variantId;
                $item->cantidad = $quantity;
                $item->save();
                app(StockReservationService::class)->reserve($item, $nextSelection, $quantity);

                return $item->fresh();
            }

            $delta = $quantity - $item->cantidad;
            $availableQuantity = $inventoryService->availableQuantity($currentSelection);

            if ($delta > 0) {
                $otherVariantsQuantity = (int) $this->items()
                    ->where('catalog_item_id', $item->catalog_item_id)
                    ->whereKeyNot($item->getKey())
                    ->sum('cantidad');
                $this->assertUserPurchaseLimit(
                    $currentSelection,
                    $otherVariantsQuantity + $quantity,
                    $excludedPurchaseId,
                    $otherVariantsQuantity + $item->cantidad,
                    $availableQuantity,
                );
            }

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

            if ($delta > 0) {
                app(StockReservationService::class)->reserve($item, $currentSelection, $delta);
            }

            if ($delta < 0) {
                app(StockReservationService::class)->release($item, $currentSelection, abs($delta));
            }

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

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

    public function removeItem(int $cartItemId): void
    {
        DB::transaction(function () use ($cartItemId): void {
            $this->invalidateCurrentCheckout();

            /** @var CartItem $item */
            $item = $this->items()
                ->where('id', $cartItemId)
                ->lockForUpdate()
                ->firstOrFail();

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

    private function invalidateCurrentCheckout(): void
    {
        $candidatePurchaseId = self::query()
            ->whereKey($this->getKey())
            ->value('current_purchase_id');
        $currentPurchase = $candidatePurchaseId === null
            ? null
            : Purchase::query()->lockForUpdate()->find($candidatePurchaseId);

        /** @var self $cart */
        $cart = self::query()->lockForUpdate()->findOrFail($this->getKey());

        if ($cart->current_purchase_id !== $candidatePurchaseId) {
            if ($cart->current_purchase_id !== null) {
                throw ValidationException::withMessages([
                    'cart' => __('api.purchase.checkout_in_progress'),
                ]);
            }

            $this->current_purchase_id = null;

            return;
        }

        if ($currentPurchase === null) {
            return;
        }

        if ($currentPurchase->status === Purchase::STATUS_PAID) {
            throw ValidationException::withMessages([
                'cart' => __('api.cart.editing_disabled'),
            ]);
        }

        if (in_array($currentPurchase->status, [
            Purchase::STATUS_CREATED,
            Purchase::STATUS_PENDING_PAYMENT,
        ], true)) {
            $currentPurchase->update([
                'status' => Purchase::STATUS_SUPERSEDED,
                'expires_at' => null,
            ]);
        }

        app(StockReservationService::class)->detachFromPurchase($currentPurchase);
        self::query()
            ->whereKey($cart->getKey())
            ->where('current_purchase_id', $currentPurchase->getKey())
            ->update(['current_purchase_id' => null]);
        $this->current_purchase_id = null;
    }

    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;
    }

    private function assertUserPurchaseLimit(
        CatalogItem|Variant $selectedItem,
        int $cartQuantity,
        ?int $excludedPurchaseId = null,
        int $heldQuantity = 0,
        ?int $maximumAddableCeiling = null,
    ): void {
        if ($this->user_id === null) {
            return;
        }

        $catalogItem = $selectedItem instanceof Variant
            ? $selectedItem->catalogItem
            : $selectedItem;

        app(UserPurchaseLimitService::class)->assertCanPurchase(
            $catalogItem,
            $this->user_id,
            $cartQuantity,
            $excludedPurchaseId,
            $this->getKey(),
            $heldQuantity,
            $maximumAddableCeiling,
            field: 'cantidad',
        );
    }

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

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

        return $query->firstOrFail();
    }
}
