<?php

namespace App\Services;

use App\Models\StockLedger;
use App\Models\WarehouseStock;
use App\Models\Product;
use Illuminate\Support\Facades\Auth;

/**
 * InventoryService
 *
 * Central, atomic inventory mutation engine.
 * RULE: Stock can ONLY change through this service.
 * Never directly update product qty or warehouse_stocks from controllers.
 *
 * All public methods MUST be called inside DB::transaction().
 */
class InventoryService
{
    protected InventoryCostService $costService;

    public function __construct(InventoryCostService $costService)
    {
        $this->costService = $costService;
    }

    /**
     * Post a stock-IN movement (opening stock, purchase, return, adjustment+, transfer-in, van-return).
     */
    public function stockIn(array $data): StockLedger
    {
        $product     = Product::findOrFail($data['product_id']);
        $productId   = $product->id;
        $warehouseId = $data['warehouse_id'];
        $baseQtyIn   = bcadd((string) ($data['base_qty_in'] ?? $data['qty_in'] ?? 0), '0', 4);
        $unitCost    = bcadd((string) ($data['unit_cost'] ?? 0), '0', 4);

        // Calculate new weighted average cost
        $newAvgCost = $this->costService->calculateWeightedAvgCost(
            $productId, $warehouseId, $baseQtyIn, $unitCost
        );

        // Get running quantity BEFORE this entry
        $runningQty = $this->costService->getRunningQty($productId, $warehouseId);
        $newRunningQty = bcadd($runningQty, $baseQtyIn, 4);

        // Update warehouse stock summary
        $this->updateOrCreateWarehouseStock($productId, $warehouseId, $baseQtyIn, $newAvgCost);

        // Update product avg_cost
        if ($product->track_stock) {
            $product->avg_cost = $newAvgCost;
            $product->save();
        }

        // Post to stock ledger
        return StockLedger::create([
            'transaction_date'   => $data['transaction_date'] ?? now()->toDateString(),
            'transaction_type'   => $data['transaction_type'],
            'reference_type'     => $data['reference_type'] ?? null,
            'reference_id'       => $data['reference_id'] ?? null,
            'reference_number'   => $data['reference_number'] ?? null,
            'product_id'         => $productId,
            'warehouse_id'       => $warehouseId,
            'unit_id'            => $data['unit_id'] ?? null,
            'qty_in'             => $data['qty_in'] ?? $baseQtyIn,
            'qty_out'            => 0,
            'base_qty_in'        => $baseQtyIn,
            'base_qty_out'       => 0,
            'unit_cost'          => $unitCost,
            'total_cost'         => bcmul($baseQtyIn, $unitCost, 4),
            'running_quantity'   => $newRunningQty,
            'batch_id'           => $data['batch_id'] ?? null,
            'expiry'             => $data['expiry'] ?? null,
            'branch_id'          => $data['branch_id'] ?? null,
            'financial_year_id'  => $data['financial_year_id'] ?? null,
            'created_by'         => $data['created_by'] ?? Auth::id(),
        ]);
    }

    /**
     * Post a stock-OUT movement (sale, adjustment-, transfer-out, van-load, damage).
     */
    public function stockOut(array $data): StockLedger
    {
        $product     = Product::findOrFail($data['product_id']);
        $productId   = $product->id;
        $warehouseId = $data['warehouse_id'];
        $baseQtyOut  = bcadd((string) ($data['base_qty_out'] ?? $data['qty_out'] ?? 0), '0', 4);

        $avgCost = $this->costService->getAvgCost($productId, $warehouseId);

        // Individual workflows can explicitly allow a negative balance. Sales use
        // this override so an out-of-stock item is still invoiced and displayed as
        // negative stock, while transfers and other stock-out movements retain the
        // product-level protection by default.
        $allowNegative = (bool) ($data['allow_negative'] ?? $product->allow_negative_stock);
        if (!$allowNegative) {
            $currentQty = $this->costService->getRunningQty($productId, $warehouseId);
            if (bccomp($currentQty, $baseQtyOut, 4) === -1) {
                throw new \RuntimeException(
                    "Insufficient stock in warehouse. Available: {$currentQty}, Requested: {$baseQtyOut}"
                );
            }
        }

        $runningQty = $this->costService->getRunningQty($productId, $warehouseId);
        $newRunningQty = bcsub($runningQty, $baseQtyOut, 4);

        // Update warehouse stock
        $this->costService->decreaseWarehouseStock($productId, $warehouseId, $baseQtyOut);

        return StockLedger::create([
            'transaction_date'   => $data['transaction_date'] ?? now()->toDateString(),
            'transaction_type'   => $data['transaction_type'],
            'reference_type'     => $data['reference_type'] ?? null,
            'reference_id'       => $data['reference_id'] ?? null,
            'reference_number'   => $data['reference_number'] ?? null,
            'product_id'         => $productId,
            'warehouse_id'       => $warehouseId,
            'unit_id'            => $data['unit_id'] ?? null,
            'qty_in'             => 0,
            'qty_out'            => $data['qty_out'] ?? $baseQtyOut,
            'base_qty_in'        => 0,
            'base_qty_out'       => $baseQtyOut,
            'unit_cost'          => $data['unit_cost'] ?? $avgCost,
            'total_cost'         => bcmul($baseQtyOut, (string) ($data['unit_cost'] ?? $avgCost), 4),
            'running_quantity'   => $newRunningQty,
            'batch_id'           => $data['batch_id'] ?? null,
            'expiry'             => $data['expiry'] ?? null,
            'branch_id'          => $data['branch_id'] ?? null,
            'financial_year_id'  => $data['financial_year_id'] ?? null,
            'created_by'         => $data['created_by'] ?? Auth::id(),
        ]);
    }

    /**
     * Get current base qty for a product in a warehouse.
     */
    public function getCurrentQty(int $productId, int $warehouseId): string
    {
        return $this->costService->getRunningQty($productId, $warehouseId);
    }

    /**
     * Expose cost service for external use (e.g. StockTransferService).
     */
    public function inventoryCostService(): InventoryCostService
    {
        return $this->costService;
    }

    /**
     * Update or create warehouse stock record (stock-in direction only).
     */
    protected function updateOrCreateWarehouseStock(
        int $productId,
        int $warehouseId,
        string|int|float $qtyIn,
        string|int|float $newAvgCost
    ): void {
        $stock = WarehouseStock::where('product_id', $productId)
            ->where('warehouse_id', $warehouseId)
            ->lockForUpdate()
            ->first();

        if ($stock) {
            $stock->qty      = bcadd((string) $stock->qty, (string) $qtyIn, 4);
            $stock->avg_cost = $newAvgCost;
            $stock->save();
        } else {
            WarehouseStock::create([
                'product_id'   => $productId,
                'warehouse_id' => $warehouseId,
                'qty'          => $qtyIn,
                'avg_cost'     => $newAvgCost,
            ]);
        }
    }
}
