<?php

namespace App\Services;

use App\Filament\Forms\Components\SalesOrderProductGrid;
use App\Filament\Resources\SalesOrderResource\Pages\CreateSalesOrder;
use App\Models\Catalog\Product;
use App\Models\Inventory\StockBatch;
use App\Models\Scopes\BranchScope;
use Illuminate\Support\Collection;

/**
 * The catalog payload the sales-order picker renders: one entry per active
 * product, branch-scoped, each carrying its sellable lots — plus the virtual
 * quotation lot for out-of-stock items (an order is a promise about the
 * future, not a shelf check).
 *
 * Shared by the Sales Order form's product grid
 * ({@see SalesOrderProductGrid}) and the
 * full-screen Create Sales Order terminal
 * ({@see CreateSalesOrder})
 * so the two surfaces can never disagree about a branch's price ranges, lot
 * lists or quote-only fallback.
 */
class SalesOrderCatalogService
{
    public function __construct(
        private readonly BatchService $batches,
        private readonly VatSettings $vat,
    ) {}

    /**
     * @return array<int, array<string, mixed>>
     */
    public function forBranch(?int $branchId): array
    {
        $vatEnabled = $this->vat->enabled();

        $products = Product::where('is_active', true)
            ->with('mainCategory')
            ->with('subCategory')
            ->with('baseUnit:id,code,name,is_fractional')
            ->withStockFor($branchId)
            ->orderBy('name')
            ->get();

        if ($products->isEmpty()) {
            return [];
        }

        $ids = $products->pluck('id')->map(fn ($id) => (int) $id)->all();

        $ranges = $branchId
            ? $this->batches->priceRangesFor($ids, $branchId)
            : [];

        $lotsByProduct = $branchId
            ? StockBatch::query()
                ->withoutGlobalScope(BranchScope::class)
                ->whereIn('product_id', $ids)
                ->where('branch_id', $branchId)
                ->active()
                ->whereNotNull('sale_price')
                ->get([
                    'id',
                    'product_id',
                    'internal_batch_number',
                    'sale_price',
                    'batch_cost',
                    'expiry_date',
                    'available_quantity',
                    'created_at',
                ])
                ->groupBy('product_id')
            : collect();

        return $products
            ->map(function (Product $p) use ($vatEnabled, $ranges, $lotsByProduct): array {
                $productLots = $lotsByProduct->get((int) $p->id) ?? collect();

                $range = $ranges[(int) $p->id] ?? null;
                $isBatchTracked = (bool) $p->is_batch_tracked;

                $priceMin = $range
                    ? (float) $range['price_min']
                    : ($productLots->min('sale_price') !== null
                        ? (float) $productLots->min('sale_price')
                        : null);

                $priceMax = $range
                    ? (float) $range['price_max']
                    : ($productLots->max('sale_price') !== null
                        ? (float) $productLots->max('sale_price')
                        : null);

                $lotCount = $range
                    ? (int) $range['lot_count']
                    : $productLots->count();

                /*
                 * Sales Orders may quote products that currently have no stock
                 * — an order is a promise about the future, not a shelf check.
                 * When nothing is available but the product was priced before,
                 * expose ONE virtual quotation lot instead of blocking the line:
                 * id = null, batch = null, available = 0, is_quote_only = true.
                 * The browser must never submit a real StockBatch id for this
                 * case — SalesOrderService::quoteLot() resolves a null
                 * batch_id itself.
                 */
                $orderedSellableLots = $this->orderedLots(
                    $productLots->filter(fn (StockBatch $lot): bool => (float) $lot->available_quantity > 0),
                    $isBatchTracked,
                    (bool) $p->is_expiry_tracked,
                );

                $lotsPayload = $orderedSellableLots
                    ->map(fn (StockBatch $lot): array => [
                        'id' => $lot->id,
                        'batch' => $lot->internal_batch_number,
                        'price' => (float) $lot->sale_price,
                        'cost' => $lot->batch_cost !== null
                            ? (float) $lot->batch_cost
                            : null,
                        'expiry' => $lot->expiry_date?->toDateString(),
                        'available' => (float) $lot->available_quantity,
                        'is_quote_only' => false,
                    ])
                    ->values()
                    ->all();

                if ($lotsPayload === [] && $productLots->isNotEmpty()) {
                    // "What did we last charge" — the same rule
                    // BatchService::unifiedBatchFor() uses for a non-tracked
                    // product's price of record.
                    $quoteLot = $productLots
                        ->sortByDesc(fn (StockBatch $lot) => $lot->created_at->getTimestamp())
                        ->first();

                    $lotsPayload = [[
                        'id' => null,
                        'batch' => null,
                        'price' => (float) $quoteLot->sale_price,
                        'cost' => $quoteLot->batch_cost !== null
                            ? (float) $quoteLot->batch_cost
                            : null,
                        'expiry' => null,
                        'available' => 0.0,
                        'is_quote_only' => true,
                    ]];
                }

                return [
                    'id' => $p->id,
                    'name' => $p->name,
                    'sku' => $p->sku,
                    // The manufacturer's own barcode, when one is on file — a
                    // scan is matched against this first, then falls back to
                    // sku. See handleScan() in the terminal/grid views.
                    'barcode' => $p->barcode,
                    // Batch tracking OFF: batches are an internal receiving
                    // detail — the "price" is a single figure (the front lot,
                    // whether sellable or quote-only), never a min–max range.
                    'price_min' => $isBatchTracked ? $priceMin : ($lotsPayload[0]['price'] ?? $priceMin),
                    'price_max' => $isBatchTracked ? $priceMax : ($lotsPayload[0]['price'] ?? $priceMax),
                    'lot_count' => $lotCount,
                    'is_batch_tracked' => $isBatchTracked,
                    'lots' => $lotsPayload,
                    // See Pos::productsPayload() — while VAT is off, the browser
                    // never even sees a rate.
                    'vat_rate' => $vatEnabled ? (float) $p->vat_rate : 0.0,
                    // See Pos::productsPayload() — the column is a path, not a URL.
                    'image_url' => $p->image_display_url,
                    'main_category' => $p->mainCategory?->name,
                    'sub_category' => $p->subCategory?->name,
                    'quantity' => (float) ($p->stock ?? 0),
                    'base_unit' => $p->baseUnit
                        ? [
                            'code' => $p->baseUnit->code,
                            'name' => $p->baseUnit->name,
                            'is_fractional' => $p->baseUnit->is_fractional,
                        ]
                        : null,
                ];
            })
            ->values()
            ->all();
    }

    /**
     * A product's lots in its sell order.
     *
     * Batch tracking ON: FEFO when the product is expiry-tracked (soonest
     * expiry first, legacy no-date lots last), FIFO (oldest receipt first)
     * otherwise — so the default lot matches the lot
     * SalesOrderService::quoteLot would quote with no explicit batch_id.
     *
     * Batch tracking OFF: newest receipt first, so `lots[0]` is the "latest
     * GRN price" lot the unified pool quotes from.
     *
     * @param  Collection<int, StockBatch>  $lots
     * @return Collection<int, StockBatch>
     */
    public function orderedLots(Collection $lots, bool $batchTracked, bool $expiryTracked): Collection
    {
        if (! $batchTracked) {
            return $lots->sortByDesc(fn (StockBatch $lot) => $lot->created_at->getTimestamp())->values();
        }

        if ($expiryTracked) {
            return $lots->sortBy([
                fn (StockBatch $lot) => $lot->expiry_date === null ? 1 : 0,
                fn (StockBatch $lot) => $lot->expiry_date?->getTimestamp() ?? PHP_INT_MAX,
                fn (StockBatch $lot) => $lot->created_at->getTimestamp(),
            ])->values();
        }

        return $lots->sortBy(fn (StockBatch $lot) => $lot->created_at->getTimestamp())->values();
    }
}
