<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Cart\Models\Cart;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use RuntimeException;
use Throwable;

class ExpireStockReservationsService
{
    private const BATCH_SIZE = 500;

    public function __construct(
        private readonly ReleaseCheckoutService $purchases,
        private readonly StockReservationService $reservations,
    ) {}

    /**
     * @return array{purchases: int, cart_reservations: int, orphan_reservations: int, failed: int}
     */
    public function expireOverdue(): array
    {
        $summary = [
            'purchases' => 0,
            'cart_reservations' => 0,
            'orphan_reservations' => 0,
            'failed' => 0,
        ];
        $lastReservationId = 0;

        do {
            $reservationIds = StockReservation::query()
                ->where('status', StockReservation::STATUS_ACTIVE)
                ->whereNotNull('expires_at')
                ->where('expires_at', '<=', now())
                ->where('id', '>', $lastReservationId)
                ->orderBy('id')
                ->limit(self::BATCH_SIZE)
                ->pluck('id');

            foreach ($reservationIds as $reservationId) {
                $lastReservationId = (int) $reservationId;

                try {
                    $owner = $this->expireReservation($lastReservationId);
                    if ($owner !== null) {
                        $summary[$owner]++;
                    }
                } catch (Throwable $exception) {
                    $summary['failed']++;
                    Log::channel('commands')->error('Failed to expire overdue stock reservation.', [
                        'command' => 'reservations:expire',
                        'stock_reservation_id' => $lastReservationId,
                        'exception' => $exception,
                    ]);
                }
            }
        } while ($reservationIds->count() === self::BATCH_SIZE);

        Log::channel('commands')->info('Stock reservation cleanup completed.', [
            'command' => 'reservations:expire',
            'expired_purchases' => $summary['purchases'],
            'expired_cart_reservations' => $summary['cart_reservations'],
            'expired_orphan_reservations' => $summary['orphan_reservations'],
            'failed_reservations' => $summary['failed'],
            'total_expired' => $summary['purchases']
                + $summary['cart_reservations']
                + $summary['orphan_reservations'],
        ]);

        return $summary;
    }

    public function expireIfOverdue(int $reservationId): bool
    {
        /** @var StockReservation|null $reservation */
        $reservation = StockReservation::query()->find($reservationId);
        if ($reservation?->status === StockReservation::STATUS_EXPIRED) {
            return true;
        }
        if (! $this->isOverdue($reservation)) {
            return false;
        }

        return $this->expireReservation($reservationId) !== null;
    }

    /** @return 'purchases'|'cart_reservations'|'orphan_reservations'|null */
    private function expireReservation(int $reservationId): ?string
    {
        $purchaseId = Purchase::query()
            ->where('stock_reservation_id', $reservationId)
            ->value('id');
        if ($purchaseId !== null) {
            return $this->expirePurchase((int) $purchaseId);
        }

        $cartId = Cart::query()
            ->where('current_stock_reservation_id', $reservationId)
            ->where('status', 'active')
            ->value('id');
        if ($cartId !== null) {
            return $this->expireCart((int) $cartId, $reservationId);
        }

        return $this->expireOrphan($reservationId);
    }

    /** @return 'purchases'|null */
    private function expirePurchase(int $purchaseId): ?string
    {
        /** @var Purchase|null $purchase */
        $purchase = Purchase::query()->find($purchaseId);
        if ($purchase === null) {
            return null;
        }

        $purchase = $this->purchases->expire($purchase);
        if ($purchase->status === Purchase::STATUS_EXPIRED) {
            return 'purchases';
        }

        $reservation = $purchase->stockReservation;
        if ($this->isOverdue($reservation)) {
            throw new RuntimeException('An overdue active reservation belongs to a purchase that cannot expire.');
        }

        return null;
    }

    /** @return 'cart_reservations'|null */
    private function expireCart(int $cartId, int $reservationId): ?string
    {
        return DB::transaction(function () use ($cartId, $reservationId): ?string {
            /** @var Cart|null $cart */
            $cart = Cart::query()
                ->whereKey($cartId)
                ->where('current_stock_reservation_id', $reservationId)
                ->where('status', Cart::STATUS_ACTIVE)
                ->lockForUpdate()
                ->first();
            if ($cart === null) {
                return null;
            }

            /** @var StockReservation|null $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->find($reservationId);
            if (! $this->isOverdue($reservation)) {
                return null;
            }

            $this->reservations->expire($reservation);
            $cart->update(['status' => Cart::STATUS_EXPIRED]);

            return 'cart_reservations';
        });
    }

    /** @return 'orphan_reservations'|null */
    private function expireOrphan(int $reservationId): ?string
    {
        /** @var StockReservation|null $reservation */
        $reservation = StockReservation::query()->find($reservationId);
        if (! $this->isOverdue($reservation)) {
            return null;
        }

        $this->reservations->expire($reservation);

        return 'orphan_reservations';
    }

    private function isOverdue(?StockReservation $reservation): bool
    {
        return $reservation !== null
            && $reservation->status === StockReservation::STATUS_ACTIVE
            && $reservation->expires_at !== null
            && ! $reservation->expires_at->isFuture();
    }
}
