<?php

namespace App\Domains\Purchase\Services;

use App\Domains\Attachable\Models\Attachment;
use App\Domains\Attachable\Services\AttachmentService;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

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

    public function startCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
    {
        $cartId = (int) $purchaseData['cart_id'];
        unset($purchaseData['cart_id']);

        return DB::transaction(function () use ($tenant, $userId, $purchaseData, $cartId): Purchase {
            $cart = $this->resolveCheckoutCart($tenant, $userId, $cartId);
            $cartItems = $cart->items()->lockForUpdate()->get();

            if ($cartItems->isEmpty()) {
                throw ValidationException::withMessages([
                    'cart_id' => 'The selected cart does not contain items.',
                ]);
            }

            $this->loadCartItems($cartItems);
            $cart->setRelation('items', $cartItems);
            $totalAmount = $cart->getTotalAmount();

            /** @var Purchase|null $purchase */
            $purchase = Purchase::query()
                ->where('cart_id', $cart->getKey())
                ->where('tenant_codigo', $tenant->codigo)
                ->where('user_id', $userId)
                ->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT])
                ->latest('id')
                ->first();

            if ($purchase === null) {
                /** @var Purchase $purchase */
                $purchase = Purchase::query()->create([
                    ...$purchaseData,
                    'cart_id' => $cart->getKey(),
                    'tenant_codigo' => $tenant->codigo,
                    'user_id' => $userId,
                    'status' => Purchase::STATUS_CREATED,
                    'payment_method' => null,
                    'total' => $totalAmount,
                ]);
            } else {
                $purchase->fill([
                    ...$purchaseData,
                    'status' => Purchase::STATUS_CREATED,
                    'payment_method' => null,
                    'total' => $totalAmount,
                ]);
                $purchase->save();
            }

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

    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' => 'The purchase payment method must be selected before finalizing.',
                ]);
            }

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

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

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

    public function confirmPurchase(Purchase $purchase): void
    {
        $snapshotPaths = [];

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

                if ($purchase->items()->exists()) {
                    return;
                }

                /** @var Cart|null $cart */
                $cart = $purchase->cart()->lockForUpdate()->first();

                if ($cart === null) {
                    throw ValidationException::withMessages([
                        'cart_id' => 'The purchase cart is no longer available.',
                    ]);
                }

                $cartItems = $cart->items()->lockForUpdate()->get();

                if ($cartItems->isEmpty()) {
                    throw ValidationException::withMessages([
                        'cart_id' => 'The purchase cart does not contain items.',
                    ]);
                }

                $this->loadCartItems($cartItems);
                $this->verifyTenantItems($purchase->tenant, $cartItems);
                $purchaseItemsPayload = $this->buildPurchaseItemsPayload($purchase, $cartItems, $snapshotPaths);

                $purchase->items()->createMany($purchaseItemsPayload);
                $this->completeCartConversion($cart, $cartItems);
            });
        } catch (Throwable $throwable) {
            foreach ($snapshotPaths as $snapshotPath) {
                Storage::disk('s3')->delete($snapshotPath);
            }

            throw $throwable;
        }
    }

    protected function verifyTenantItems(Tenant $tenant, Collection $cartItems): void
    {
        foreach ($cartItems as $item) {
            $selectedItem = $item->selectedItem();
            if ($selectedItem === null) {
                throw ValidationException::withMessages([
                    'cart_id' => 'One or more catalog items could not be loaded.',
                ]);
            }

            if ($item->catalogItem?->tenant_code !== $tenant->codigo) {
                throw ValidationException::withMessages([
                    'cart_id' => 'One or more catalog items do not belong to the tenant.',
                ]);
            }
        }
    }

    /**
     * @param  Collection<int, CartItem>  $cartItems
     * @return Collection<int, CartItem>
     */
    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' => 'The selected cart is no longer active.',
            ]);
        }

        return $cart;
    }

    /**
     * @param  Collection<int, CartItem>  $cartItems
     * @return array<int, array<string, mixed>>
     */
    protected function buildPurchaseItemsPayload(
        Purchase $purchase,
        Collection $cartItems,
        array &$snapshotPaths = [],
    ): array {
        return $cartItems
            ->map(function (CartItem $item) use ($purchase, &$snapshotPaths): array {
                $selectedItem = $item->selectedItem();
                $quantity = (int) $item['cantidad'];
                $unitPrice = $selectedItem?->getPrice() ?? 0;
                $imageAttachment = $this->snapshotFirstImage($purchase, $item);

                if ($imageAttachment !== null) {
                    $snapshotPaths[] = $imageAttachment->path;
                }

                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,
                ];
            })
            ->all();
    }

    /**
     * @param  Collection<int, CartItem>  $cartItems
     */
    protected function completeCartConversion(Cart $cart, Collection $cartItems): void
    {
        foreach ($cartItems as $item) {
            $selectedItem = $item->selectedItem();
            $quantity = (int) $item->cantidad;

            try {
                $this->catalogInventoryService->commit(
                    $selectedItem,
                    $quantity,
                );
            } catch (\InvalidArgumentException $exception) {
                throw ValidationException::withMessages([
                    'cart_id' => 'The selected cart has inconsistent stock state.',
                ]);
            }
        }

        $cart->status = 'converted';
        $cart->user_id = null;
        $cart->guest_token = null;
        $cart->save();
        $cart->delete();
    }

    /** @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 snapshotFirstImage(Purchase $purchase, CartItem $item): ?Attachment
    {
        $source = $item->variant?->attachments->first()
            ?? $item->catalogItem?->attachments->first();

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

        return $this->attachmentService->copy(
            $source,
            "purchase/{$purchase->id}",
        );
    }

    /** @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();
    }
}
