<?php

namespace App\Services;

use App\Exceptions\StockReferenceException;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\ReturnGrn;
use App\Models\Inventory\ReturnGrnItem;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\MovementType;
use App\Models\Lookups\ReturnGrnStatus;
use App\Models\Purchasing\Supplier;
use App\Support\SerialInput;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Stock-out back to a supplier via a Return GRN, the mirror of GrnService:
 *
 *  - create()  only ever produces a DRAFT return GRN with its line items —
 *              no stock effect yet.
 *  - confirm() is the only thing that moves stock OUT (`return_out`
 *              movements) and consumes the quantity of the exact lot a
 *              line pins (a lot is mandatory — there is no batch-less
 *              fallback), flags returning serials as returned_to_supplier,
 *              and credits the supplier ledger (outstanding balance drops).
 *  - delete()  is a STATUS change (return_grn_status_id → DELETED), not an
 *              Eloquent delete — the row stays visible. If the document was
 *              confirmed, deletion reverses every movement
 *              (`return_out_reversal`), restocks batches, restores serials to
 *              available at the branch, and debits the supplier back.
 *
 * Every stock change goes through StockMovementService (the Golden Rule).
 */
class ReturnGrnService
{
    public function __construct(
        private readonly DocumentNumberService $numbers,
        private readonly StockMovementService $stock,
        private readonly UnitConversionService $units,
        private readonly BatchService $batches,
        private readonly SerialService $serials,
        private readonly SupplierService $suppliers,
    ) {}

    /**
     * @param  array{branch_id:int,supplier_id?:int|null,reference?:string,reason?:string,note?:string}  $header
     * @param  array<int,array{product_id:int,unit_id?:int,source_grn_item_id?:int,stock_batch_id?:int|string|null,serial_numbers?:string,quantity:float,unit_cost:float|null,total_cost?:float}>  $lines
     */
    public function create(array $header, array $lines): ReturnGrn
    {
        // Supplier is optional: a return can be recorded for stock-out
        // reasons alone. When one IS named, confirm() also credits their
        // ledger (and delete() reverses it) — both already guard on
        // supplier_id being set, so a supplierless document simply has no
        // ledger leg.
        $supplierId = (int) ($header['supplier_id'] ?? 0);

        return DB::transaction(function () use ($header, $supplierId, $lines) {
            $returnGrn = ReturnGrn::create([
                'return_grn_number' => $this->numbers->next('return_grn'),
                'branch_id' => $header['branch_id'],
                'supplier_id' => $supplierId > 0 ? $supplierId : null,
                'reference' => $header['reference'] ?? null,
                'reason' => $header['reason'] ?? null,
                'note' => $header['note'] ?? null,
                'total_cost' => 0,
                'return_grn_status_id' => ReturnGrnStatus::DRAFT,
            ]);

            $totalCost = 0.0;

            foreach ($lines as $line) {
                $unitCost = isset($line['unit_cost']) ? (float) $line['unit_cost'] : null;
                $lineTotal = $unitCost !== null ? round($unitCost * (float) $line['quantity'], 2) : 0.0;
                $totalCost += $lineTotal;

                ReturnGrnItem::create([
                    'return_grn_id' => $returnGrn->id,
                    'product_id' => $line['product_id'],
                    'unit_id' => $line['unit_id'] ?? null,
                    'source_grn_item_id' => $line['source_grn_item_id'] ?? null,
                    'stock_batch_id' => ! empty($line['stock_batch_id']) ? (int) $line['stock_batch_id'] : null,
                    'serial_numbers' => ! empty($line['serial_numbers'])
                        ? array_values(array_filter(array_map('trim', explode(',', (string) $line['serial_numbers']))))
                        : null,
                    'quantity' => $line['quantity'],
                    'unit_cost' => $unitCost,
                    'total_cost' => $lineTotal,
                ]);
            }

            $returnGrn->update(['total_cost' => round($totalCost, 2)]);
            $returnGrn->load('branch:id,name');

            AuditLog::record('return_grn.created', $returnGrn, [
                'return_grn_number' => $returnGrn->return_grn_number,
                'branch' => $returnGrn->branch?->name,
                'reference' => $header['reference'] ?? null,
                'reason' => $header['reason'] ?? null,
                'items_count' => count($lines),
                'total_cost' => (string) round($totalCost, 2),
                'status' => 'draft',
            ]);

            return $returnGrn;
        });
    }

    /**
     * Confirm a draft return GRN: send the goods back to the supplier —
     * stock out, batch consumption, serial flags, supplier ledger credit.
     */
    public function confirm(ReturnGrn $returnGrn, int $confirmedByUserId): ReturnGrn
    {
        if (! $returnGrn->isDraft()) {
            throw new \InvalidArgumentException('Only draft return GRNs can be confirmed.');
        }

        return DB::transaction(function () use ($returnGrn, $confirmedByUserId) {
            $returnGrn->load([
                'items.product:id,name,sku,base_unit_id',
                'branch:id,name',
            ]);

            $itemSummaries = [];
            $totalCost = 0.0;

            foreach ($returnGrn->items as $item) {
                $productName = $item->product?->name ?? "Product#{$item->product_id}";
                $product = $item->product;

                $baseQty = $product && $item->unit_id
                    ? $this->units->toBaseUnit($product, (int) $item->unit_id, (float) $item->quantity)
                    : (float) $item->quantity;

                if ($baseQty <= 0) {
                    throw new \InvalidArgumentException("Line for {$productName} has no quantity to return.");
                }

                // ── A line that actually carries serials: every unit leaves ──
                $serialNumbers = SerialInput::numbers($item->serial_numbers);
                if ($serialNumbers !== []) {
                    $serialCount = count($serialNumbers);
                    if (abs($serialCount - $baseQty) > 0.0001) {
                        throw StockReferenceException::serial(
                            $item->product_id,
                            "Product {$productName}: expected {$baseQty} serial number(s) but received {$serialCount}",
                        );
                    }

                    $unitBaseQty = $baseQty / $serialCount;

                    foreach ($serialNumbers as $serialNumber) {
                        $serial = ProductSerial::query()
                            ->where('product_id', $item->product_id)
                            ->where('serial_number', $serialNumber)
                            ->first();

                        if (! $serial) {
                            throw StockReferenceException::serial(
                                $item->product_id,
                                "Serial #{$serialNumber} is not on file",
                            );
                        }

                        $this->serials->validateAvailable($serial);

                        if ((int) $serial->branch_id !== (int) $returnGrn->branch_id) {
                            throw StockReferenceException::serial(
                                $item->product_id,
                                "Serial #{$serialNumber} is not in this branch",
                            );
                        }

                        $stockBatchId = $serial->stock_batch_id;
                        if (! $stockBatchId) {
                            throw StockReferenceException::lot(
                                $item->product_id,
                                "Serial #{$serialNumber} has no lot on file",
                            );
                        }

                        $this->stock->record(
                            branchId: $returnGrn->branch_id,
                            productId: $item->product_id,
                            movementTypeCode: MovementType::RETURN_OUT,
                            quantityChange: -$unitBaseQty,
                            unitCost: isset($item->unit_cost) ? (float) $item->unit_cost : null,
                            referenceType: $returnGrn,
                            referenceNumber: $returnGrn->return_grn_number,
                            productName: $productName,
                            branchName: $returnGrn->branch?->name,
                            stockBatchId: $stockBatchId,
                        );

                        if ($stockBatchId) {
                            $this->batches->consume(StockBatch::findOrFail($stockBatchId), $unitBaseQty);
                        }

                        $this->serials->returnToSupplier($serial, $returnGrn->return_grn_number);
                    }

                    if (isset($item->unit_cost) && $item->unit_cost !== null) {
                        $totalCost += (float) $item->unit_cost * (float) $item->quantity;
                    }
                    $itemSummaries[] = [
                        'product' => $productName,
                        'qty' => (float) $item->quantity,
                        'serials' => $serialNumbers,
                    ];

                    continue;
                }

                // ── Every other line: pin the exact lot being returned ───────
                if (empty($item->stock_batch_id)) {
                    throw StockReferenceException::lot(
                        $item->product_id,
                        "Return GRN line for {$productName} has no lot selected — pick the consignment being returned.",
                    );
                }

                $batch = StockBatch::findOrFail($item->stock_batch_id);
                if ((float) $batch->available_quantity + 0.0001 < $baseQty) {
                    throw new RuntimeException(
                        "Cannot return {$baseQty} of {$productName}: only {$batch->available_quantity} available in batch {$batch->internal_batch_number}."
                    );
                }

                $this->stock->record(
                    branchId: $returnGrn->branch_id,
                    productId: $item->product_id,
                    movementTypeCode: MovementType::RETURN_OUT,
                    quantityChange: -$baseQty,
                    unitCost: isset($item->unit_cost) ? (float) $item->unit_cost : null,
                    referenceType: $returnGrn,
                    referenceNumber: $returnGrn->return_grn_number,
                    productName: $productName,
                    branchName: $returnGrn->branch?->name,
                    stockBatchId: $batch->id,
                );

                $this->batches->consume($batch, $baseQty);

                if (isset($item->unit_cost) && $item->unit_cost !== null) {
                    $totalCost += (float) $item->unit_cost * (float) $item->quantity;
                }
                $itemSummaries[] = ['product' => $productName, 'qty' => (float) $item->quantity];
            }

            if ($returnGrn->supplier_id && $totalCost > 0) {
                $this->suppliers->credit(
                    Supplier::findOrFail($returnGrn->supplier_id),
                    $totalCost,
                    $returnGrn,
                    "Return GRN {$returnGrn->return_grn_number} sent to supplier",
                );
            }

            $returnGrn->update([
                'return_grn_status_id' => ReturnGrnStatus::CONFIRMED,
                'confirmed_by' => $confirmedByUserId,
                'confirmed_at' => now(),
            ]);

            AuditLog::record('return_grn.confirmed', $returnGrn, [
                'return_grn_number' => $returnGrn->return_grn_number,
                'branch' => $returnGrn->branch?->name,
                'items_count' => $returnGrn->items->count(),
                'items' => $itemSummaries,
                'total_cost' => (string) round($totalCost, 2),
            ]);

            return $returnGrn;
        });
    }

    /**
     * Delete a return GRN (admin-only). Reverses every stock effect if the
     * document was confirmed; always sets status to Deleted. Does not
     * physically remove the record — the row remains visible in the table.
     */
    public function delete(ReturnGrn $returnGrn, int $deletedByUserId): void
    {
        if ($returnGrn->isDeleted()) {
            throw new \InvalidArgumentException('Return GRN is already deleted.');
        }

        DB::transaction(function () use ($returnGrn, $deletedByUserId) {
            $stockReversed = $returnGrn->isConfirmed();

            if ($stockReversed) {
                $returnGrn->load([
                    'items.product:id,name,base_unit_id',
                    'branch:id,name',
                ]);

                $totalCost = 0.0;

                foreach ($returnGrn->items as $item) {
                    $productName = $item->product?->name ?? "Product#{$item->product_id}";
                    $product = $item->product;

                    $baseQty = $product && $item->unit_id
                        ? $this->units->toBaseUnit($product, (int) $item->unit_id, (float) $item->quantity)
                        : (float) $item->quantity;

                    $serialNumbers = SerialInput::numbers($item->serial_numbers);

                    if ($serialNumbers !== []) {
                        $unitBaseQty = $baseQty / count($serialNumbers);

                        foreach ($serialNumbers as $serialNumber) {
                            $serial = ProductSerial::query()
                                ->where('product_id', $item->product_id)
                                ->where('serial_number', $serialNumber)
                                ->first();

                            if (! $serial) {
                                continue;
                            }

                            $stockBatchId = $serial->stock_batch_id;

                            $this->stock->record(
                                branchId: $returnGrn->branch_id,
                                productId: $item->product_id,
                                movementTypeCode: MovementType::RETURN_OUT_REVERSAL,
                                quantityChange: $unitBaseQty,
                                unitCost: isset($item->unit_cost) ? (float) $item->unit_cost : null,
                                referenceType: $returnGrn,
                                referenceNumber: $returnGrn->return_grn_number,
                                note: "Reversal of return GRN {$returnGrn->return_grn_number} deletion",
                                productName: $productName,
                                branchName: $returnGrn->branch?->name,
                                stockBatchId: $stockBatchId,
                            );

                            if ($stockBatchId) {
                                $this->batches->restock(StockBatch::findOrFail($stockBatchId), $unitBaseQty);
                            }

                            $this->serials->reverseReturnToSupplier($serial, (int) $returnGrn->branch_id, $returnGrn->return_grn_number);
                        }
                    } else {
                        $stockBatchId = ! empty($item->stock_batch_id) ? (int) $item->stock_batch_id : null;

                        $this->stock->record(
                            branchId: $returnGrn->branch_id,
                            productId: $item->product_id,
                            movementTypeCode: MovementType::RETURN_OUT_REVERSAL,
                            quantityChange: $baseQty,
                            unitCost: isset($item->unit_cost) ? (float) $item->unit_cost : null,
                            referenceType: $returnGrn,
                            referenceNumber: $returnGrn->return_grn_number,
                            note: "Reversal of return GRN {$returnGrn->return_grn_number} deletion",
                            productName: $productName,
                            branchName: $returnGrn->branch?->name,
                            stockBatchId: $stockBatchId,
                        );

                        if ($stockBatchId) {
                            $this->batches->restock(StockBatch::findOrFail($stockBatchId), $baseQty);
                        }
                    }

                    if (isset($item->unit_cost) && $item->unit_cost !== null) {
                        $totalCost += (float) $item->unit_cost * (float) $item->quantity;
                    }
                }

                if ($returnGrn->supplier_id && $totalCost > 0) {
                    $this->suppliers->debit(
                        Supplier::findOrFail($returnGrn->supplier_id),
                        $totalCost,
                        $returnGrn,
                        "Reversal of return GRN {$returnGrn->return_grn_number} deletion",
                    );
                }
            } else {
                $returnGrn->load('branch:id,name');
            }

            AuditLog::record('return_grn.deleted', $returnGrn, [
                'return_grn_number' => $returnGrn->return_grn_number,
                'branch' => $returnGrn->branch?->name,
                'previous_status' => $returnGrn->returnGrnStatus?->name ?? 'unknown',
                'items_count' => $returnGrn->items->count(),
                'stock_reversed' => $stockReversed,
            ], $deletedByUserId);

            // Status change only — the record stays visible in the table.
            $returnGrn->return_grn_status_id = ReturnGrnStatus::DELETED;
            $returnGrn->deleted_by = $deletedByUserId;
            $returnGrn->deleted_at = now();
            $returnGrn->saveQuietly();
        });
    }
}
