<?php

namespace App\Services;

use App\Models\InventoryMovement;
use App\Models\InventoryProduct;
use App\Models\InventoryTransferDocument;
use App\Models\InventoryTransferLine;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

/**
 * Confirmación y cancelación de documentos de inventario (ADR-002 v2): recepción (`in`),
 * entrega interna y salida documentada (`out`).
 * Delega en {@see InventoryMovementService::recordMovementWithoutTransaction} dentro de una sola transacción.
 */
class InventoryTransferDocumentService
{
    public function __construct(
        private readonly InventoryMovementService $movementService
    ) {}

    /**
     * Crea cabecera en borrador y líneas en una transacción.
     *
     * @param  array<string, mixed>  $header  folio, transfer_type, destination_area_id?, recipient_user_id?, supplier_id?, supplier_reference?, notes?
     * @param  list<array{inventory_product_id: int, quantity: string|int|float, sort_order?: int}>  $lines
     */
    /**
     * Crea documento en borrador y lo confirma en la misma transacción (entrada/salida rápida).
     *
     * @param  array<string, mixed>  $header
     * @param  list<array{inventory_product_id: int, quantity: string|int|float, sort_order?: int}>  $lines
     */
    public function createAndConfirm(array $header, array $lines, int $userId): InventoryTransferDocument
    {
        return DB::transaction(function () use ($header, $lines, $userId): InventoryTransferDocument {
            $folio = isset($header['folio']) && trim((string) $header['folio']) !== ''
                ? trim((string) $header['folio'])
                : $this->allocateQuickFolio((string) $header['transfer_type']);

            $header['folio'] = $folio;

            $doc = $this->createDraft($header, $lines, $userId);

            return $this->confirm($doc, $userId);
        });
    }

    public function allocateQuickFolio(string $transferType): string
    {
        $prefix = match ($transferType) {
            InventoryTransferDocument::TRANSFER_INBOUND_RECEIPT => 'ENT',
            InventoryTransferDocument::TRANSFER_OUTBOUND => 'SAL',
            default => 'MOV',
        };

        for ($attempt = 0; $attempt < 8; $attempt++) {
            $suffix = $attempt > 0 ? '-'.random_int(10, 99) : '';
            $folio = sprintf('%s-%s%s', $prefix, now()->format('Ymd-His'), $suffix);
            if (! InventoryTransferDocument::query()->where('folio', $folio)->exists()) {
                return $folio;
            }
            usleep(5000);
        }

        throw new InvalidArgumentException('No se pudo generar un folio único para el movimiento.');
    }

    public function createDraft(array $header, array $lines, int $createdByUserId): InventoryTransferDocument
    {
        return DB::transaction(function () use ($header, $lines, $createdByUserId): InventoryTransferDocument {
            $doc = InventoryTransferDocument::query()->create([
                'folio' => $header['folio'],
                'transfer_type' => $header['transfer_type'],
                'status' => InventoryTransferDocument::STATUS_DRAFT,
                'destination_area_id' => $header['destination_area_id'] ?? null,
                'recipient_user_id' => $header['recipient_user_id'] ?? null,
                'supplier_id' => $header['supplier_id'] ?? null,
                'supplier_reference' => $header['supplier_reference'] ?? null,
                'notes' => $header['notes'] ?? null,
                'created_by' => $createdByUserId,
            ]);

            $this->persistLines($doc->id, $lines);

            return $doc->fresh([
                'lines.inventoryProduct',
                'destinationArea',
                'recipientUser',
                'supplier',
                'createdByUser',
            ]);
        });
    }

    /**
     * Reemplaza cabecera (excepto created_by) y líneas; solo en borrador.
     *
     * @param  array<string, mixed>  $header
     * @param  list<array{inventory_product_id: int, quantity: string|int|float, sort_order?: int}>  $lines
     */
    public function updateDraft(InventoryTransferDocument $document, array $header, array $lines): InventoryTransferDocument
    {
        return DB::transaction(function () use ($document, $header, $lines): InventoryTransferDocument {
            /** @var InventoryTransferDocument $doc */
            $doc = InventoryTransferDocument::query()->lockForUpdate()->findOrFail($document->id);

            if ($doc->status !== InventoryTransferDocument::STATUS_DRAFT) {
                throw new InvalidArgumentException('Solo se puede editar un documento en borrador.');
            }

            $doc->update([
                'folio' => $header['folio'],
                'transfer_type' => $header['transfer_type'],
                'destination_area_id' => $header['destination_area_id'] ?? null,
                'recipient_user_id' => $header['recipient_user_id'] ?? null,
                'supplier_id' => $header['supplier_id'] ?? null,
                'supplier_reference' => $header['supplier_reference'] ?? null,
                'notes' => $header['notes'] ?? null,
            ]);

            InventoryTransferLine::query()
                ->where('inventory_transfer_document_id', $doc->id)
                ->delete();

            $this->persistLines($doc->id, $lines);

            return $doc->fresh([
                'lines.inventoryProduct',
                'destinationArea',
                'recipientUser',
                'supplier',
                'createdByUser',
            ]);
        });
    }

    /**
     * @param  list<array{inventory_product_id: int, quantity: string|int|float, sort_order?: int}>  $lines
     */
    private function persistLines(int $documentId, array $lines): void
    {
        foreach ($lines as $index => $line) {
            $sort = array_key_exists('sort_order', $line) ? (int) $line['sort_order'] : $index;
            InventoryTransferLine::query()->create([
                'inventory_transfer_document_id' => $documentId,
                'inventory_product_id' => (int) $line['inventory_product_id'],
                'quantity' => (string) $line['quantity'],
                'sort_order' => $sort,
            ]);
        }
    }

    /**
     * Aplica movimientos por línea (`in` o `out` según `transfer_type`) y marca el documento confirmado.
     *
     * @throws InvalidArgumentException
     */
    public function confirm(InventoryTransferDocument $document, int $confirmingUserId): InventoryTransferDocument
    {
        return DB::transaction(function () use ($document, $confirmingUserId): InventoryTransferDocument {
            /** @var InventoryTransferDocument $doc */
            $doc = InventoryTransferDocument::query()->lockForUpdate()->findOrFail($document->id);

            if ($doc->status !== InventoryTransferDocument::STATUS_DRAFT) {
                throw new InvalidArgumentException('Solo se puede confirmar un documento en borrador.');
            }

            [$movementType, $referencePrefix] = $this->resolveMovementTypeAndReferencePrefix($doc);

            $this->assertDestinationForConfirm($doc);

            $lines = InventoryTransferLine::query()
                ->where('inventory_transfer_document_id', $doc->id)
                ->orderBy('inventory_product_id')
                ->orderBy('id')
                ->get();

            if ($lines->isEmpty()) {
                throw new InvalidArgumentException('El documento no tiene líneas para confirmar.');
            }

            foreach ($lines as $line) {
                $qty = trim((string) $line->quantity);
                if ($qty === '' || bccomp($qty, '0', 4) <= 0) {
                    throw new InvalidArgumentException('Cada línea debe tener cantidad mayor a cero.');
                }

                $product = InventoryProduct::query()->findOrFail($line->inventory_product_id);

                $reference = sprintf('%s:%s:line:%s', $referencePrefix, $doc->folio, $line->id);

                $this->movementService->recordMovementWithoutTransaction(
                    $product,
                    $movementType,
                    $qty,
                    $reference,
                    $confirmingUserId
                );
            }

            $doc->update([
                'status' => InventoryTransferDocument::STATUS_CONFIRMED,
                'confirmed_by' => $confirmingUserId,
                'confirmed_at' => now(),
            ]);

            return $doc->fresh(['lines', 'destinationArea', 'recipientUser', 'supplier', 'confirmedByUser']);
        });
    }

    /**
     * @return array{0: string, 1: string} [movement type, reference prefix without trailing colon segment]
     */
    private function resolveMovementTypeAndReferencePrefix(InventoryTransferDocument $doc): array
    {
        return match ($doc->transfer_type) {
            InventoryTransferDocument::TRANSFER_INBOUND_RECEIPT => [InventoryMovement::TYPE_IN, 'receipt'],
            InventoryTransferDocument::TRANSFER_INTERNAL_DELIVERY,
            InventoryTransferDocument::TRANSFER_OUTBOUND => [InventoryMovement::TYPE_OUT, 'transfer'],
            default => throw new InvalidArgumentException(
                'Tipo de documento no válido: '.$doc->transfer_type.'. Use inbound_receipt, internal_delivery u outbound.'
            ),
        };
    }

    /**
     * @throws InvalidArgumentException
     */
    public function cancelDraft(InventoryTransferDocument $document, int $actingUserId): InventoryTransferDocument
    {
        return DB::transaction(function () use ($document, $actingUserId): InventoryTransferDocument {
            $doc = InventoryTransferDocument::query()->lockForUpdate()->findOrFail($document->id);

            if ($doc->status !== InventoryTransferDocument::STATUS_DRAFT) {
                throw new InvalidArgumentException('Solo se puede cancelar un documento en borrador.');
            }

            $doc->update([
                'status' => InventoryTransferDocument::STATUS_CANCELLED,
                'cancelled_by' => $actingUserId,
                'cancelled_at' => now(),
            ]);

            return $doc->fresh([
                'lines',
                'destinationArea',
                'recipientUser',
                'supplier',
                'createdByUser',
                'cancelledByUser',
            ]);
        });
    }

    private function assertDestinationForConfirm(InventoryTransferDocument $doc): void
    {
        if ($doc->transfer_type !== InventoryTransferDocument::TRANSFER_INTERNAL_DELIVERY) {
            return;
        }

        if ($doc->destination_area_id === null && $doc->recipient_user_id === null) {
            throw new InvalidArgumentException(
                'Para entrega interna indique área de destino o usuario receptor antes de confirmar.'
            );
        }
    }
}
