<?php

namespace Database\Seeders\Demo;

use App\Models\Branch;
use App\Models\Catalog\Product;
use App\Models\Purchasing\Supplier;
use App\Services\GrnService;
use App\Services\PurchaseOrderService;
use Database\Seeders\Demo\Concerns\GeneratesDemoData;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Auth;

/**
 * Stocks every branch (via direct "opening stock" GRNs), then layers on
 * purchase-order-driven receiving and ad-hoc restocks spread across the last
 * ~6 months. This is what makes SalesSeeder able to sell without running dry.
 */
class PurchasingSeeder extends Seeder
{
    use GeneratesDemoData;

    private const OPENING_CHUNK_SIZE = 40;

    private const PO_COUNT = 45;

    private const RESTOCK_GRN_COUNT = 40;

    private const DRAFT_GRN_COUNT = 5;

    private GrnService $grns;

    private PurchaseOrderService $purchaseOrders;

    public function run(): void
    {
        $this->grns = app(GrnService::class);
        $this->purchaseOrders = app(PurchaseOrderService::class);

        $branches = Branch::orderBy('id')->get();
        $suppliers = Supplier::where('is_blacklisted', false)->get();
        $products = Product::orderBy('id')->get();
        $adminId = (int) Auth::id();

        $this->openingStock($branches, $suppliers, $products);
        $this->purchaseOrderFlow($branches, $suppliers, $products, $adminId);
        $this->restocks($branches, $suppliers, $products, $adminId);
    }

    private function openingStock(Collection $branches, Collection $suppliers, Collection $products): void
    {
        foreach ($branches as $branch) {
            foreach ($products->chunk(self::OPENING_CHUNK_SIZE) as $chunkIndex => $chunk) {
                $supplier = $suppliers->random();
                $date = now()->subMonths(6)->addDays($chunkIndex * 2 + random_int(0, 2));

                $lines = [];
                foreach ($chunk as $product) {
                    $qty = $this->openingQuantity($product);
                    $lines[] = $this->buildGrnLine($product, $qty, strtoupper(substr($branch->city ?? $branch->name, 0, 2)));
                }

                $grn = $this->grns->create(
                    header: ['branch_id' => $branch->id, 'supplier_id' => $supplier->id, 'reference' => 'Opening stock'],
                    lines: $lines,
                );
                $this->grns->confirm($grn, (int) Auth::id());
                $this->backdate($grn, $date, cascadeStockMovements: true);
            }
        }
    }

    private function purchaseOrderFlow(Collection $branches, Collection $suppliers, Collection $products, int $adminId): void
    {
        for ($i = 1; $i <= self::PO_COUNT; $i++) {
            $branch = $branches->random();
            $supplier = $suppliers->random();
            $lineProducts = $products->random(random_int(3, 8));
            $poDate = $this->recentDate(5);

            $lines = $lineProducts->map(fn (Product $p) => [
                'product_id' => $p->id,
                'quantity' => $this->restockQuantity($p),
                'unit_cost' => $this->purchasePriceFor($p),
            ])->values()->all();

            $po = $this->purchaseOrders->create(
                header: ['supplier_id' => $supplier->id, 'branch_id' => $branch->id, 'expected_date' => $poDate->copy()->addDays(7)->toDateString()],
                lines: $lines,
            );
            $this->backdate($po, $poDate);

            $roll = $i % 20;
            if ($roll < 2) {
                // stays draft
                continue;
            }

            $this->purchaseOrders->send($po);

            if ($roll < 5) {
                // stays sent, awaiting delivery
                continue;
            }

            if ($roll === 5) {
                $this->purchaseOrders->cancel($po, 'Supplier could not fulfil the order in time.');

                continue;
            }

            $po->load('items.product');
            $isPartial = $roll < 12;

            $grnLines = [];
            foreach ($po->items as $item) {
                $receiveQty = $isPartial
                    ? round((float) $item->ordered_quantity * fake()->randomFloat(2, 0.4, 0.75), 2)
                    : (float) $item->ordered_quantity;

                if (in_array($item->product_id, CatalogSeeder::$serializedIds, true)) {
                    $receiveQty = max(1.0, round($receiveQty));
                }

                $grnLines[] = $this->buildGrnLine($item->product, $receiveQty, 'PO', (float) $item->unit_cost);
            }

            $grn = $this->grns->create(
                header: ['branch_id' => $branch->id, 'supplier_id' => $supplier->id, 'purchase_order_id' => $po->id, 'reference' => $po->po_number],
                lines: $grnLines,
            );
            $this->grns->confirm($grn, $adminId);
            $grnDate = $poDate->copy()->addDays(random_int(2, 10));
            $this->backdate($grn, $grnDate, cascadeStockMovements: true);
        }
    }

    private function restocks(Collection $branches, Collection $suppliers, Collection $products, int $adminId): void
    {
        for ($i = 1; $i <= self::RESTOCK_GRN_COUNT; $i++) {
            $branch = $branches->random();
            $supplier = $suppliers->random();
            $lineProducts = $products->random(random_int(5, 15));
            $date = $this->recentDate(6);

            $lines = $lineProducts->map(fn (Product $p) => $this->buildGrnLine(
                $p,
                $this->restockQuantity($p),
                'RS',
                salePrice: round($this->salePriceFor($p) * fake()->randomFloat(2, 0.95, 1.08), 2),
            ))->values()->all();

            $grn = $this->grns->create(
                header: ['branch_id' => $branch->id, 'supplier_id' => $supplier->id, 'reference' => 'Restock'],
                lines: $lines,
            );

            if ($i <= self::DRAFT_GRN_COUNT) {
                // Leave unconfirmed on purpose — draft GRNs waiting for approval.
                $this->backdate($grn, $date);

                continue;
            }

            $this->grns->confirm($grn, $adminId);
            $this->backdate($grn, $date, cascadeStockMovements: true);
        }
    }

    /**
     * @return array{product_id:int,quantity:float,unit_cost:float,sale_price:float,expiry_date?:string,manufacture_date?:string,serial_numbers?:array<int,string>}
     */
    private function buildGrnLine(Product $product, float $quantity, string $tag, ?float $unitCost = null, ?float $salePrice = null): array
    {
        $line = [
            'product_id' => $product->id,
            'quantity' => $quantity,
            'unit_cost' => $unitCost ?? $this->purchasePriceFor($product),
            // The lot owns the selling price now — a received consignment
            // without one is unsellable, so every demo receipt prices its lots.
            'sale_price' => $salePrice ?? $this->salePriceFor($product),
        ];

        if (in_array($product->id, CatalogSeeder::$serializedIds, true)) {
            $count = max(1, (int) round($quantity));
            $line['quantity'] = (float) $count;
            $line['serial_numbers'] = [];
            for ($s = 1; $s <= $count; $s++) {
                $line['serial_numbers'][] = "SN-{$product->sku}-{$tag}-".fake()->unique()->numerify('######');
            }
        } elseif (in_array($product->id, CatalogSeeder::$perishableIds, true)) {
            // Most batches expire well in the future; a small slice are near-expiry for realism.
            $expiresSoon = random_int(1, 10) === 1;
            $line['manufacture_date'] = now()->subMonths(random_int(1, 4))->toDateString();
            $line['expiry_date'] = $expiresSoon
                ? now()->addDays(random_int(5, 20))->toDateString()
                : now()->addMonths(random_int(6, 18))->toDateString();
        }

        return $line;
    }

    private function openingQuantity(Product $product): float
    {
        return match (true) {
            in_array($product->id, CatalogSeeder::$serializedIds, true) => (float) random_int(4, 15),
            in_array($product->id, CatalogSeeder::$perishableIds, true) => (float) random_int(60, 300),
            default => (float) random_int(30, 150),
        };
    }

    private function restockQuantity(Product $product): float
    {
        return match (true) {
            in_array($product->id, CatalogSeeder::$serializedIds, true) => (float) random_int(2, 8),
            in_array($product->id, CatalogSeeder::$perishableIds, true) => (float) random_int(20, 120),
            default => (float) random_int(10, 60),
        };
    }

    /**
     * The product's "typical" selling price from the in-process catalog price
     * book — the price CatalogSeeder faked for it in the same demo run. A
     * standalone PurchasingSeeder run (no CatalogSeeder before it) fakes a
     * fresh plausible price instead; either way the price lands on the GRN's
     * lots, never on the product row.
     */
    private function salePriceFor(Product $product): float
    {
        return CatalogSeeder::$priceBook[$product->id]['sale'] ?? round(fake()->randomFloat(2, 90, 349999), 2);
    }

    private function purchasePriceFor(Product $product): float
    {
        return CatalogSeeder::$priceBook[$product->id]['purchase'] ?? round($this->salePriceFor($product) * fake()->randomFloat(2, 0.55, 0.75), 2);
    }
}
