<?php

namespace App\Domains\Purchase\Services;

use App\Domains\Attachable\Models\Attachment;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
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\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class CheckoutService
{
    public function __construct(
        private readonly CatalogInventoryService $catalogInventoryService,
    ) {}

    public function startCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
    {
        return DB::transaction(function () use ($tenant, $userId, $purchaseData): Purchase {
            $directItem = $purchaseData['direct_item'] ?? null;
            $cartId = isset($purchaseData['cart_id']) ? (int) $purchaseData['cart_id'] : null;
            unset($purchaseData['direct_item'], $purchaseData['cart_id']);

            if (is_array($directItem)) {
                return $this->startDirectCheckout(
                    $tenant,
                    $userId,
                    $purchaseData,
                    $directItem,
                );
            }

            if ($cartId === null) {
                throw ValidationException::withMessages([
                    'cart_id' => __('api.purchase.source_required'),
                ]);
            }

            return $this->startCartCheckout(
                $tenant,
                $userId,
                $purchaseData,
                $cartId,
            );
        });
    }

    public function completePurchase(Purchase $purchase): Purchase
    {
        return DB::transaction(function () use ($purchase): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if ($purchase->payment_method === null) {
                throw ValidationException::withMessages([
                    'payment_method' => __('api.purchase.payment_method_required'),
                ]);
            }

            if (in_array($purchase->status, [
                Purchase::STATUS_PAID,
                Purchase::STATUS_IN_REVIEW,
                Purchase::STATUS_CANCELLED,
                Purchase::STATUS_REJECTED,
                Purchase::STATUS_EXPIRED,
            ], true)) {
                return $this->loadPurchase($purchase);
            }

            $purchase->update([
                'status' => Purchase::STATUS_PENDING_PAYMENT,
                'total' => $purchase->calculateCurrentTotalAmount(),
            ]);

            return $this->loadPurchase($purchase);
        });
    }

    public function submitForReview(Purchase $purchase): Purchase
    {
        return DB::transaction(function () use ($purchase): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if (in_array($purchase->status, [
                Purchase::STATUS_IN_REVIEW,
                Purchase::STATUS_PAID,
            ], true)) {
                return $this->loadPurchase($purchase);
            }

            if (
                $purchase->status !== Purchase::STATUS_PENDING_PAYMENT
                || ($purchase->expires_at !== null && $purchase->expires_at->isPast())
            ) {
                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.not_available_for_review'),
                ]);
            }

            $purchase->update([
                'status' => Purchase::STATUS_IN_REVIEW,
                'expires_at' => null,
            ]);

            return $this->loadPurchase($purchase);
        });
    }

    /**
     * @param  array<string, string>  $customerData
     */
    public function updateCustomerData(Purchase $purchase, array $customerData): Purchase
    {
        return DB::transaction(function () use ($purchase, $customerData): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if (
                ! in_array($purchase->status, [
                    Purchase::STATUS_CREATED,
                    Purchase::STATUS_PENDING_PAYMENT,
                ], true)
                || ($purchase->expires_at !== null && $purchase->expires_at->isPast())
            ) {
                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.not_editable'),
                ]);
            }

            $purchase->update($customerData);

            return $this->loadPurchase($purchase);
        });
    }

    public function updateItemQuantity(
        Purchase $purchase,
        PurchaseItem $purchaseItem,
        int $quantity,
    ): Purchase {
        return DB::transaction(function () use ($purchase, $purchaseItem, $quantity): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if (
                $purchase->status !== Purchase::STATUS_CREATED
                || ($purchase->expires_at !== null && $purchase->expires_at->isPast())
            ) {
                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.not_editable'),
                ]);
            }

            /** @var PurchaseItem|null $purchaseItem */
            $purchaseItem = $purchase->items()
                ->whereKey($purchaseItem->getKey())
                ->lockForUpdate()
                ->first();

            if ($purchaseItem === null) {
                throw new NotFoundHttpException('Purchase item not found.');
            }

            if ($purchaseItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) {
                throw ValidationException::withMessages([
                    'item' => __('api.purchase.item_not_editable'),
                ]);
            }

            $currentQuantity = (int) $purchaseItem->cantidad;
            $difference = $quantity - $currentQuantity;

            if ($difference !== 0) {
                $selection = $this->resolvePurchaseItemSelection($purchase->tenant, $purchaseItem);

                try {
                    if ($difference > 0) {
                        $this->catalogInventoryService->reserve($selection, $difference);
                    } else {
                        $this->catalogInventoryService->release($selection, abs($difference));
                    }
                } catch (\InvalidArgumentException $exception) {
                    throw ValidationException::withMessages([
                        'quantity' => __('api.purchase.insufficient_stock'),
                    ]);
                }

                $purchaseItem->update([
                    'cantidad' => $quantity,
                    'total' => (float) $purchaseItem->precio_unitario * $quantity,
                ]);
                $this->syncSourceCartItemQuantity($purchase, $purchaseItem, $quantity);
            }

            $purchase->update([
                'total' => $purchase->calculateCurrentTotalAmount(),
            ]);

            return $this->loadPurchase($purchase);
        });
    }

    public function prepareItemEditing(Purchase $purchase): Purchase
    {
        return DB::transaction(function () use ($purchase): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if (
                ! in_array($purchase->status, [
                    Purchase::STATUS_CREATED,
                    Purchase::STATUS_PENDING_PAYMENT,
                ], true)
                || ($purchase->expires_at !== null && $purchase->expires_at->isPast())
            ) {
                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.not_editable'),
                ]);
            }

            $purchase->telepagosQr()->delete();
            $purchase->update([
                'status' => Purchase::STATUS_CREATED,
                'payment_method' => null,
                'transfer_payer_dni' => null,
                'expires_at' => now()->addMinutes(
                    max(1, (int) config('purchase.checkout_expiration_minutes', 30)),
                ),
            ]);

            return $this->loadPurchase($purchase);
        });
    }

    public function confirmPurchase(Purchase $purchase): void
    {
        DB::transaction(function () use ($purchase): void {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if ($purchase->status === Purchase::STATUS_PAID) {
                return;
            }

            if (in_array($purchase->status, [
                Purchase::STATUS_CANCELLED,
                Purchase::STATUS_REJECTED,
                Purchase::STATUS_EXPIRED,
            ], true)) {
                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.cannot_confirm'),
                ]);
            }

            $items = $purchase->items()
                ->where('reservation_status', PurchaseItem::RESERVATION_ACTIVE)
                ->lockForUpdate()
                ->get();

            foreach ($items as $item) {
                $selection = $this->resolvePurchaseItemSelection($purchase->tenant, $item);

                try {
                    $this->catalogInventoryService->commit($selection, (int) $item->cantidad);
                } catch (\InvalidArgumentException $exception) {
                    throw ValidationException::withMessages([
                        'items' => __('api.purchase.inconsistent_reservation'),
                    ]);
                }

                $item->update([
                    'reservation_status' => PurchaseItem::RESERVATION_COMMITTED,
                ]);
            }

            $this->finalizeSourceCart($purchase);
        });
    }

    public function cancelPurchase(Purchase $purchase): Purchase
    {
        return $this->releasePurchase($purchase, Purchase::STATUS_CANCELLED);
    }

    public function expirePurchase(Purchase $purchase): Purchase
    {
        return $this->releasePurchase($purchase, Purchase::STATUS_EXPIRED);
    }

    public function expireOverduePurchases(): int
    {
        $expiredCount = 0;

        Purchase::query()
            ->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT])
            ->whereNotNull('expires_at')
            ->where('expires_at', '<=', now())
            ->orderBy('id')
            ->eachById(function (Purchase $purchase) use (&$expiredCount): void {
                $purchase = $this->expirePurchase($purchase);

                if ($purchase->status === Purchase::STATUS_EXPIRED) {
                    $expiredCount++;
                }
            });

        return $expiredCount;
    }

    private function releasePurchase(Purchase $purchase, string $targetStatus): Purchase
    {
        return DB::transaction(function () use ($purchase, $targetStatus): Purchase {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()
                ->lockForUpdate()
                ->findOrFail($purchase->getKey());

            if ($purchase->status === Purchase::STATUS_PAID) {
                if ($targetStatus === Purchase::STATUS_EXPIRED) {
                    return $this->loadPurchase($purchase);
                }

                throw ValidationException::withMessages([
                    'purchase' => __('api.purchase.paid_cannot_cancel'),
                ]);
            }

            if (in_array($purchase->status, [
                Purchase::STATUS_CANCELLED,
                Purchase::STATUS_REJECTED,
                Purchase::STATUS_EXPIRED,
            ], true)) {
                return $this->loadPurchase($purchase);
            }

            if (
                $targetStatus === Purchase::STATUS_EXPIRED
                && ($purchase->expires_at === null || $purchase->expires_at->isFuture())
            ) {
                return $this->loadPurchase($purchase);
            }

            $items = $purchase->items()
                ->where('reservation_status', PurchaseItem::RESERVATION_ACTIVE)
                ->lockForUpdate()
                ->get();
            $reservationReturnedToCart = $this->restoreSourceCart($purchase);

            foreach ($items as $item) {
                if (! $reservationReturnedToCart) {
                    $selection = $this->resolvePurchaseItemSelection($purchase->tenant, $item);

                    try {
                        $this->catalogInventoryService->release($selection, (int) $item->cantidad);
                    } catch (\InvalidArgumentException $exception) {
                        throw ValidationException::withMessages([
                            'items' => __('api.purchase.inconsistent_reservation'),
                        ]);
                    }
                }

                $item->update([
                    'reservation_status' => PurchaseItem::RESERVATION_RELEASED,
                ]);
            }

            $purchase->update([
                'status' => $targetStatus,
            ]);

            return $this->loadPurchase($purchase);
        });
    }

    /**
     * @param  array<string, mixed>  $purchaseData
     * @param  array<string, mixed>  $directItem
     */
    private function startDirectCheckout(
        Tenant $tenant,
        int $userId,
        array $purchaseData,
        array $directItem,
    ): Purchase {
        $catalogItemId = (int) $directItem['catalog_item_id'];
        $variantId = isset($directItem['variant_id']) ? (int) $directItem['variant_id'] : null;
        $quantity = (int) $directItem['cantidad'];
        $selection = $this->resolveSelection($tenant, $catalogItemId, $variantId);
        $availableQuantity = $this->catalogInventoryService->availableQuantity($selection);

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

        try {
            $this->catalogInventoryService->reserve($selection, $quantity);
        } catch (\InvalidArgumentException $exception) {
            throw ValidationException::withMessages([
                'direct_item.cantidad' => __('api.purchase.insufficient_stock'),
            ]);
        }

        $purchase = $this->createPurchase(
            $tenant,
            $userId,
            $purchaseData,
            $selection->getPrice() * $quantity,
            null,
        );
        $cartItem = $this->makeDirectCartItem($selection, $catalogItemId, $variantId, $quantity);
        $purchase->items()->createMany(
            $this->buildPurchaseItemsPayload(collect([$cartItem])),
        );

        return $this->loadPurchase($purchase);
    }

    /**
     * @param  array<string, mixed>  $purchaseData
     */
    private function startCartCheckout(
        Tenant $tenant,
        int $userId,
        array $purchaseData,
        int $cartId,
    ): Purchase {
        $cart = $this->resolveCheckoutCart($tenant, $userId, $cartId);
        $cartItems = $cart->items()->lockForUpdate()->get();

        if ($cartItems->isEmpty()) {
            throw ValidationException::withMessages([
                'cart_id' => __('api.purchase.empty_cart'),
            ]);
        }

        $this->loadCartItems($cartItems);
        $this->verifyTenantItems($tenant, $cartItems);
        $cart->setRelation('items', $cartItems);

        $purchase = $this->createPurchase(
            $tenant,
            $userId,
            $purchaseData,
            $cart->getTotalAmount(),
            $cart->getKey(),
        );
        $purchase->items()->createMany(
            $this->buildPurchaseItemsPayload($cartItems),
        );

        // PurchaseItem owns the reservation during checkout. The source cart is
        // kept with its owner so it can be restored if the purchase is cancelled
        // or expires. Only active carts participate in the identity constraint.
        $cart->update([
            'status' => 'checkout',
            'guest_token' => null,
        ]);

        return $this->loadPurchase($purchase);
    }

    private function restoreSourceCart(Purchase $purchase): bool
    {
        if ($purchase->cart_id === null) {
            return false;
        }

        /** @var Cart|null $sourceCart */
        $sourceCart = Cart::withTrashed()
            ->whereKey($purchase->cart_id)
            ->lockForUpdate()
            ->first();

        if ($sourceCart === null) {
            return false;
        }

        /** @var Cart|null $activeCart */
        $activeCart = Cart::query()
            ->where('tenant_codigo', $purchase->tenant_codigo)
            ->where('user_id', $purchase->user_id)
            ->where('status', 'active')
            ->where('id', '!=', $sourceCart->getKey())
            ->lockForUpdate()
            ->first();

        if ($activeCart !== null) {
            $sourceItems = $sourceCart->items()->lockForUpdate()->get();

            foreach ($sourceItems as $sourceItem) {
                /** @var CartItem|null $activeItem */
                $activeItem = $activeCart->items()
                    ->where('catalog_item_id', $sourceItem->catalog_item_id)
                    ->where('variant_id', $sourceItem->variant_id)
                    ->lockForUpdate()
                    ->first();

                if ($activeItem === null) {
                    $activeCart->items()->create([
                        'catalog_item_id' => $sourceItem->catalog_item_id,
                        'variant_id' => $sourceItem->variant_id,
                        'cantidad' => $sourceItem->cantidad,
                    ]);
                } else {
                    $activeItem->increment('cantidad', (int) $sourceItem->cantidad);
                }
            }

            $sourceCart->update([
                'status' => 'converted',
                'guest_token' => null,
            ]);

            if (! $sourceCart->trashed()) {
                $sourceCart->delete();
            }

            return true;
        }

        if ($sourceCart->trashed()) {
            $sourceCart->restore();
        }

        $sourceCart->update([
            'status' => 'active',
            'user_id' => $purchase->user_id,
            'guest_token' => null,
        ]);

        return true;
    }

    private function syncSourceCartItemQuantity(
        Purchase $purchase,
        PurchaseItem $purchaseItem,
        int $quantity,
    ): void {
        if ($purchase->cart_id === null) {
            return;
        }

        $sourceCart = Cart::withTrashed()
            ->whereKey($purchase->cart_id)
            ->lockForUpdate()
            ->first();

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

        $sourceCart->items()
            ->where('catalog_item_id', $purchaseItem->source_catalog_item_id)
            ->where('variant_id', $purchaseItem->source_variant_id)
            ->update([
                'cantidad' => $quantity,
            ]);
    }

    private function finalizeSourceCart(Purchase $purchase): void
    {
        if ($purchase->cart_id === null) {
            return;
        }

        /** @var Cart|null $sourceCart */
        $sourceCart = Cart::withTrashed()
            ->whereKey($purchase->cart_id)
            ->lockForUpdate()
            ->first();

        if ($sourceCart === null || $sourceCart->trashed()) {
            return;
        }

        $sourceCart->update([
            'status' => 'converted',
            'guest_token' => null,
        ]);
        $sourceCart->delete();
    }

    /**
     * @param  array<string, mixed>  $purchaseData
     */
    private function createPurchase(
        Tenant $tenant,
        int $userId,
        array $purchaseData,
        float $total,
        ?int $cartId,
    ): Purchase {
        return Purchase::query()->create([
            ...$purchaseData,
            'cart_id' => $cartId,
            'tenant_codigo' => $tenant->codigo,
            'user_id' => $userId,
            'status' => Purchase::STATUS_CREATED,
            'payment_method' => null,
            'expires_at' => now()->addMinutes(
                max(1, (int) config('purchase.checkout_expiration_minutes', 30)),
            ),
            'total' => $total,
        ]);
    }

    protected function verifyTenantItems(Tenant $tenant, Collection $cartItems): void
    {
        foreach ($cartItems as $item) {
            if ($item->selectedItem() === null) {
                throw ValidationException::withMessages([
                    'cart_id' => __('api.purchase.catalog_item_missing'),
                ]);
            }

            if ($item->catalogItem?->tenant_code !== $tenant->codigo) {
                throw ValidationException::withMessages([
                    'cart_id' => __('api.purchase.catalog_item_wrong_tenant'),
                ]);
            }
        }
    }

    protected function resolveCheckoutCart(Tenant $tenant, int $userId, int $cartId): Cart
    {
        /** @var Cart|null $cart */
        $cart = Cart::query()
            ->lockForUpdate()
            ->find($cartId);

        if ($cart === null || $cart->tenant_codigo !== $tenant->codigo || $cart->user_id !== $userId) {
            throw new NotFoundHttpException('Cart not found for tenant.');
        }

        if ($cart->status !== 'active') {
            throw ValidationException::withMessages([
                'cart_id' => __('api.purchase.inactive_cart'),
            ]);
        }

        return $cart;
    }

    /**
     * @param  Collection<int, CartItem>  $cartItems
     * @return array<int, array<string, mixed>>
     */
    protected function buildPurchaseItemsPayload(Collection $cartItems): array
    {
        return $cartItems
            ->map(function (CartItem $item): array {
                $selectedItem = $item->selectedItem();
                $quantity = (int) $item->cantidad;
                $unitPrice = $selectedItem?->getPrice() ?? 0;
                $imageAttachment = $this->firstImageAttachment($item);

                return [
                    'source_catalog_item_id' => $item->catalog_item_id,
                    'source_variant_id' => $item->variant_id,
                    'image_attachment_id' => $imageAttachment?->id,
                    'nombre' => $item->catalogItem->nombre,
                    'descripcion' => $item->catalogItem->descripcion,
                    'slug' => $item->catalogItem->slug,
                    'item_nombre' => $selectedItem->getName(),
                    'variant_attributes' => $item->variant === null
                        ? []
                        : $this->snapshotAttributes($item->variant),
                    'cantidad' => $quantity,
                    'precio_unitario' => $unitPrice,
                    'discount_total' => null,
                    'tax_total' => null,
                    'total' => $unitPrice * $quantity,
                    'reservation_status' => PurchaseItem::RESERVATION_ACTIVE,
                ];
            })
            ->all();
    }

    private function resolveSelection(
        Tenant $tenant,
        int $catalogItemId,
        ?int $variantId,
    ): CatalogItem|Variant {
        /** @var CatalogItem|null $catalogItem */
        $catalogItem = CatalogItem::query()
            ->whereKey($catalogItemId)
            ->where('tenant_code', $tenant->codigo)
            ->lockForUpdate()
            ->first();

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

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

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

            return $catalogItem;
        }

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

            $catalogItem->setRelation(
                'inventory',
                Inventory::query()->whereKey($catalogItem->inventory_id)->lockForUpdate()->firstOrFail(),
            );

            return $catalogItem;
        }

        /** @var Variant|null $variant */
        $variant = Variant::query()
            ->whereKey($variantId)
            ->where('catalog_item_id', $catalogItem->id)
            ->lockForUpdate()
            ->first();

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

        $variant->setRelation('catalogItem', $catalogItem);
        $variant->setRelation(
            'inventory',
            Inventory::query()->whereKey($variant->inventory_id)->lockForUpdate()->firstOrFail(),
        );

        return $variant;
    }

    private function resolvePurchaseItemSelection(Tenant $tenant, PurchaseItem $item): CatalogItem|Variant
    {
        return $this->resolveSelection(
            $tenant,
            (int) $item->source_catalog_item_id,
            $item->source_variant_id === null ? null : (int) $item->source_variant_id,
        );
    }

    private function makeDirectCartItem(
        CatalogItem|Variant $selection,
        int $catalogItemId,
        ?int $variantId,
        int $quantity,
    ): CartItem {
        $catalogItem = $selection instanceof Variant
            ? $selection->catalogItem
            : $selection;
        $catalogItem->loadMissing(['inventory', 'attachments']);

        if ($selection instanceof Variant) {
            $selection->loadMissing([
                'inventory',
                'attachments',
                'catalogItem',
                'definitions.itemAttribute.attribute',
            ]);
        }

        $item = new CartItem([
            'catalog_item_id' => $catalogItemId,
            'variant_id' => $variantId,
            'cantidad' => $quantity,
        ]);
        $item->setRelation('catalogItem', $catalogItem);
        $item->setRelation('variant', $selection instanceof Variant ? $selection : null);

        return $item;
    }

    /** @param Collection<int, CartItem> $cartItems */
    private function loadCartItems(Collection $cartItems): void
    {
        $cartItems->load([
            'catalogItem.inventory',
            'catalogItem.attachments',
            'variant.inventory',
            'variant.attachments',
            'variant.catalogItem',
            'variant.definitions.itemAttribute.attribute',
        ]);
    }

    private function loadPurchase(Purchase $purchase): Purchase
    {
        return $purchase->load([
            'items.imageAttachment',
        ]);
    }

    private function firstImageAttachment(CartItem $item): ?Attachment
    {
        return $item->variant?->attachments->first()
            ?? $item->catalogItem?->attachments->first();
    }

    /** @return array<int, array{name: string, value: mixed}> */
    private function snapshotAttributes(Variant $variant): array
    {
        return $variant->definitions
            ->map(fn ($definition): array => [
                'name' => (string) ($definition->itemAttribute?->attribute?->nombre ?? ''),
                'value' => $definition->value,
            ])
            ->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null)
            ->values()
            ->all();
    }
}
