<?php

namespace App\Services;

use App\Events\OrdenLavanderiaCreada;
use App\Events\OrdenLavanderiaTerminada;
use App\Models\CajaMovimiento;
use App\Models\OrdenEstadoLog;
use App\Models\OrdenLavanderia;
use App\Models\OrdenLavanderiaDetalle;
use App\Models\OrdenLavanderiaBolsa;
use App\Models\OrdenLavanderiaPago;
use App\Models\LaundrySaleProduct;
use App\Models\Producto;
use App\Models\TipoServicio;
use App\Models\Etapa;
use App\Models\ShippingOrder;
use App\Repositories\Contracts\OrdenLavanderiaRepositoryInterface;
use App\Support\LaundryAuditLogger;
use App\Support\LaundrySemaphoreColorResolver;
use App\Support\OrdenLavanderiaDetalleLineValidator;
use DomainException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class OrdenLavanderiaService
{
    public function __construct(
        private readonly OrdenLavanderiaRepositoryInterface $ordenes,
        private readonly ImpresionInstruccionesLavanderiaService $impresionInstrucciones,
        private readonly LaundryPricingService $laundryPricing
    ) {}

    /**
     * Crea orden desde carrito.
     * - Ítems con `producto_id`: catálogo lavandería (legacy); descuenta stock solo si es producto físico.
     * - Ítems solo TIBU (sin `producto_id`): precios desde matriz `laundry_piece_prices` + `pricing_rules` (C2); snapshot en línea; no toca inventario interno.
     *
     * @param  array{cliente_nombre: string, cliente_email?: string, cliente_id?: int, detalles_ropa?: string, instrucciones?: string, is_priority?: bool, metodo_pago?: string, caja_id?: int}  $data
     * @param  array<int, array<string, mixed>>  $items
     */
    public function crearOrdenDesdeCarrito(array $data, array $items, ?int $recepcionistaId = null): OrdenLavanderia
    {
        $hadPaymentsKey = array_key_exists('payments', $data);
        $paymentsIn = $hadPaymentsKey ? $data['payments'] : null;
        $finalizar = (bool) ($data['finalizar'] ?? false);
        $hadMetodoKey = array_key_exists('metodo_pago', $data);
        unset($data['payments'], $data['finalizar']);

        $shippingInput = [
            'is_delivery' => (bool) ($data['is_delivery'] ?? false),
            'delivery_address' => $data['delivery_address'] ?? null,
        ];
        unset($data['is_delivery'], $data['delivery_address']);

        $idsProducto = collect($items)->pluck('producto_id')->filter(fn ($id) => $id !== null && $id !== '')->map(fn ($id) => (int) $id)->unique()->values()->all();
        $productos = $idsProducto === [] ? collect() : Producto::whereIn('id', $idsProducto)->get()->keyBy('id');

        $subtotal = 0.0;
        $tipoServicio = 'Venta';
        $detallesRopa = $data['detalles_ropa'] ?? 'Ver detalle en ítems';

        $lineas = [];
        foreach ($items as $index => $item) {
            if (OrdenLavanderiaDetalleLineValidator::isInformationalItem($item)) {
                $lineas[] = [
                    'kind' => 'informational',
                    'item' => $item,
                    'cantidad' => (float) ($item['cantidad'] ?? 1),
                ];
            } elseif (! empty($item['producto_id'])) {
                $p = $productos->get((int) $item['producto_id']);
                if (! $p) {
                    continue;
                }
                $cant = (float) $item['cantidad'];
                $precio = (float) $p->precio;
                $lineTotal = round($cant * $precio, 2);
                $subtotal += $lineTotal;
                if ($p->es_servicio && $tipoServicio === 'Venta') {
                    $tipoServicio = $p->nombre;
                }
                $lineas[] = [
                    'kind' => 'producto',
                    'item' => $item,
                    'producto' => $p,
                    'precio_unitario' => $precio,
                    'cantidad' => $cant,
                    'subtotal' => $lineTotal,
                ];
            } elseif (! empty($item['laundry_sale_product_id'])) {
                $resolved = $this->laundryPricing->previewSaleCatalogLineMoney($item, $index);
                $subtotal += $resolved['subtotal'];
                if ($tipoServicio === 'Venta') {
                    $st = Etapa::query()->find((int) $resolved['resolved_etapa_id']);
                    if ($st) {
                        $tipoServicio = $st->name;
                    }
                }
                $lineas[] = array_merge(['kind' => 'sale_catalog', 'item' => $item], $resolved);
            } else {
                $resolved = $this->resolveTibuLineMoney($item, $index);
                $subtotal += $resolved['subtotal'];
                if ($tipoServicio === 'Venta' && ! empty($item['etapa_id'])) {
                    $st = Etapa::query()->find((int) $item['etapa_id']);
                    if ($st) {
                        $tipoServicio = $st->name;
                    }
                }
                $lineas[] = array_merge(['kind' => 'tibu', 'item' => $item], $resolved);
            }
        }

        $folioUnico = $this->generarFolioUnico();
        $semaphore = LaundrySemaphoreColorResolver::resolveThresholdsForOrderPayload($data);
        $fechaEntrega = self::parseFechaEntregaInput($data['fecha_entrega'] ?? null);

        $payload = array_merge($data, [
            'folio_unico' => $folioUnico,
            'codigo_qr' => $this->generarCodigoQR($folioUnico),
            'tipo_servicio' => $tipoServicio,
            'detalles_ropa' => $detallesRopa,
            'total' => round($subtotal, 2),
            'subtotal' => round($subtotal, 2),
            'impuestos' => 0,
            'fecha_entrega' => $fechaEntrega,
            'semaphore_hours_red' => $semaphore['red'],
            'semaphore_hours_yellow' => $semaphore['yellow'],
            'current_step' => OrdenLavanderia::STEP_RECEPCION,
            'current_step_started_at' => now(),
            'estatus' => OrdenLavanderia::ESTATUS_PENDIENTE,
            'recepcionista_id' => $recepcionistaId,
        ]);

        return DB::transaction(function () use ($payload, $lineas, $shippingInput, $hadPaymentsKey, $paymentsIn, $finalizar, $hadMetodoKey, $recepcionistaId): OrdenLavanderia {
            $orden = $this->ordenes->create($payload);
            $this->createShippingOrderForOrden($orden, $shippingInput);

            foreach ($lineas as $line) {
                if ($line['kind'] === 'producto') {
                    /** @var Producto $p */
                    $p = $line['producto'];
                    $item = $line['item'];
                    $cant = (float) $line['cantidad'];
                    $precio = (float) $line['precio_unitario'];
                    $sub = (float) $line['subtotal'];

                    OrdenLavanderiaDetalle::create([
                        'orden_lavanderia_id' => $orden->id,
                        'etapa_id' => isset($item['etapa_id']) ? (int) $item['etapa_id'] : null,
                        'producto_id' => $p->id,
                        'garment_type_id' => $item['garment_type_id'] ?? null,
                        'load_type_id' => $item['load_type_id'] ?? null,
                        'billing_mode' => $item['billing_mode'] ?? null,
                        'weight_kg' => isset($item['weight_kg']) ? (float) $item['weight_kg'] : null,
                        'cantidad' => $cant,
                        'precio_unitario' => $precio,
                        'subtotal' => $sub,
                        'observations' => $item['observations'] ?? null,
                    ]);

                    if (! $p->es_servicio && $cant > 0) {
                        $p->descontarStock((int) round($cant, 0));
                    }
                } elseif ($line['kind'] === 'sale_catalog') {
                    $item = $line['item'];
                    $extra = (float) ($line['extra_cost'] ?? 0);
                    OrdenLavanderiaDetalle::create([
                        'orden_lavanderia_id' => $orden->id,
                        'etapa_id' => (int) $line['resolved_etapa_id'],
                        'producto_id' => null,
                        'laundry_sale_product_id' => (int) $item['laundry_sale_product_id'],
                        'garment_type_id' => null,
                        'load_type_id' => $line['resolved_load_type_id'],
                        'billing_mode' => $line['resolved_billing_mode'],
                        'weight_kg' => $line['weight_kg'],
                        'cantidad' => (float) $line['cantidad'],
                        'cantidad_cobro' => (float) ($line['cantidad_cobro'] ?? $line['cantidad']),
                        'precio_unitario' => (float) $line['precio_unitario'],
                        'subtotal' => (float) $line['subtotal'],
                        'extra_cost' => $extra > 0 ? $extra : null,
                        'observations' => $item['observations'] ?? null,
                    ]);
                    $saleProduct = LaundrySaleProduct::query()
                        ->whereKey((int) $item['laundry_sale_product_id'])
                        ->lockForUpdate()
                        ->first();
                    if (
                        $saleProduct !== null
                        && $saleProduct->billing_unit === LaundrySaleProduct::BILLING_UNIT_PIECE
                        && ($line['resolved_billing_mode'] ?? null) === OrdenLavanderiaDetalleLineValidator::BILLING_BY_GARMENT
                        && $saleProduct->manejaInventarioPos()
                    ) {
                        $qty = (int) round((float) $line['cantidad'], 0);
                        if ($qty > 0) {
                            $saleProduct->descontarStock($qty);
                        }
                    }
                } elseif ($line['kind'] === 'informational') {
                    $item = $line['item'];
                    OrdenLavanderiaDetalle::create([
                        'orden_lavanderia_id' => $orden->id,
                        'etapa_id' => null,
                        'producto_id' => null,
                        'laundry_sale_product_id' => (int) $item['laundry_sale_product_id'],
                        'garment_type_id' => (int) $item['garment_type_id'],
                        'load_type_id' => null,
                        'billing_mode' => null,
                        'weight_kg' => null,
                        'cantidad' => (float) $line['cantidad'],
                        'precio_unitario' => 0,
                        'subtotal' => 0,
                        'is_informational' => true,
                        'observations' => $item['observations'] ?? null,
                    ]);
                } else {
                    $item = $line['item'];
                    OrdenLavanderiaDetalle::create([
                        'orden_lavanderia_id' => $orden->id,
                        'etapa_id' => (int) $item['etapa_id'],
                        'producto_id' => null,
                        'garment_type_id' => $item['garment_type_id'] ?? null,
                        'load_type_id' => $line['resolved_load_type_id'],
                        'billing_mode' => $line['resolved_billing_mode'],
                        'weight_kg' => $line['weight_kg'],
                        'cantidad' => (float) $line['cantidad'],
                        'cantidad_cobro' => (float) ($line['cantidad_cobro'] ?? $line['cantidad']),
                        'precio_unitario' => (float) $line['precio_unitario'],
                        'subtotal' => (float) $line['subtotal'],
                        'observations' => $item['observations'] ?? null,
                    ]);
                }
            }

            $this->impresionInstrucciones->obtenerAlertasPorArea($orden);
            event(new OrdenLavanderiaCreada($orden));

            $orden = $this->aplicarCierreRecepcionSiFinaliza(
                $orden,
                $finalizar,
                $hadPaymentsKey,
                $paymentsIn,
                $hadMetodoKey,
                $recepcionistaId
            );

            return $orden->load([
                'recepcionista',
                'cliente.clienteCategoria',
                'detalles.producto',
                'detalles.etapa',
                'detalles.garmentType',
                'detalles.laundrySaleProduct',
                'detalles.loadType',
                'slaTipoServicio',
                'shippingOrder',
                'pagos',
                'bolsas',
            ]);
        });
    }

    public function crearOrden(array $data, ?int $recepcionistaId = null): OrdenLavanderia
    {
        $hadPaymentsKey = array_key_exists('payments', $data);
        $paymentsIn = $hadPaymentsKey ? $data['payments'] : null;
        $finalizar = (bool) ($data['finalizar'] ?? false);
        $hadMetodoKey = array_key_exists('metodo_pago', $data);
        unset($data['payments'], $data['finalizar']);

        $shippingInput = [
            'is_delivery' => (bool) ($data['is_delivery'] ?? false),
            'delivery_address' => $data['delivery_address'] ?? null,
        ];
        unset($data['is_delivery'], $data['delivery_address']);

        $folioUnico = $this->generarFolioUnico();
        $semaphore = LaundrySemaphoreColorResolver::resolveThresholdsForOrderPayload($data);
        $fechaEntrega = self::parseFechaEntregaInput($data['fecha_entrega'] ?? null);

        $payload = array_merge([
            'tipo_servicio' => 'Recepcion',
            'detalles_ropa' => 'Sin detalle',
            'total' => 0,
            'subtotal' => 0,
            'impuestos' => 0,
        ], $data, [
            'folio_unico' => $folioUnico,
            'codigo_qr' => $this->generarCodigoQR($folioUnico),
            'fecha_entrega' => $fechaEntrega,
            'semaphore_hours_red' => $semaphore['red'],
            'semaphore_hours_yellow' => $semaphore['yellow'],
            'current_step' => OrdenLavanderia::STEP_RECEPCION,
            'current_step_started_at' => now(),
            'estatus' => OrdenLavanderia::ESTATUS_PENDIENTE,
            'recepcionista_id' => $recepcionistaId,
        ]);

        return DB::transaction(function () use ($payload, $shippingInput, $hadPaymentsKey, $paymentsIn, $finalizar, $hadMetodoKey, $recepcionistaId): OrdenLavanderia {
            $orden = $this->ordenes->create($payload);
            $this->createShippingOrderForOrden($orden, $shippingInput);

            $this->impresionInstrucciones->obtenerAlertasPorArea($orden);

            event(new OrdenLavanderiaCreada($orden));

            $orden = $this->aplicarCierreRecepcionSiFinaliza(
                $orden,
                $finalizar,
                $hadPaymentsKey,
                $paymentsIn,
                $hadMetodoKey,
                $recepcionistaId
            );

            return $orden->load(['shippingOrder', 'pagos', 'bolsas']);
        });
    }

    /**
     * Permite captura incremental en recepción (cabecera + líneas) antes de avanzar de etapa.
     *
     * @param  array<int, array<string, mixed>>|null  $items
     */
    public function actualizarOrdenEnRecepcion(
        OrdenLavanderia $orden,
        array $data,
        ?array $items = null,
        ?int $operadorId = null
    ): OrdenLavanderia {
        $hadPaymentsKey = array_key_exists('payments', $data);
        $paymentsIn = $hadPaymentsKey ? $data['payments'] : null;
        $finalizar = (bool) ($data['finalizar'] ?? false);
        $hadMetodoKey = array_key_exists('metodo_pago', $data);
        unset($data['payments'], $data['finalizar']);

        return DB::transaction(function () use ($orden, $data, $items, $hadPaymentsKey, $paymentsIn, $finalizar, $hadMetodoKey, $operadorId): OrdenLavanderia {
            /** @var OrdenLavanderia $locked */
            $locked = OrdenLavanderia::query()
                ->whereKey($orden->id)
                ->lockForUpdate()
                ->firstOrFail();

            $isClosed = in_array($locked->estatus, [
                OrdenLavanderia::ESTATUS_TERMINADO,
                OrdenLavanderia::ESTATUS_ENTREGADO,
                OrdenLavanderia::ESTATUS_CANCELADO,
            ], true);
            if ($isClosed) {
                throw new \DomainException('La orden ya está cerrada; no se puede editar.');
            }

            $canEditLines = $locked->estatus === OrdenLavanderia::ESTATUS_PENDIENTE
                && $locked->current_step === OrdenLavanderia::STEP_RECEPCION;

            if ($items !== null && ! $canEditLines) {
                throw new \DomainException('La orden ya no está en recepción; no se pueden editar líneas.');
            }

            $updateData = $data;
            if ($items !== null) {
                $locked->load(['detalles.producto', 'detalles.laundrySaleProduct']);
                foreach ($locked->detalles as $detalle) {
                    if ($detalle->producto_id !== null) {
                        $producto = $detalle->producto;
                        if ($producto && ! $producto->es_servicio && $detalle->cantidad > 0) {
                            $producto->devolverStock((int) round((float) $detalle->cantidad, 0));
                        }
                    } elseif ($detalle->laundry_sale_product_id !== null) {
                        $lp = $detalle->laundrySaleProduct;
                        if (
                            $lp
                            && $lp->billing_unit === LaundrySaleProduct::BILLING_UNIT_PIECE
                            && $detalle->billing_mode === OrdenLavanderiaDetalleLineValidator::BILLING_BY_GARMENT
                            && $lp->manejaInventarioPos()
                            && (float) $detalle->cantidad > 0
                        ) {
                            $lp->devolverStock((int) round((float) $detalle->cantidad, 0));
                        }
                    }
                }

                OrdenLavanderiaDetalle::query()
                    ->where('orden_lavanderia_id', $locked->id)
                    ->delete();

                $lineas = [];
                $subtotal = 0.0;
                $tipoServicio = 'Recepcion';
                $detallesRopa = $updateData['detalles_ropa'] ?? ($locked->detalles_ropa ?: 'Sin detalle');

                $idsProducto = collect($items)->pluck('producto_id')->filter(fn ($id) => $id !== null && $id !== '')->map(fn ($id) => (int) $id)->unique()->values()->all();
                $productos = $idsProducto === [] ? collect() : Producto::whereIn('id', $idsProducto)->get()->keyBy('id');

                foreach ($items as $index => $item) {
                    if (OrdenLavanderiaDetalleLineValidator::isInformationalItem($item)) {
                        $lineas[] = [
                            'kind' => 'informational',
                            'item' => $item,
                            'cantidad' => (float) ($item['cantidad'] ?? 1),
                        ];
                    } elseif (! empty($item['producto_id'])) {
                        $p = $productos->get((int) $item['producto_id']);
                        if (! $p) {
                            continue;
                        }
                        $cant = (float) $item['cantidad'];
                        $precio = (float) $p->precio;
                        $lineTotal = round($cant * $precio, 2);
                        $subtotal += $lineTotal;
                        if ($p->es_servicio && $tipoServicio === 'Recepcion') {
                            $tipoServicio = $p->nombre;
                        }
                        $lineas[] = [
                            'kind' => 'producto',
                            'item' => $item,
                            'producto' => $p,
                            'precio_unitario' => $precio,
                            'cantidad' => $cant,
                            'subtotal' => $lineTotal,
                        ];
                    } elseif (! empty($item['laundry_sale_product_id'])) {
                        $resolved = $this->laundryPricing->previewSaleCatalogLineMoney($item, $index);
                        $subtotal += $resolved['subtotal'];
                        if ($tipoServicio === 'Recepcion') {
                            $st = Etapa::query()->find((int) $resolved['resolved_etapa_id']);
                            if ($st) {
                                $tipoServicio = $st->name;
                            }
                        }
                        $lineas[] = array_merge(['kind' => 'sale_catalog', 'item' => $item], $resolved);
                    } else {
                        $resolved = $this->resolveTibuLineMoney($item, $index);
                        $subtotal += $resolved['subtotal'];
                        if ($tipoServicio === 'Recepcion' && ! empty($item['etapa_id'])) {
                            $st = Etapa::query()->find((int) $item['etapa_id']);
                            if ($st) {
                                $tipoServicio = $st->name;
                            }
                        }
                        $lineas[] = array_merge(['kind' => 'tibu', 'item' => $item], $resolved);
                    }
                }

                foreach ($lineas as $line) {
                    if ($line['kind'] === 'producto') {
                        /** @var Producto $p */
                        $p = $line['producto'];
                        $item = $line['item'];
                        $cant = (float) $line['cantidad'];
                        $precio = (float) $line['precio_unitario'];
                        $sub = (float) $line['subtotal'];

                        OrdenLavanderiaDetalle::create([
                            'orden_lavanderia_id' => $locked->id,
                            'etapa_id' => isset($item['etapa_id']) ? (int) $item['etapa_id'] : null,
                            'producto_id' => $p->id,
                            'garment_type_id' => $item['garment_type_id'] ?? null,
                            'load_type_id' => $item['load_type_id'] ?? null,
                            'billing_mode' => $item['billing_mode'] ?? null,
                            'weight_kg' => isset($item['weight_kg']) ? (float) $item['weight_kg'] : null,
                            'cantidad' => $cant,
                            'precio_unitario' => $precio,
                            'subtotal' => $sub,
                            'observations' => $item['observations'] ?? null,
                        ]);

                        if (! $p->es_servicio && $cant > 0) {
                            $p->descontarStock((int) round($cant, 0));
                        }
                    } elseif ($line['kind'] === 'sale_catalog') {
                        $item = $line['item'];
                        $extra = (float) ($line['extra_cost'] ?? 0);
                        OrdenLavanderiaDetalle::create([
                            'orden_lavanderia_id' => $locked->id,
                            'etapa_id' => (int) $line['resolved_etapa_id'],
                            'producto_id' => null,
                            'laundry_sale_product_id' => (int) $item['laundry_sale_product_id'],
                            'garment_type_id' => null,
                            'load_type_id' => $line['resolved_load_type_id'],
                            'billing_mode' => $line['resolved_billing_mode'],
                            'weight_kg' => $line['weight_kg'],
                            'cantidad' => (float) $line['cantidad'],
                            'cantidad_cobro' => (float) ($line['cantidad_cobro'] ?? $line['cantidad']),
                            'precio_unitario' => (float) $line['precio_unitario'],
                            'subtotal' => (float) $line['subtotal'],
                            'extra_cost' => $extra > 0 ? $extra : null,
                            'observations' => $item['observations'] ?? null,
                        ]);
                        $saleProduct = LaundrySaleProduct::query()
                            ->whereKey((int) $item['laundry_sale_product_id'])
                            ->lockForUpdate()
                            ->first();
                        if (
                            $saleProduct !== null
                            && $saleProduct->billing_unit === LaundrySaleProduct::BILLING_UNIT_PIECE
                            && ($line['resolved_billing_mode'] ?? null) === OrdenLavanderiaDetalleLineValidator::BILLING_BY_GARMENT
                            && $saleProduct->manejaInventarioPos()
                        ) {
                            $qty = (int) round((float) $line['cantidad'], 0);
                            if ($qty > 0) {
                                $saleProduct->descontarStock($qty);
                            }
                        }
                    } elseif ($line['kind'] === 'informational') {
                        $item = $line['item'];
                        OrdenLavanderiaDetalle::create([
                            'orden_lavanderia_id' => $locked->id,
                            'etapa_id' => null,
                            'producto_id' => null,
                            'laundry_sale_product_id' => (int) $item['laundry_sale_product_id'],
                            'garment_type_id' => (int) $item['garment_type_id'],
                            'load_type_id' => null,
                            'billing_mode' => null,
                            'weight_kg' => null,
                            'cantidad' => (float) $line['cantidad'],
                            'precio_unitario' => 0,
                            'subtotal' => 0,
                            'is_informational' => true,
                            'observations' => $item['observations'] ?? null,
                        ]);
                    } else {
                        $item = $line['item'];
                        OrdenLavanderiaDetalle::create([
                            'orden_lavanderia_id' => $locked->id,
                            'etapa_id' => (int) $item['etapa_id'],
                            'producto_id' => null,
                            'garment_type_id' => $item['garment_type_id'] ?? null,
                            'load_type_id' => $line['resolved_load_type_id'],
                            'billing_mode' => $line['resolved_billing_mode'],
                            'weight_kg' => $line['weight_kg'],
                            'cantidad' => (float) $line['cantidad'],
                            'cantidad_cobro' => (float) ($line['cantidad_cobro'] ?? $line['cantidad']),
                            'precio_unitario' => (float) $line['precio_unitario'],
                            'subtotal' => (float) $line['subtotal'],
                            'observations' => $item['observations'] ?? null,
                        ]);
                    }
                }

                $updateData['tipo_servicio'] = $tipoServicio;
                $updateData['detalles_ropa'] = $detallesRopa;
                $updateData['total'] = round($subtotal, 2);
                $updateData['subtotal'] = round($subtotal, 2);
                $updateData['impuestos'] = 0;

                if (! array_key_exists('fecha_entrega', $updateData) && $locked->fecha_entrega === null) {
                    $updateData['fecha_entrega'] = LaundrySemaphoreColorResolver::defaultFechaEntrega();
                }
            }

            if (array_key_exists('fecha_entrega', $updateData)) {
                $updateData['fecha_entrega'] = self::parseFechaEntregaInput($updateData['fecha_entrega']);
            }

            if (
                array_key_exists('semaphore_hours_red', $updateData)
                || array_key_exists('semaphore_hours_yellow', $updateData)
            ) {
                $semaphore = LaundrySemaphoreColorResolver::resolveThresholdsForOrderPayload([
                    'semaphore_hours_red' => $updateData['semaphore_hours_red'] ?? $locked->semaphore_hours_red,
                    'semaphore_hours_yellow' => $updateData['semaphore_hours_yellow'] ?? $locked->semaphore_hours_yellow,
                ]);
                $updateData['semaphore_hours_red'] = $semaphore['red'];
                $updateData['semaphore_hours_yellow'] = $semaphore['yellow'];
            }

            $locked->fill($updateData);
            $locked->save();
            $this->syncShippingOrderOnReceptionUpdate($locked, $data);

            $locked = $this->aplicarCierreRecepcionSiFinaliza(
                $locked,
                $finalizar,
                $hadPaymentsKey,
                $paymentsIn,
                $hadMetodoKey,
                $operadorId
            );

            return $locked->load([
                'detalles.producto',
                'detalles.etapa',
                'detalles.garmentType',
                'detalles.laundrySaleProduct',
                'detalles.loadType',
                'slaTipoServicio',
                'shippingOrder',
                'pagos',
                'bolsas',
            ]);
        });
    }

    /**
     * Sincroniza estado logístico (shipping_orders) al editar orden desde recepción.
     *
     * @param  array<string, mixed>  $data
     */
    private function syncShippingOrderOnReceptionUpdate(OrdenLavanderia $orden, array $data): void
    {
        if (! array_key_exists('is_delivery', $data) && ! array_key_exists('delivery_address', $data)) {
            return;
        }

        $shipping = $orden->shippingOrder()->first();
        if ($shipping === null) {
            $shipping = $this->createShippingOrderForOrden($orden, [
                'is_delivery' => (bool) ($data['is_delivery'] ?? false),
                'delivery_address' => $data['delivery_address'] ?? null,
            ]);
        }

        if (array_key_exists('is_delivery', $data)) {
            $isDelivery = (bool) $data['is_delivery'];
            $shipping->is_delivery = $isDelivery;
            if ($isDelivery) {
                if ($shipping->delivery_status === ShippingOrder::STATUS_NOT_REQUESTED) {
                    $shipping->delivery_status = ShippingOrder::STATUS_PENDING;
                }
                if ($shipping->pending_at === null) {
                    $shipping->pending_at = now();
                }
            } else {
                $shipping->delivery_status = ShippingOrder::STATUS_NOT_REQUESTED;
                $shipping->pending_at = null;
            }
        }

        if (array_key_exists('delivery_address', $data)) {
            $shipping->delivery_address = $data['delivery_address'] ?: null;
        }

        $shipping->save();
    }

    /**
     * @param  array{is_delivery: bool, delivery_address?: string|null}  $shippingInput
     */
    private function createShippingOrderForOrden(OrdenLavanderia $orden, array $shippingInput): ShippingOrder
    {
        $isDelivery = $shippingInput['is_delivery'] ?? false;
        $status = ShippingOrder::STATUS_NOT_REQUESTED;
        $pendingAt = null;
        if ($isDelivery) {
            $status = ShippingOrder::STATUS_PENDING;
            $pendingAt = now();
        }

        return ShippingOrder::create([
            'orden_lavanderia_id' => $orden->id,
            'is_delivery' => $isDelivery,
            'delivery_address' => $shippingInput['delivery_address'] ?? null,
            'delivery_status' => $status,
            'pending_at' => $pendingAt,
        ]);
    }

    public function avanzarStep(OrdenLavanderia $orden, ?int $operadorId = null): OrdenLavanderia
    {
        $stepAnterior = $orden->current_step;
        $nextStep = match ($stepAnterior) {
            OrdenLavanderia::STEP_RECEPCION => OrdenLavanderia::STEP_LAVADO,
            OrdenLavanderia::STEP_LAVADO => OrdenLavanderia::STEP_SECADO,
            OrdenLavanderia::STEP_SECADO => OrdenLavanderia::STEP_PLANCHADO,
            OrdenLavanderia::STEP_PLANCHADO => OrdenLavanderia::STEP_TERMINADO,
            default => $orden->current_step,
        };

        if ($stepAnterior === $nextStep) {
            LaundryAuditLogger::invalidStepTransition(
                $orden->id,
                $stepAnterior,
                $nextStep,
                [
                    'folio_fp' => LaundryAuditLogger::folioFingerprint($orden->folio_unico),
                    'user_id' => $operadorId,
                    'context' => 'avanzar_step_no_op',
                ]
            );
        }

        $updates = [
            'current_step' => $nextStep,
            'current_step_started_at' => now(),
        ];

        // Marcar fin de etapa anterior
        if ($stepAnterior === OrdenLavanderia::STEP_LAVADO) {
            $updates['lavado_finished_at'] = now();
        } elseif ($stepAnterior === OrdenLavanderia::STEP_SECADO) {
            $updates['secado_finished_at'] = now();
        } elseif ($stepAnterior === OrdenLavanderia::STEP_PLANCHADO) {
            $updates['planchado_finished_at'] = now();
        }

        // Inicio de etapa y asignación de operador (quien hace la acción)
        if ($nextStep === OrdenLavanderia::STEP_LAVADO && $orden->lavado_started_at === null) {
            $updates['lavado_started_at'] = now();
            $updates['estatus'] = OrdenLavanderia::ESTATUS_EN_PROCESO;
            $updates['operador_lavado_id'] = $operadorId ?? $orden->operador_lavado_id;
        } elseif ($nextStep === OrdenLavanderia::STEP_SECADO && $orden->secado_started_at === null) {
            $updates['secado_started_at'] = now();
            $updates['operador_secado_id'] = $operadorId ?? $orden->operador_secado_id;
        } elseif ($nextStep === OrdenLavanderia::STEP_PLANCHADO && $orden->planchado_started_at === null) {
            $updates['planchado_started_at'] = now();
            $updates['operador_planchado_id'] = $operadorId ?? $orden->operador_planchado_id;
        } elseif ($nextStep === OrdenLavanderia::STEP_TERMINADO && $orden->terminado_at === null) {
            $updates['terminado_at'] = now();
            $updates['estatus'] = OrdenLavanderia::ESTATUS_TERMINADO;
            event(new OrdenLavanderiaTerminada($orden));
        }

        $ordenActualizada = $this->ordenes->update($orden, $updates);

        OrdenEstadoLog::create([
            'orden_lavanderia_id' => $orden->id,
            'step_anterior' => $stepAnterior,
            'step_nuevo' => $nextStep,
            'user_id' => $operadorId,
        ]);

        return $ordenActualizada;
    }

    public function marcarEntregada(OrdenLavanderia $orden, ?int $entregadoPorId = null): OrdenLavanderia
    {
        $stepAnterior = $orden->current_step;
        $ordenActualizada = $this->ordenes->update($orden, [
            'current_step' => OrdenLavanderia::STEP_ENTREGADO,
            'current_step_started_at' => now(),
            'entregado_at' => now(),
            'estatus' => OrdenLavanderia::ESTATUS_ENTREGADO,
            'entregado_por_id' => $entregadoPorId ?? $orden->entregado_por_id,
        ]);

        OrdenEstadoLog::create([
            'orden_lavanderia_id' => $orden->id,
            'step_anterior' => $stepAnterior,
            'step_nuevo' => OrdenLavanderia::STEP_ENTREGADO,
            'user_id' => $entregadoPorId,
        ]);

        return $ordenActualizada;
    }

    /**
     * Cancela la orden: reversión de stock en productos físicos y egreso en caja si hubo pago.
     */
    public function cancelarOrden(OrdenLavanderia $orden, string $motivo, ?int $canceladoPorId = null): OrdenLavanderia
    {
        $stepAnterior = $orden->current_step;

        return DB::transaction(function () use ($orden, $motivo, $canceladoPorId, $stepAnterior): OrdenLavanderia {
            $orden->load(['detalles.producto', 'detalles.laundrySaleProduct']);

            foreach ($orden->detalles as $detalle) {
                if ($detalle->producto_id !== null) {
                    $producto = $detalle->producto;
                    if ($producto && ! $producto->es_servicio && $detalle->cantidad > 0) {
                        $producto->devolverStock((int) round((float) $detalle->cantidad, 0));
                    }
                } elseif ($detalle->laundry_sale_product_id !== null) {
                    $lp = $detalle->laundrySaleProduct;
                    if (
                        $lp
                        && $lp->billing_unit === LaundrySaleProduct::BILLING_UNIT_PIECE
                        && $detalle->billing_mode === OrdenLavanderiaDetalleLineValidator::BILLING_BY_GARMENT
                        && $lp->manejaInventarioPos()
                        && (float) $detalle->cantidad > 0
                    ) {
                        $lp->devolverStock((int) round((float) $detalle->cantidad, 0));
                    }
                }
            }

            $totalOrden = (float) ($orden->total ?? 0);
            if ($orden->caja_id && $totalOrden > 0) {
                CajaMovimiento::create([
                    'caja_id' => $orden->caja_id,
                    'user_id' => $canceladoPorId,
                    'tipo' => CajaMovimiento::TIPO_EGRESO,
                    'monto' => $totalOrden,
                    'motivo' => 'Devolución por cancelación - Folio: '.$orden->folio_unico.'. '.$motivo,
                    'venta_id' => $orden->id,
                ]);
            }

            $ordenActualizada = $this->ordenes->update($orden, [
                'estatus' => OrdenLavanderia::ESTATUS_CANCELADO,
                'cancelado_at' => now(),
                'cancelado_por_id' => $canceladoPorId,
                'motivo_cancelacion' => $motivo,
            ]);

            OrdenEstadoLog::create([
                'orden_lavanderia_id' => $orden->id,
                'step_anterior' => $stepAnterior,
                'step_nuevo' => 'cancelado',
                'user_id' => $canceladoPorId,
            ]);

            return $ordenActualizada;
        });
    }

    /**
     * @return array{precio_unitario: float, subtotal: float, cantidad: float, weight_kg: float|null}
     */
    private function resolveTibuLineMoney(array $item, int $index): array
    {
        return $this->laundryPricing->previewTibuLineMoney($item, $index);
    }

    /**
     * Cobro y cierre de recepción: solo cuando el POS envía finalizar=true.
     *
     * @param  array<int, array<string, mixed>>|null  $paymentsIn
     */
    private function aplicarCierreRecepcionSiFinaliza(
        OrdenLavanderia $orden,
        bool $finalizar,
        bool $hadPaymentsKey,
        ?array $paymentsIn,
        bool $hadMetodoKey,
        ?int $operadorId
    ): OrdenLavanderia {
        if (! $finalizar) {
            return $orden;
        }

        if (
            $orden->current_step !== OrdenLavanderia::STEP_RECEPCION
            || $orden->estatus !== OrdenLavanderia::ESTATUS_PENDIENTE
        ) {
            throw new \DomainException(
                'La orden ya no está en recepción; solo se puede cobrar y cerrar desde el POS mientras esté abierta.'
            );
        }

        if ($hadPaymentsKey && is_array($paymentsIn)) {
            $this->sincronizarPagosMultiples($orden, $paymentsIn, true);
        } elseif ($hadMetodoKey && ($orden->metodo_pago ?? null) && (float) $orden->total > 0) {
            $this->sincronizarPagoUnicoDesdeMetodoColumna($orden);
        } else {
            $this->sincronizarPagosMultiples($orden, [], true);
        }

        $orden->refresh();

        return $this->avanzarStep($orden, $operadorId);
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    private function sincronizarPagosMultiples(OrdenLavanderia $orden, array $rows, bool $finalizar): void
    {
        $orden->refresh();
        $total = round((float) ($orden->total ?? 0), 2);

        if ($rows === []) {
            OrdenLavanderiaPago::query()->where('orden_lavanderia_id', $orden->id)->delete();
            $orden->metodo_pago = null;
            $orden->save();
            if ($finalizar && $total > 0) {
                throw new DomainException('Debe registrar al menos un pago al finalizar la venta.');
            }

            return;
        }

        $allowed = ['efectivo', 'tarjeta', 'transferencia'];
        $sum = 0.0;
        foreach ($rows as $idx => $r) {
            $mp = $r['metodo_pago'] ?? null;
            if (! is_string($mp) || ! in_array($mp, $allowed, true)) {
                throw new DomainException('Método de pago inválido en línea '.($idx + 1).'.');
            }
            $m = round((float) ($r['monto'] ?? 0), 2);
            if ($m <= 0) {
                throw new DomainException('Cada pago debe ser mayor a cero.');
            }
            $sum += $m;
        }
        $sum = round($sum, 2);

        if ($total <= 0) {
            throw new DomainException('No se registran pagos si el total de la orden es cero.');
        }
        if (abs($sum - $total) > 0.02) {
            throw new DomainException('La suma de pagos ('.$sum.') debe coincidir con el total ('.$total.').');
        }

        OrdenLavanderiaPago::query()->where('orden_lavanderia_id', $orden->id)->delete();
        foreach ($rows as $r) {
            OrdenLavanderiaPago::create([
                'orden_lavanderia_id' => $orden->id,
                'metodo_pago' => (string) $r['metodo_pago'],
                'monto' => round((float) $r['monto'], 2),
            ]);
        }

        $unique = collect($rows)->pluck('metodo_pago')->unique()->values();
        $orden->metodo_pago = $unique->count() === 1 ? (string) $unique->first() : OrdenLavanderia::METODO_MIXTO;
        $orden->save();
    }

    private function sincronizarPagoUnicoDesdeMetodoColumna(OrdenLavanderia $orden): void
    {
        $orden->refresh();
        $total = round((float) ($orden->total ?? 0), 2);
        $mp = $orden->metodo_pago;
        OrdenLavanderiaPago::query()->where('orden_lavanderia_id', $orden->id)->delete();

        if ($total <= 0) {
            if ($mp !== null && $mp !== '') {
                $orden->metodo_pago = null;
                $orden->save();
            }

            return;
        }

        if (! is_string($mp) || $mp === '' || $mp === OrdenLavanderia::METODO_MIXTO) {
            return;
        }
        $allowed = ['efectivo', 'tarjeta', 'transferencia'];
        if (! in_array($mp, $allowed, true)) {
            return;
        }
        OrdenLavanderiaPago::create([
            'orden_lavanderia_id' => $orden->id,
            'metodo_pago' => $mp,
            'monto' => $total,
        ]);
    }

    /**
     * Bolsas de empaque con código de barras único por fila. Al bajar la cantidad se eliminan las de mayor índice.
     *
     * @return Collection<int, OrdenLavanderiaBolsa>
     */
    public function sincronizarBolsasPacking(OrdenLavanderia $orden, int $cantidad): Collection
    {
        if ($cantidad < 0 || $cantidad > 500) {
            throw new DomainException('La cantidad de bolsas debe estar entre 0 y 500.');
        }

        if (in_array($orden->estatus, [
            OrdenLavanderia::ESTATUS_CANCELADO,
            OrdenLavanderia::ESTATUS_ENTREGADO,
        ], true)) {
            throw new DomainException('No se pueden modificar bolsas en órdenes entregadas o canceladas.');
        }

        return DB::transaction(function () use ($orden, $cantidad): Collection {
            OrdenLavanderia::query()->whereKey($orden->id)->lockForUpdate()->firstOrFail();

            if ($cantidad === 0) {
                OrdenLavanderiaBolsa::query()->where('orden_lavanderia_id', $orden->id)->delete();

                return OrdenLavanderiaBolsa::query()->where('orden_lavanderia_id', $orden->id)->orderBy('indice')->get();
            }

            $n = (int) OrdenLavanderiaBolsa::query()->where('orden_lavanderia_id', $orden->id)->count();
            if ($cantidad < $n) {
                OrdenLavanderiaBolsa::query()
                    ->where('orden_lavanderia_id', $orden->id)
                    ->where('indice', '>', $cantidad)
                    ->delete();
            }

            $n = (int) OrdenLavanderiaBolsa::query()->where('orden_lavanderia_id', $orden->id)->count();
            for ($i = $n + 1; $i <= $cantidad; $i++) {
                OrdenLavanderiaBolsa::create([
                    'orden_lavanderia_id' => $orden->id,
                    'indice' => $i,
                    'codigo' => $this->generarCodigoBolsaUnico(),
                ]);
            }

            return OrdenLavanderiaBolsa::query()
                ->where('orden_lavanderia_id', $orden->id)
                ->orderBy('indice')
                ->get();
        });
    }

    private function generarCodigoBolsaUnico(): string
    {
        do {
            $codigo = \App\Support\LaundryScanCode::generateBagCode();
        } while (
            OrdenLavanderiaBolsa::query()->where('codigo', $codigo)->exists()
            || OrdenLavanderiaBolsa::query()
                ->whereRaw("REPLACE(REPLACE(codigo, '-', ''), ' ', '') = ?", [\App\Support\LaundryScanCode::compact($codigo)])
                ->exists()
        );

        return $codigo;
    }

    private function generarFolioUnico(): string
    {
        do {
            $folio = \App\Support\LaundryScanCode::generateFolio();
        } while ($this->ordenes->findByFolio($folio) !== null);

        return $folio;
    }

    private function generarCodigoQR(string $folioUnico): string
    {
        $qrData = [
            'folio' => $folioUnico,
            'timestamp' => now()->toIso8601String(),
        ];

        return base64_encode(json_encode($qrData));
    }

    private static function parseFechaEntregaInput(mixed $value): \Illuminate\Support\Carbon
    {
        if ($value !== null && $value !== '') {
            return \Illuminate\Support\Carbon::parse($value, config('app.timezone'));
        }

        return LaundrySemaphoreColorResolver::defaultFechaEntrega();
    }
}
