<?php

namespace App\Services;

use App\Exceptions\InsufficientStockException;
use App\Exceptions\StockException;
use App\Models\Order;
use App\Models\Product;
use App\Models\Setting;
use App\Models\StockMovement;
use App\Models\StockReservation;
use App\Models\VariantStockMovement;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class InventoryService
{
    /**
     * Default reservation TTL in seconds (15 minutes).
     * Configurable via Settings: stock_reservation_ttl_seconds
     */
    protected int $defaultReservationTtl = 900;

    public function checkAvailability(int $productId, int $quantity): bool
    {
        $product = Product::find($productId);

        if (!$product) {
            return false;
        }

        // Get available stock (total stock minus pending reservations)
        $availableStock = $this->getAvailableStock($productId);

        return $availableStock >= $quantity;
    }

    /**
     * Get available stock (total stock minus pending reservations)
     */
    public function getAvailableStock(int $productId): int
    {
        $product = Product::find($productId);

        if (!$product) {
            return 0;
        }

        // Get total reserved quantity for pending reservations
        $reservedQuantity = StockReservation::where('product_id', $productId)
            ->pending()
            ->sum('quantity');

        // Use current_stock (calculated from movements) instead of legacy stock field
        $currentStock = $product->current_stock;
        return max(0, $currentStock - $reservedQuantity);
    }

    /**
     * Reserve stock with database lock to prevent race conditions
     *
     * @param int $productId
     * @param int $quantity
     * @param int|null $ttl Reservation TTL in seconds (default: 5 minutes)
     * @param int|null $orderId Optional order ID to link reservation
     * @return string Reservation token
     * @throws InsufficientStockException
     */
    public function reserve(int $productId, int $quantity, ?int $ttl = null, ?int $orderId = null): string
    {
        if ($ttl === null) {
            $configured = Setting::get('stock_reservation_ttl_seconds');
            $ttl = $configured !== '' && $configured !== null ? (int) $configured : $this->defaultReservationTtl;
            $ttl = max(60, min(3600, $ttl)); // Clamp between 1 min and 60 min
        }

        return DB::transaction(function () use ($productId, $quantity, $ttl, $orderId) {
            // Lock the product row for update to prevent race conditions
            $product = Product::lockForUpdate()->find($productId);

            if (!$product) {
                throw new StockException("Product not found: {$productId}");
            }

            // Calculate available stock (total - pending reservations)
            $reservedQuantity = StockReservation::where('product_id', $productId)
                ->pending()
                ->sum('quantity');

            // Use current_stock (calculated from movements) instead of legacy stock field
            $currentStock = $product->current_stock;
            $availableStock = max(0, $currentStock - $reservedQuantity);

            if ($availableStock < $quantity) {
                throw new InsufficientStockException(
                    $product->name,
                    $quantity,
                    $availableStock
                );
            }

            // Create reservation
            $reservation = StockReservation::create([
                'product_id' => $productId,
                'order_id' => $orderId,
                'quantity' => $quantity,
                'reservation_token' => Str::random(64),
                'expires_at' => now()->addSeconds($ttl),
                'status' => 'pending',
            ]);

            Log::info('Stock reserved', [
                'product_id' => $productId,
                'quantity' => $quantity,
                'reservation_token' => $reservation->reservation_token,
                'expires_at' => $reservation->expires_at,
            ]);

            return $reservation->reservation_token;
        });
    }

    /**
     * Confirm reservation and decrement stock
     *
     * @param string $reservationToken
     * @return bool
     * @throws StockException
     */
    public function confirmReservation(string $reservationToken): bool
    {
        return DB::transaction(function () use ($reservationToken) {
            $reservation = StockReservation::where('reservation_token', $reservationToken)
                ->where('status', 'pending')
                ->lockForUpdate()
                ->first();

            if (!$reservation) {
                throw new StockException("Reservation not found or already processed: {$reservationToken}");
            }

            if ($reservation->isExpired()) {
                $reservation->update(['status' => 'expired']);
                throw new StockException("Reservation expired: {$reservationToken}");
            }

            // Lock product and decrement stock
            $product = Product::lockForUpdate()->find($reservation->product_id);

            // Use current_stock (calculated from movements) instead of legacy stock field
            $currentStock = $product->current_stock;
            
            if (!$product || $currentStock < $reservation->quantity) {
                $reservation->update(['status' => 'released']);
                throw new InsufficientStockException(
                    $product->name ?? 'Unknown',
                    $reservation->quantity,
                    $currentStock ?? 0
                );
            }

            // Create stock movement for sale (this will update current_stock via recalculateStock)
            $notes = $reservation->order_id !== null
                ? "Order #{$reservation->order_id}"
                : "Reservation confirmed: {$reservationToken}";
            if ($product->isVariant()) {
                \App\Models\VariantStockMovement::create([
                    'product_id' => $product->id,
                    'type' => \App\Models\VariantStockMovement::TYPE_SALE,
                    'quantity' => -$reservation->quantity,
                    'notes' => $notes,
                    'created_by' => 1, // System user
                ]);
            } else {
                \App\Models\StockMovement::create([
                    'product_id' => $product->id,
                    'type' => \App\Models\StockMovement::TYPE_SALE,
                    'quantity' => -$reservation->quantity,
                    'notes' => $notes,
                    'created_by' => 1, // System user
                ]);
            }
            
            // Recalculate stock
            $product->recalculateStock();

            // Mark reservation as confirmed
            $reservation->update(['status' => 'confirmed']);

            Log::info('Stock reservation confirmed and decremented', [
                'reservation_token' => $reservationToken,
                'product_id' => $reservation->product_id,
                'quantity' => $reservation->quantity,
            ]);

            return true;
        });
    }

    /**
     * Release reservation (cancel it)
     *
     * @param string $reservationToken
     * @return bool
     */
    public function release(string $reservationToken): bool
    {
        $reservation = StockReservation::where('reservation_token', $reservationToken)
            ->where('status', 'pending')
            ->first();

        if (!$reservation) {
            return false;
        }

        $reservation->update(['status' => 'released']);

        Log::info('Stock reservation released', [
            'reservation_token' => $reservationToken,
            'product_id' => $reservation->product_id,
            'quantity' => $reservation->quantity,
        ]);

        return true;
    }

    /**
     * Release reservation by order ID
     *
     * @param int $orderId
     * @return int Number of reservations released
     */
    public function releaseByOrderId(int $orderId, bool $cancelAwaitingPayment = false): int
    {
        $count = StockReservation::where('order_id', $orderId)
            ->where('status', 'pending')
            ->update(['status' => 'released']);

        if ($count > 0) {
            Log::info('Stock reservations released by order ID', [
                'order_id' => $orderId,
                'count' => $count,
            ]);
        }

        if ($cancelAwaitingPayment) {
            $this->cancelAwaitingPaymentOrder($orderId, 'reservation_released');
        }

        return $count;
    }

    /**
     * Decrement stock directly (with lock for race condition prevention)
     * Use this for direct stock decrements without reservations
     *
     * @param int $productId
     * @param int $quantity
     * @return bool
     * @throws InsufficientStockException
     */
    public function decrement(int $productId, int $quantity): bool
    {
        return DB::transaction(function () use ($productId, $quantity) {
            // Lock product row
            $product = Product::lockForUpdate()->find($productId);

            if (!$product) {
                throw new StockException("Product not found: {$productId}");
            }

            // Check available stock (considering pending reservations)
            $availableStock = $this->getAvailableStock($productId);

            if ($availableStock < $quantity) {
                throw new InsufficientStockException(
                    $product->name,
                    $quantity,
                    $availableStock
                );
            }

            // Use stock movements instead of direct decrement
            $currentStock = $product->current_stock;
            
            if ($currentStock < $quantity) {
                throw new InsufficientStockException(
                    $product->name,
                    $quantity,
                    $currentStock
                );
            }
            
            // Create stock movement
            \App\Models\StockMovement::create([
                'product_id' => $productId,
                'type' => \App\Models\StockMovement::TYPE_SALE,
                'quantity' => -$quantity,
                'notes' => 'Direct stock decrement',
                'created_by' => 1, // System user
            ]);
            
            // Recalculate stock
            $product->recalculateStock();
            $affected = 1;

            if ($affected > 0) {
                Log::info('Inventory decremented', [
                    'product_id' => $productId,
                    'quantity' => $quantity,
                ]);

                return true;
            }

            throw new InsufficientStockException(
                $product->name,
                $quantity,
                $product->current_stock
            );
        });
    }

    /**
     * Increment stock
     *
     * @param int $productId
     * @param int $quantity
     * @return void
     */
    public function increment(int $productId, int $quantity): void
    {
        // Use stock movements instead of direct increment
        $product = Product::find($productId);
        
        if ($product) {
            \App\Models\StockMovement::create([
                'product_id' => $productId,
                'type' => \App\Models\StockMovement::TYPE_RESTOCK,
                'quantity' => $quantity,
                'notes' => 'Direct stock increment',
                'created_by' => 1, // System user
            ]);
            
            // Recalculate stock
            $product->recalculateStock();
        }

        Log::info('Inventory incremented', [
            'product_id' => $productId,
            'quantity' => $quantity,
        ]);
    }

    /**
     * Handle inventory side effects when an order status changes via admin.
     */
    public function handleOrderStatusChange(Order $order, string $previousStatus, string $newStatus): void
    {
        if ($previousStatus === $newStatus) {
            return;
        }

        if ($newStatus === Order::STATUS_CANCELLED) {
            if ($previousStatus === Order::STATUS_AWAITING_PAYMENT) {
                $this->releaseByOrderId($order->id);

                return;
            }

            if ($this->shouldRestockOnCancel($previousStatus)) {
                $this->restockOrder($order, 'cancelled');
            }

            return;
        }

        if (
            $newStatus === Order::STATUS_RETURNED
            && in_array($previousStatus, [Order::STATUS_SHIPPED, Order::STATUS_DELIVERED], true)
        ) {
            $this->restockOrder($order, 'returned');
        }
    }

    /**
     * Whether sold quantities for this order still need to be restocked (idempotency check).
     */
    public function orderHasPendingRestock(Order $order): bool
    {
        $order->loadMissing('items');

        foreach ($order->items as $item) {
            if (!$item->product_id) {
                continue;
            }

            $soldByTable = $this->getSoldQuantitiesByTable($item->product_id, $order->id);
            $restockedByTable = $this->getRestockedQuantitiesByTable($item->product_id, $order->id);
            $qtyToRestock = max(0, $soldByTable['stock'] - $restockedByTable['stock'])
                + max(0, $soldByTable['variant'] - $restockedByTable['variant']);

            if ($qtyToRestock > 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Restock products for a cancelled or returned order (idempotent).
     */
    public function restockOrder(Order $order, string $reason): void
    {
        $order->loadMissing('items');
        $restockNote = "Order #{$order->id} {$reason}";
        $createdBy = auth()->id() ?? 1;

        foreach ($order->items as $item) {
            if (!$item->product_id) {
                continue;
            }

            $product = Product::lockForUpdate()->find($item->product_id);
            if (!$product) {
                continue;
            }

            $soldByTable = $this->getSoldQuantitiesByTable($product->id, $order->id);
            $restockedByTable = $this->getRestockedQuantitiesByTable($product->id, $order->id);
            $qtyToRestock = max(0, $soldByTable['stock'] - $restockedByTable['stock'])
                + max(0, $soldByTable['variant'] - $restockedByTable['variant']);

            if ($qtyToRestock <= 0) {
                continue;
            }

            if ($soldByTable['stock'] > $restockedByTable['stock']) {
                StockMovement::create([
                    'product_id' => $product->id,
                    'type' => StockMovement::TYPE_RESTOCK,
                    'quantity' => $soldByTable['stock'] - $restockedByTable['stock'],
                    'notes' => $restockNote,
                    'created_by' => $createdBy,
                ]);
            }

            if ($soldByTable['variant'] > $restockedByTable['variant']) {
                VariantStockMovement::create([
                    'product_id' => $product->id,
                    'type' => VariantStockMovement::TYPE_RESTOCK,
                    'quantity' => $soldByTable['variant'] - $restockedByTable['variant'],
                    'notes' => $restockNote,
                    'created_by' => $createdBy,
                ]);
            }

            $product->recalculateStock();

            Log::info('Order restock movement created', [
                'order_id' => $order->id,
                'product_id' => $product->id,
                'quantity' => $qtyToRestock,
                'reason' => $reason,
            ]);
        }
    }

    private function shouldRestockOnCancel(string $previousStatus): bool
    {
        return in_array($previousStatus, [
            Order::STATUS_PLACED,
            Order::STATUS_CONFIRMED,
            Order::STATUS_PACKED,
            Order::STATUS_AWAITING_PICKUP,
            Order::STATUS_SHIPPED,
            Order::STATUS_DELIVERED,
            Order::STATUS_RETURNED,
        ], true);
    }

    /**
     * @return list<string>
     */
    private function saleNotePatterns(int $orderId): array
    {
        return [
            "Order #{$orderId}",
            "POS Order #{$orderId}",
            "POS Order #{$orderId} (Synced)",
        ];
    }

    /**
     * @return array{stock: int, variant: int}
     */
    private function getSoldQuantitiesByTable(int $productId, int $orderId): array
    {
        $patterns = $this->saleNotePatterns($orderId);

        $stockSale = (int) StockMovement::where('product_id', $productId)
            ->where('type', StockMovement::TYPE_SALE)
            ->whereIn('notes', $patterns)
            ->sum('quantity');

        $variantSale = (int) VariantStockMovement::where('product_id', $productId)
            ->where('type', VariantStockMovement::TYPE_SALE)
            ->whereIn('notes', $patterns)
            ->sum('quantity');

        return [
            'stock' => abs($stockSale),
            'variant' => abs($variantSale),
        ];
    }

    /**
     * @return array{stock: int, variant: int}
     */
    private function getRestockedQuantitiesByTable(int $productId, int $orderId): array
    {
        $prefix = "Order #{$orderId}";

        return [
            'stock' => (int) StockMovement::where('product_id', $productId)
                ->where('type', StockMovement::TYPE_RESTOCK)
                ->where('notes', 'like', $prefix . '%')
                ->sum('quantity'),
            'variant' => (int) VariantStockMovement::where('product_id', $productId)
                ->where('type', VariantStockMovement::TYPE_RESTOCK)
                ->where('notes', 'like', $prefix . '%')
                ->sum('quantity'),
        ];
    }

    /**
     * Clean up expired reservations
     *
     * @return int Number of expired reservations cleaned up
     */
    public function cleanupExpiredReservations(): int
    {
        $expiredOrderIds = StockReservation::expired()
            ->whereNotNull('order_id')
            ->pluck('order_id')
            ->unique()
            ->filter()
            ->values()
            ->all();

        $count = StockReservation::expired()
            ->update(['status' => 'expired']);

        if ($count > 0) {
            Log::info('Expired stock reservations cleaned up', [
                'count' => $count,
            ]);
        }

        foreach ($expiredOrderIds as $orderId) {
            $this->cancelAwaitingPaymentOrder((int) $orderId, 'reservation_expired');
        }

        return $count;
    }

    /**
     * Cancel an awaiting payment order and mark payment as failed.
     */
    public function cancelAwaitingPaymentOrder(int $orderId, string $reason = 'system'): bool
    {
        $updated = DB::transaction(function () use ($orderId, $reason) {
            $order = Order::where('id', $orderId)
                ->where('status', Order::STATUS_AWAITING_PAYMENT)
                ->where('payment_method', 'online')
                ->whereIn('payment_status', [Order::PAYMENT_PENDING, Order::PAYMENT_FAILED])
                ->lockForUpdate()
                ->first();

            if (!$order) {
                return 0;
            }

            // Guard: do not cancel if gateway already captured or is in authorization pipeline.
            if (!empty($order->razorpay_payment_id)) {
                $gatewayState = app(RazorpayService::class)->resolvePaymentState($order->razorpay_payment_id);
                $status = $gatewayState['status'] ?? RazorpayService::PAYMENT_STATUS_UNKNOWN;

                if ($status === RazorpayService::PAYMENT_STATUS_CAPTURED) {
                    $order->update([
                        'payment_status' => Order::PAYMENT_PAID,
                        'status' => Order::STATUS_PLACED,
                        'placed_at' => $order->placed_at ?? now(),
                    ]);

                    Log::warning('Awaiting payment auto-cancel skipped: gateway already captured', [
                        'order_id' => $orderId,
                        'reason' => $reason,
                    ]);

                    return 0;
                }

                if ($status === RazorpayService::PAYMENT_STATUS_AUTHORIZED) {
                    Log::info('Awaiting payment auto-cancel skipped: gateway authorization in progress', [
                        'order_id' => $orderId,
                        'reason' => $reason,
                    ]);

                    return 0;
                }
            }

            return Order::whereKey($order->id)->update([
                'status' => Order::STATUS_CANCELLED,
                'payment_status' => Order::PAYMENT_FAILED,
            ]);
        });

        if ($updated > 0) {
            Log::info('Awaiting payment order auto-cancelled', [
                'order_id' => $orderId,
                'reason' => $reason,
            ]);

            return true;
        }

        return false;
    }
}
