<?php

namespace App\Services;

use App\Models\FranchiseStock;
use App\Models\FranchiseStockBatch;
use App\Models\FranchiseTransaction;
use App\Models\Franchise;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use RuntimeException;

class FranchiseStockService
{
    public function __construct(private readonly FranchiseNotificationService $notificationService)
    {
    }

    /**
     * @param  array<string, mixed>  $context
     */
    public function addStock(
        int $franchiseId,
        int $productId,
        float $quantity,
        float $unitPrice,
        float $unitBv,
        string $transactionType = 'stock_in',
        array $context = []
    ): FranchiseTransaction {
        if ($quantity <= 0) {
            throw new InvalidArgumentException('Quantity must be greater than zero.');
        }

        return DB::transaction(function () use ($franchiseId, $productId, $quantity, $unitPrice, $unitBv, $transactionType, $context) {
            $stock = FranchiseStock::query()->firstOrCreate(
                ['franchise_id' => $franchiseId, 'product_id' => $productId],
                ['quantity' => 0, 'reserved_quantity' => 0]
            );

            $opening = (float) $stock->quantity;
            $closing = $opening + $quantity;
            $stock->quantity = $closing;
            $stock->save();

            $batchId = $context['batch_id'] ?? null;
            if (! $batchId && ! empty($context['batch_code'])) {
                $batch = FranchiseStockBatch::query()->firstOrCreate(
                    [
                        'franchise_id' => $franchiseId,
                        'product_id' => $productId,
                        'batch_code' => (string) $context['batch_code'],
                    ],
                    [
                        'expiry_date' => $context['expiry_date'] ?? null,
                        'quantity' => 0,
                        'received_at' => now(),
                    ]
                );
                if (! empty($context['expiry_date'])) {
                    $batch->expiry_date = $context['expiry_date'];
                }
                $batch->quantity = (float) $batch->quantity + $quantity;
                $batch->received_at = $batch->received_at ?? now();
                $batch->save();
                $batchId = $batch->id;
            }

            $transaction = FranchiseTransaction::query()->create([
                'franchise_id' => $franchiseId,
                'user_id' => $context['user_id'] ?? null,
                'order_id' => $context['order_id'] ?? null,
                'product_id' => $productId,
                'batch_id' => $batchId,
                'transaction_type' => $transactionType,
                'amount' => round($unitPrice * $quantity, 2),
                'bv' => round($unitBv * $quantity, 4),
                'quantity' => $quantity,
                'opening_qty' => $opening,
                'closing_qty' => $closing,
                'unit_price' => $unitPrice,
                'unit_bv' => $unitBv,
                'reference' => $context['reference'] ?? null,
                'narration' => $context['narration'] ?? null,
                'transacted_at' => now(),
                'created_by' => $context['created_by'] ?? null,
            ]);

            $this->checkLowStock($franchiseId, $productId, $closing);

            return $transaction;
        });
    }

    /**
     * @param  array<string, mixed>  $context
     */
    public function removeStock(
        int $franchiseId,
        int $productId,
        float $quantity,
        float $unitPrice,
        float $unitBv,
        string $transactionType = 'stock_out',
        array $context = []
    ): FranchiseTransaction {
        if ($quantity <= 0) {
            throw new InvalidArgumentException('Quantity must be greater than zero.');
        }

        return DB::transaction(function () use ($franchiseId, $productId, $quantity, $unitPrice, $unitBv, $transactionType, $context) {
            $stock = FranchiseStock::query()
                ->where('franchise_id', $franchiseId)
                ->where('product_id', $productId)
                ->lockForUpdate()
                ->first();

            if (! $stock) {
                throw new RuntimeException('Stock not found for this product.');
            }

            $available = (float) $stock->quantity - (float) $stock->reserved_quantity;
            if ($available < $quantity) {
                throw new RuntimeException('Insufficient stock available.');
            }

            $opening = (float) $stock->quantity;
            $closing = $opening - $quantity;
            $stock->quantity = $closing;
            $stock->save();

            $batchId = $context['batch_id'] ?? null;
            if ($batchId) {
                $batch = FranchiseStockBatch::query()->whereKey($batchId)->lockForUpdate()->first();
                if ($batch) {
                    $batch->quantity = max(0, (float) $batch->quantity - $quantity);
                    $batch->save();
                }
            }

            $transaction = FranchiseTransaction::query()->create([
                'franchise_id' => $franchiseId,
                'user_id' => $context['user_id'] ?? null,
                'order_id' => $context['order_id'] ?? null,
                'product_id' => $productId,
                'batch_id' => $batchId,
                'transaction_type' => $transactionType,
                'amount' => round($unitPrice * $quantity, 2),
                'bv' => round($unitBv * $quantity, 4),
                'quantity' => $quantity,
                'opening_qty' => $opening,
                'closing_qty' => $closing,
                'unit_price' => $unitPrice,
                'unit_bv' => $unitBv,
                'reference' => $context['reference'] ?? null,
                'narration' => $context['narration'] ?? null,
                'transacted_at' => now(),
                'created_by' => $context['created_by'] ?? null,
            ]);

            $this->checkLowStock($franchiseId, $productId, $closing);

            return $transaction;
        });
    }

    /**
     * @param  array<string, mixed>  $context
     */
    public function recordDamage(
        int $franchiseId,
        int $productId,
        float $quantity,
        float $unitPrice,
        float $unitBv,
        array $context = []
    ): FranchiseTransaction {
        return $this->removeStock($franchiseId, $productId, $quantity, $unitPrice, $unitBv, 'damage', $context);
    }

    /**
     * @param  array<string, mixed>  $context
     */
    public function transferStock(
        int $fromFranchiseId,
        int $toFranchiseId,
        int $productId,
        float $quantity,
        float $unitPrice,
        float $unitBv,
        array $context = []
    ): void {
        DB::transaction(function () use ($fromFranchiseId, $toFranchiseId, $productId, $quantity, $unitPrice, $unitBv, $context): void {
            $this->removeStock($fromFranchiseId, $productId, $quantity, $unitPrice, $unitBv, 'transfer_out', $context);
            $this->addStock($toFranchiseId, $productId, $quantity, $unitPrice, $unitBv, 'transfer_in', $context);
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $items
     */
    public function receiveStockRequest(int $franchiseId, array $items, array $context = []): void
    {
        foreach ($items as $item) {
            $this->addStock(
                $franchiseId,
                (int) $item['product_id'],
                (float) $item['quantity'],
                (float) $item['unit_price'],
                (float) $item['unit_bv'],
                'stock_in',
                $context
            );
        }
    }

    public function reserveForSale(int $franchiseId, int $productId, float $quantity): void
    {
        if ($quantity <= 0) {
            return;
        }

        DB::transaction(function () use ($franchiseId, $productId, $quantity): void {
            $stock = FranchiseStock::query()
                ->where('franchise_id', $franchiseId)
                ->where('product_id', $productId)
                ->lockForUpdate()
                ->first();

            if (! $stock) {
                throw new RuntimeException('Stock not found for reservation.');
            }

            $available = (float) $stock->quantity - (float) $stock->reserved_quantity;
            if ($available < $quantity) {
                throw new RuntimeException('Insufficient stock to reserve.');
            }

            $stock->reserved_quantity = (float) $stock->reserved_quantity + $quantity;
            $stock->save();
        });
    }

    private function checkLowStock(int $franchiseId, int $productId, float $closingQty): void
    {
        $threshold = (float) setting('franchise_low_stock_threshold', 5);
        if ($closingQty > $threshold) {
            return;
        }

        $franchise = Franchise::query()->find($franchiseId);
        if (! $franchise) {
            return;
        }

        $this->notificationService->notify(
            $franchise,
            'Low stock alert',
            "Product #{$productId} stock is low (".number_format($closingQty, 2).').',
            route('franchise.stock')
        );
    }
}

