<?php

namespace App\Domains\Purchase\Services;

use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\PurchaseItem;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class UserPurchaseLimitService
{
    public function assertCanPurchase(
        CatalogItem $catalogItem,
        int $userId,
        int $requestedQuantity,
        ?int $excludedPurchaseId = null,
        ?int $excludedCartId = null,
        int $heldQuantity = 0,
        ?int $maximumAddableCeiling = null,
        string $field = 'quantity',
    ): void {
        DB::transaction(function () use (
            $catalogItem,
            $userId,
            $requestedQuantity,
            $excludedPurchaseId,
            $excludedCartId,
            $heldQuantity,
            $maximumAddableCeiling,
            $field,
        ): void {
            /** @var CatalogItem $catalogItem */
            $catalogItem = CatalogItem::query()
                ->whereKey($catalogItem->getKey())
                ->lockForUpdate()
                ->firstOrFail();
            $limit = $catalogItem->max_units_per_user;

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

            $purchasedQuantity = (int) PurchaseItem::query()
                ->where('source_catalog_item_id', $catalogItem->getKey())
                ->whereHas('purchase', function ($query) use ($userId, $excludedPurchaseId): void {
                    $query
                        ->where('user_id', $userId)
                        ->whereIn('status', [
                            Purchase::STATUS_CREATED,
                            Purchase::STATUS_PENDING_PAYMENT,
                            Purchase::STATUS_IN_REVIEW,
                            Purchase::STATUS_PAID,
                        ])
                        ->when(
                            $excludedPurchaseId !== null,
                            fn ($query) => $query->whereKeyNot($excludedPurchaseId),
                        );
                })
                ->sum('cantidad');

            $checkoutQuantity = (int) CartItem::query()
                ->where('catalog_item_id', $catalogItem->getKey())
                ->whereHas('cart.purchases', function ($query) use ($userId, $excludedPurchaseId): void {
                    $query
                        ->where('user_id', $userId)
                        ->whereIn('status', [
                            Purchase::STATUS_CREATED,
                            Purchase::STATUS_PENDING_PAYMENT,
                            Purchase::STATUS_IN_REVIEW,
                        ])
                        ->whereDoesntHave('items')
                        ->when(
                            $excludedPurchaseId !== null,
                            fn ($query) => $query->whereKeyNot($excludedPurchaseId),
                        );
                })
                ->sum('cantidad');

            $reservedCartQuantity = (int) CartItem::query()
                ->where('catalog_item_id', $catalogItem->getKey())
                ->whereHas('cart', fn ($query) => $query
                    ->where('user_id', $userId)
                    ->where('status', 'active')
                    ->when(
                        $excludedCartId !== null,
                        fn ($query) => $query->whereKeyNot($excludedCartId),
                    ))
                ->whereHas('cart.currentStockReservation', fn ($query) => $query
                    ->where('status', 'active')
                    ->whereDoesntHave('purchase'))
                ->sum('cantidad');

            if ($purchasedQuantity + $checkoutQuantity + $reservedCartQuantity + $requestedQuantity > $limit) {
                $remainingQuota = max(
                    0,
                    $limit - $purchasedQuantity - $checkoutQuantity - $reservedCartQuantity,
                );

                $maximumAddableQuantity = max(0, $remainingQuota - $heldQuantity);

                throw new PurchaseLimitExceededException(
                    $catalogItem,
                    $maximumAddableCeiling === null
                        ? $maximumAddableQuantity
                        : min($maximumAddableQuantity, $maximumAddableCeiling),
                    $field,
                );
            }
        });
    }

    /**
     * @param  Collection<int, CatalogItem>  $catalogItems
     * @return Collection<int, int|null>
     */
    public function remainingByCatalogItem(Collection $catalogItems, ?int $userId): Collection
    {
        $limits = $catalogItems
            ->unique('id')
            ->mapWithKeys(fn (CatalogItem $item): array => [$item->getKey() => $item->max_units_per_user]);

        if ($userId === null || $limits->filter(fn ($limit) => $limit !== null)->isEmpty()) {
            return $limits->map(fn (): ?int => null);
        }

        $ids = $limits->keys();
        $purchased = PurchaseItem::query()
            ->selectRaw('source_catalog_item_id, SUM(cantidad) AS quantity')
            ->whereIn('source_catalog_item_id', $ids)
            ->whereHas('purchase', fn ($query) => $query
                ->where('user_id', $userId)
                ->whereIn('status', [
                    Purchase::STATUS_CREATED,
                    Purchase::STATUS_PENDING_PAYMENT,
                    Purchase::STATUS_IN_REVIEW,
                    Purchase::STATUS_PAID,
                ]))
            ->groupBy('source_catalog_item_id')
            ->pluck('quantity', 'source_catalog_item_id');

        $checkout = CartItem::query()
            ->selectRaw('catalog_item_id, SUM(cantidad) AS quantity')
            ->whereIn('catalog_item_id', $ids)
            ->whereHas('cart.purchases', fn ($query) => $query
                ->where('user_id', $userId)
                ->whereIn('status', [
                    Purchase::STATUS_CREATED,
                    Purchase::STATUS_PENDING_PAYMENT,
                    Purchase::STATUS_IN_REVIEW,
                ])
                ->whereDoesntHave('items'))
            ->groupBy('catalog_item_id')
            ->pluck('quantity', 'catalog_item_id');

        $reserved = CartItem::query()
            ->selectRaw('catalog_item_id, SUM(cantidad) AS quantity')
            ->whereIn('catalog_item_id', $ids)
            ->whereHas('cart', fn ($query) => $query
                ->where('user_id', $userId)
                ->where('status', 'active'))
            ->whereHas('cart.currentStockReservation', fn ($query) => $query
                ->where('status', 'active')
                ->whereDoesntHave('purchase'))
            ->groupBy('catalog_item_id')
            ->pluck('quantity', 'catalog_item_id');

        return $limits->map(function (?int $limit, int $catalogItemId) use ($purchased, $checkout, $reserved): ?int {
            if ($limit === null) {
                return null;
            }

            $used = (int) ($purchased[$catalogItemId] ?? 0)
                + (int) ($checkout[$catalogItemId] ?? 0)
                + (int) ($reserved[$catalogItemId] ?? 0);

            return max(0, $limit - $used);
        });
    }
}
