<?php

namespace App\Services;

use App\Exceptions\InsufficientStockException;
use App\Exceptions\StockException;
use App\Models\InventoryBalance;
use App\Models\InventoryReservation;
use App\Models\Location;
use App\Models\Product;
use App\Models\Setting;
use App\Models\StockLedger;
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\Facades\Schema;
use Illuminate\Support\Str;

/**
 * Location-aware inventory with dual-write to legacy product stock tables.
 */
class LocationInventoryService
{
    use LocationInventoryModeConcern;

    protected int $defaultReservationTtl = 900;

    public function defaultLocationId(): int
    {
        $store = Location::defaultStore();
        if (!$store) {
            app(LocationService::class)->ensureCompanyAndDefaults();
            $store = Location::defaultStore();
        }

        if (!$store) {
            throw new StockException('Default store location is not configured.');
        }

        return (int) $store->id;
    }

    public function getOrCreateBalance(int $locationId, int $productId, bool $lock = false): InventoryBalance
    {
        $query = InventoryBalance::query()->where('location_id', $locationId)->where('product_id', $productId);
        if ($lock) {
            $balance = $query->lockForUpdate()->first();
        } else {
            $balance = $query->first();
        }

        if ($balance) {
            return $balance;
        }

        return InventoryBalance::create([
            'location_id' => $locationId,
            'product_id' => $productId,
            'on_hand' => 0,
            'reserved' => 0,
            'damaged' => 0,
            'in_transit_in' => 0,
            'in_transit_out' => 0,
        ]);
    }

    public function getAvailableStock(int $productId, ?int $locationId = null): int
    {
        if (!$this->useLocationInventory()) {
            return app(InventoryService::class)->getAvailableStock($productId);
        }

        if ($locationId) {
            $balance = InventoryBalance::query()
                ->where('location_id', $locationId)
                ->where('product_id', $productId)
                ->first();

            return $balance ? $balance->available() : 0;
        }

        return (int) InventoryBalance::query()
            ->where('product_id', $productId)
            ->whereHas('location', fn ($q) => $q->fulfillsOnline()) // all active stores with Fulfills Online
            ->selectRaw('COALESCE(SUM(GREATEST(on_hand - reserved - damaged, 0)), 0) as available_total')
            ->value('available_total');
    }

    public function getOnHand(int $productId, int $locationId): int
    {
        return (int) (InventoryBalance::query()
            ->where('location_id', $locationId)
            ->where('product_id', $productId)
            ->value('on_hand') ?? 0);
    }

    /**
     * Apply a ledger movement and update balance. Dual-writes legacy movements when $dualWriteLegacy is true.
     */
    public function applyMovement(
        int $locationId,
        int $productId,
        string $type,
        int $quantity,
        ?string $notes = null,
        ?string $referenceType = null,
        ?int $referenceId = null,
        ?int $createdBy = null,
        bool $dualWriteLegacy = true,
        bool $affectsOnHand = true,
        bool $affectsReserved = false,
        int $reservedDelta = 0,
        bool $affectsInTransitOut = false,
        int $inTransitOutDelta = 0,
        bool $affectsInTransitIn = false,
        int $inTransitInDelta = 0,
        bool $affectsDamaged = false,
        int $damagedDelta = 0,
    ): StockLedger {
        return DB::transaction(function () use (
            $locationId,
            $productId,
            $type,
            $quantity,
            $notes,
            $referenceType,
            $referenceId,
            $createdBy,
            $dualWriteLegacy,
            $affectsOnHand,
            $affectsReserved,
            $reservedDelta,
            $affectsInTransitOut,
            $inTransitOutDelta,
            $affectsInTransitIn,
            $inTransitInDelta,
            $affectsDamaged,
            $damagedDelta,
        ) {
            $product = Product::lockForUpdate()->find($productId);
            if (!$product) {
                throw new StockException("Product not found: {$productId}");
            }

            $balance = $this->getOrCreateBalance($locationId, $productId, true);
            $balance = InventoryBalance::query()->whereKey($balance->id)->lockForUpdate()->firstOrFail();

            if ($affectsOnHand) {
                $newOnHand = $balance->on_hand + $quantity;
                if ($newOnHand < 0) {
                    throw new InsufficientStockException($product->name, abs($quantity), $balance->available());
                }
                $balance->on_hand = $newOnHand;
            }

            if ($affectsReserved) {
                $balance->reserved = max(0, $balance->reserved + $reservedDelta);
            }
            if ($affectsInTransitOut) {
                $balance->in_transit_out = max(0, $balance->in_transit_out + $inTransitOutDelta);
            }
            if ($affectsInTransitIn) {
                $balance->in_transit_in = max(0, $balance->in_transit_in + $inTransitInDelta);
            }
            if ($affectsDamaged) {
                $balance->damaged = max(0, $balance->damaged + $damagedDelta);
            }

            $balance->save();

            $ledger = StockLedger::create([
                'location_id' => $locationId,
                'product_id' => $productId,
                'type' => $type,
                'quantity' => $quantity,
                'balance_after' => $balance->on_hand,
                'reference_type' => $referenceType,
                'reference_id' => $referenceId,
                'notes' => $notes,
                'created_by' => $this->requireCreatedBy($createdBy),
            ]);

            if ($dualWriteLegacy && $affectsOnHand && $quantity !== 0) {
                $this->dualWriteLegacyMovement($product, $type, $quantity, $notes, $createdBy);
            }

            // Keep company aggregate in sync for storefront transitional reads
            if ($affectsOnHand) {
                $aggregate = (int) InventoryBalance::query()->where('product_id', $productId)->sum('on_hand');
                $product->current_stock = $aggregate;
                $product->save();
            }

            return $ledger;
        });
    }

    protected function dualWriteLegacyMovement(Product $product, string $type, int $quantity, ?string $notes, ?int $createdBy): void
    {
        $legacyType = match ($type) {
            StockLedger::TYPE_SALE => 'sale',
            StockLedger::TYPE_RESTOCK, StockLedger::TYPE_PURCHASE_RECEIPT, StockLedger::TYPE_TRANSFER_IN, StockLedger::TYPE_RETURN => 'restock',
            StockLedger::TYPE_OPENING => 'opening_stock',
            StockLedger::TYPE_ADJUSTMENT, StockLedger::TYPE_DAMAGE, StockLedger::TYPE_CYCLE_COUNT, StockLedger::TYPE_TRANSFER_OUT => 'adjustment',
            default => null,
        };

        if ($legacyType === null) {
            return;
        }

        $payload = [
            'product_id' => $product->id,
            'type' => $legacyType,
            'quantity' => $quantity,
            'notes' => $notes,
            'created_by' => $this->requireCreatedBy($createdBy),
        ];

        if ($product->isVariant()) {
            VariantStockMovement::create($payload);
        } else {
            StockMovement::create($payload);
        }
    }

    public function reserve(int $productId, int $quantity, int $locationId, ?int $ttl = null, ?int $orderId = null): string
    {
        if (!$this->useLocationInventory()) {
            return app(InventoryService::class)->reserve($productId, $quantity, $ttl, $orderId);
        }

        if ($ttl === null) {
            $configured = Setting::get('stock_reservation_ttl_seconds');
            $ttl = $configured !== '' && $configured !== null ? (int) $configured : $this->defaultReservationTtl;
            $ttl = max(60, min(3600, $ttl));
        }

        return DB::transaction(function () use ($productId, $quantity, $locationId, $ttl, $orderId) {
            $product = Product::lockForUpdate()->find($productId);
            if (!$product) {
                throw new StockException("Product not found: {$productId}");
            }

            $balance = $this->getOrCreateBalance($locationId, $productId, true);
            $balance = InventoryBalance::query()->whereKey($balance->id)->lockForUpdate()->firstOrFail();

            if ($balance->available() < $quantity) {
                throw new InsufficientStockException($product->name, $quantity, $balance->available());
            }

            $balance->reserved += $quantity;
            $balance->save();

            $token = Str::random(64);

            InventoryReservation::create([
                'location_id' => $locationId,
                'product_id' => $productId,
                'order_id' => $orderId,
                'quantity' => $quantity,
                'reservation_token' => $token,
                'expires_at' => now()->addSeconds($ttl),
                'status' => 'pending',
            ]);

            // Dual-write legacy reservation for transitional cleanup jobs
            if (Schema::hasTable('stock_reservations')) {
                StockReservation::create([
                    'product_id' => $productId,
                    'order_id' => $orderId,
                    'quantity' => $quantity,
                    'reservation_token' => $token,
                    'expires_at' => now()->addSeconds($ttl),
                    'status' => 'pending',
                ]);
            }

            StockLedger::create([
                'location_id' => $locationId,
                'product_id' => $productId,
                'type' => StockLedger::TYPE_SALE_RESERVE,
                'quantity' => 0,
                'balance_after' => $balance->on_hand,
                'reference_type' => 'order',
                'reference_id' => $orderId,
                'notes' => "Reserved {$quantity}",
                'created_by' => $this->requireCreatedBy(),
            ]);

            return $token;
        });
    }

    public function confirmReservation(string $reservationToken): bool
    {
        if (!$this->useLocationInventory()) {
            return app(InventoryService::class)->confirmReservation($reservationToken);
        }

        return DB::transaction(function () use ($reservationToken) {
            $reservation = InventoryReservation::query()
                ->where('reservation_token', $reservationToken)
                ->where('status', 'pending')
                ->lockForUpdate()
                ->first();

            if (!$reservation) {
                // Fallback to legacy path during transition
                return app(InventoryService::class)->confirmReservation($reservationToken);
            }

            if ($reservation->isExpired()) {
                $this->releaseReservationModel($reservation);
                $reservation->update(['status' => 'expired']);
                throw new StockException("Reservation expired: {$reservationToken}");
            }

            $notes = $reservation->order_id
                ? "Order #{$reservation->order_id}"
                : "Reservation confirmed: {$reservationToken}";

            $this->applyMovement(
                locationId: $reservation->location_id,
                productId: $reservation->product_id,
                type: StockLedger::TYPE_SALE,
                quantity: -$reservation->quantity,
                notes: $notes,
                referenceType: 'order',
                referenceId: $reservation->order_id,
                dualWriteLegacy: true,
                affectsOnHand: true,
                affectsReserved: true,
                reservedDelta: -$reservation->quantity,
            );

            $reservation->update(['status' => 'confirmed']);

            if (Schema::hasTable('stock_reservations')) {
                StockReservation::where('reservation_token', $reservationToken)
                    ->where('status', 'pending')
                    ->update(['status' => 'confirmed']);
            }

            return true;
        });
    }

    public function release(string $reservationToken): bool
    {
        $reservation = InventoryReservation::query()
            ->where('reservation_token', $reservationToken)
            ->where('status', 'pending')
            ->first();

        if ($reservation) {
            return DB::transaction(function () use ($reservation) {
                $this->releaseReservationModel($reservation);
                $reservation->update(['status' => 'released']);

                if (Schema::hasTable('stock_reservations')) {
                    StockReservation::where('reservation_token', $reservation->reservation_token)
                        ->where('status', 'pending')
                        ->update(['status' => 'released']);
                }

                return true;
            });
        }

        return app(InventoryService::class)->release($reservationToken);
    }

    public function releaseByOrderId(int $orderId): int
    {
        if (!$this->useLocationInventory()) {
            return app(InventoryService::class)->releaseByOrderId($orderId);
        }

        $reservations = InventoryReservation::query()
            ->where('order_id', $orderId)
            ->where('status', 'pending')
            ->get();

        $count = 0;
        foreach ($reservations as $reservation) {
            $this->release($reservation->reservation_token);
            $count++;
        }

        if (Schema::hasTable('stock_reservations')) {
            StockReservation::where('order_id', $orderId)->where('status', 'pending')->update(['status' => 'released']);
        }

        return $count;
    }

    protected function releaseReservationModel(InventoryReservation $reservation): void
    {
        $balance = $this->getOrCreateBalance($reservation->location_id, $reservation->product_id, true);
        $balance = InventoryBalance::query()->whereKey($balance->id)->lockForUpdate()->firstOrFail();
        $balance->reserved = max(0, $balance->reserved - $reservation->quantity);
        $balance->save();

        StockLedger::create([
            'location_id' => $reservation->location_id,
            'product_id' => $reservation->product_id,
            'type' => StockLedger::TYPE_SALE_RELEASE,
            'quantity' => 0,
            'balance_after' => $balance->on_hand,
            'reference_type' => 'order',
            'reference_id' => $reservation->order_id,
            'notes' => "Released reservation {$reservation->reservation_token}",
            'created_by' => $this->requireCreatedBy(),
        ]);
    }

    public function decrementForSale(int $productId, int $quantity, int $locationId, string $notes, ?int $createdBy = null): void
    {
        // Respect reservations at this location (POS should not oversell reserved qty)
        $balance = $this->getOrCreateBalance($locationId, $productId, true);
        if ($balance->available() < $quantity) {
            $product = Product::find($productId);
            throw new InsufficientStockException($product?->name ?? 'Product', $quantity, $balance->available());
        }

        $this->applyMovement(
            locationId: $locationId,
            productId: $productId,
            type: StockLedger::TYPE_SALE,
            quantity: -$quantity,
            notes: $notes,
            referenceType: 'sale',
            createdBy: $createdBy,
        );
    }

    public function restock(int $productId, int $quantity, int $locationId, ?string $notes = null, ?int $createdBy = null): void
    {
        $this->applyMovement(
            locationId: $locationId,
            productId: $productId,
            type: StockLedger::TYPE_RESTOCK,
            quantity: $quantity,
            notes: $notes,
            referenceType: 'restock',
            createdBy: $createdBy,
        );
    }

    public function adjust(int $productId, int $delta, int $locationId, ?string $notes = null, ?int $createdBy = null): void
    {
        $this->applyMovement(
            locationId: $locationId,
            productId: $productId,
            type: StockLedger::TYPE_ADJUSTMENT,
            quantity: $delta,
            notes: $notes,
            referenceType: 'adjustment',
            createdBy: $createdBy,
        );
    }

    public function cleanupExpiredReservations(): int
    {
        if (!$this->useLocationInventory()) {
            return app(InventoryService::class)->cleanupExpiredReservations();
        }

        $expired = InventoryReservation::expired()->get();
        $count = 0;
        foreach ($expired as $reservation) {
            DB::transaction(function () use ($reservation, &$count) {
                $locked = InventoryReservation::query()->whereKey($reservation->id)->lockForUpdate()->first();
                if (!$locked || $locked->status !== 'pending') {
                    return;
                }
                $this->releaseReservationModel($locked);
                $locked->update(['status' => 'expired']);
                $count++;
            });
        }

        // Also run legacy cleanup
        app(InventoryService::class)->cleanupExpiredReservations();

        return $count;
    }

    /**
     * Backfill balances from products.current_stock.
     * Only seeds products that have NO inventory_balances anywhere (avoids double-counting).
     * Defaults to Central Warehouse (opening-stock home); stores receive stock via transfer.
     */
    public function backfillFromProductStock(?int $locationId = null): int
    {
        if (!$this->useLocationInventory()) {
            return 0;
        }

        $locationId = $locationId
            ?? (Location::centralWarehouse()?->id)
            ?? $this->defaultLocationId();
        $count = 0;

        Product::query()
            ->whereDoesntHave('inventoryBalances')
            ->orderBy('id')
            ->chunkById(200, function ($products) use ($locationId, &$count) {
                foreach ($products as $product) {
                    // Skip variant parents (non-sellable stock holders)
                    if ($product->product_type === 'variant' && is_null($product->parent_id)) {
                        continue;
                    }

                    $qty = (int) $product->current_stock;
                    if ($qty <= 0) {
                        continue;
                    }

                    $balance = $this->getOrCreateBalance($locationId, $product->id);

                    DB::transaction(function () use ($locationId, $product, $qty, $balance, &$count) {
                        $locked = InventoryBalance::query()->whereKey($balance->id)->lockForUpdate()->first();
                        if (!$locked) {
                            return;
                        }

                        // Only seed opening if no ledger exists yet for this product/location
                        $hasLedger = StockLedger::where('location_id', $locationId)->where('product_id', $product->id)->exists();
                        if ($hasLedger || $locked->on_hand > 0) {
                            return;
                        }

                        $locked->on_hand = max(0, $qty);
                        $locked->save();

                        StockLedger::create([
                            'location_id' => $locationId,
                            'product_id' => $product->id,
                            'type' => StockLedger::TYPE_OPENING,
                            'quantity' => $locked->on_hand,
                            'balance_after' => $locked->on_hand,
                            'reference_type' => 'backfill',
                            'notes' => 'Backfill from products.current_stock',
                            'created_by' => $this->resolveCreatedBy() ?? 1, // CLI/backfill may lack auth; prefer real user when present
                        ]);
                        $count++;
                    });
                }
            });

        Log::info('Inventory backfill completed', ['location_id' => $locationId, 'rows' => $count]);

        return $count;
    }
}
