<?php

namespace App\Http\Controllers;

use App\Models\Cliente;
use App\Models\ConfiguracionEmpresa;
use App\Models\OrdenLavanderia;
use App\Models\OrdenLavanderiaBolsa;
use App\Models\Producto;
use App\Repositories\Contracts\OrdenLavanderiaRepositoryInterface;
use App\Services\OrdenLavanderiaService;
use App\Services\TicketService;
use App\Support\LaundryBagLabel;
use App\Support\LaundryScanInput;
use App\Support\LaundryScanResolver;
use App\Support\OrdenLavanderiaDetalleLineValidator;
use App\Support\SeguimientoBarcode;
use App\Support\TicketObservationLines;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use DomainException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class OrdenLavanderiaController extends Controller
{
    public function __construct(
        private readonly OrdenLavanderiaService $service,
        private readonly TicketService $ticketService,
        private readonly OrdenLavanderiaRepositoryInterface $ordenes
    ) {}

    public function index(Request $request): JsonResponse
    {
        $this->authorize('viewAny', OrdenLavanderia::class);

        $perPage = min(100, max(1, (int) $request->get('per_page', 20)));
        $filters = $request->only([
            'estatus',
            'current_step',
            'cliente_nombre',
            'folio_unico',
            'is_priority',
            'tipo_servicio_id',
            'caja_id',
            'recepcionista_id',
            'created_from',
            'created_to',
        ]);

        $result = $this->ordenes->paginate($filters, $perPage);

        return response()->json([
            'data' => $result,
            'meta' => null,
            'errors' => null,
        ], JsonResponse::HTTP_OK);
    }

    /**
     * Órdenes abiertas editables en POS (pendiente + recepción).
     */
    public function openOrders(Request $request): JsonResponse
    {
        $this->authorize('viewAny', OrdenLavanderia::class);

        $limit = min(100, max(1, (int) $request->get('limit', 50)));
        $filters = [];

        if ($request->filled('caja_id')) {
            $filters['caja_id'] = (int) $request->get('caja_id');
        }

        if ($request->boolean('mine')) {
            $filters['recepcionista_id'] = (int) Auth::id();
        }

        $ordenes = $this->ordenes->listAbiertasEnRecepcion($filters, $limit);

        return response()->json([
            'data' => $ordenes,
            'meta' => ['count' => $ordenes->count()],
            'errors' => null,
        ], JsonResponse::HTTP_OK);
    }

    /**
     * Export CSV del listado de órdenes con los mismos filtros que index (§25). Requiere reportes.ver.
     */
    public function exportCsv(Request $request): \Symfony\Component\HttpFoundation\StreamedResponse
    {
        $this->authorize('exportHistorialCsv', OrdenLavanderia::class);

        $filters = $request->only([
            'estatus',
            'current_step',
            'cliente_nombre',
            'folio_unico',
            'is_priority',
            'tipo_servicio_id',
            'caja_id',
            'created_from',
            'created_to',
        ]);

        $maxRows = max(1, (int) config('lavanderia.export_csv_max_rows', 10000));
        $filename = 'ordenes-historial-'.now()->format('Y-m-d_His').'.csv';

        return response()->streamDownload(function () use ($filters, $maxRows): void {
            $handle = fopen('php://output', 'w');
            if ($handle === false) {
                return;
            }
            fwrite($handle, "\xEF\xBB\xBF");
            fputcsv($handle, [
                'id',
                'folio_unico',
                'cliente_nombre',
                'created_at',
                'estatus',
                'total',
                'metodo_pago',
                'caja_id',
                'tipo_servicio_id',
                'priority_name',
                'is_priority',
                'laundry_sale_lines_count',
            ]);
            $count = 0;
            foreach (
                OrdenLavanderia::query()
                    ->with(['slaTipoServicio'])
                    ->withCount([
                        'detalles as laundry_sale_lines_count' => function ($q): void {
                            $q->whereNotNull('laundry_sale_product_id');
                        },
                    ])
                    ->historialFilters($filters)
                    ->orderByDesc('created_at')
                    ->lazy(500) as $orden
            ) {
                if ($count >= $maxRows) {
                    break;
                }
                $count++;
                fputcsv($handle, [
                    $orden->id,
                    $orden->folio_unico,
                    $orden->cliente_nombre,
                    $orden->created_at?->toIso8601String(),
                    $orden->estatus,
                    $orden->total,
                    $orden->metodo_pago,
                    $orden->caja_id,
                    $orden->tipo_servicio_id,
                    $orden->slaTipoServicio?->name,
                    $orden->is_priority ? '1' : '0',
                    (int) ($orden->laundry_sale_lines_count ?? 0),
                ]);
            }
            fclose($handle);
        }, $filename, [
            'Content-Type' => 'text/csv; charset=UTF-8',
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $this->authorize('create', OrdenLavanderia::class);

        $hasItems = ! empty($request->input('items')) && is_array($request->input('items'));

        if ($hasItems) {
            $validator = Validator::make($request->all(), [
                'cliente_id' => ['required', 'integer', 'exists:clientes,id'],
                'cliente_nombre' => ['required', 'string', 'max:255'],
                'cliente_email' => ['nullable', 'email', 'max:255'],
                'detalles_ropa' => ['nullable', 'string'],
                'instrucciones' => ['nullable', 'string'],
                'mostrar_observaciones_en_ticket' => ['sometimes', 'boolean'],
                'impresora_id' => ['nullable', 'integer', 'exists:impresoras,id'],
                'is_priority' => ['sometimes', 'boolean'],
                'tipo_servicio_id' => ['nullable', 'integer', 'exists:tipos_servicio,id'],
                'fecha_entrega' => ['nullable', 'date'],
                'semaphore_hours_red' => ['nullable', 'integer', 'min:0', 'max:168'],
                'semaphore_hours_yellow' => ['nullable', 'integer', 'min:0', 'max:168'],
                'metodo_pago' => ['nullable', 'in:efectivo,tarjeta,transferencia'],
                'payments' => ['nullable', 'array'],
                'payments.*.metodo_pago' => ['required', 'in:efectivo,tarjeta,transferencia'],
                'payments.*.monto' => ['required', 'numeric', 'min:0.01'],
                'finalizar' => ['sometimes', 'boolean'],
                'items' => ['required', 'array', 'min:1'],
                'items.*.producto_id' => ['nullable', 'integer', 'exists:productos,id'],
                'items.*.laundry_sale_product_id' => ['nullable', 'integer', 'exists:laundry_sale_products,id'],
                'items.*.etapa_id' => ['nullable', 'integer', 'exists:etapas,id'],
                'items.*.precio_unitario' => ['nullable', 'numeric', 'min:0'],
                'items.*.cantidad' => ['required', 'numeric', 'min:0.001'],
                'items.*.garment_type_id' => ['nullable', 'integer', 'exists:garment_types,id'],
                'items.*.load_type_id' => ['nullable', 'integer', 'exists:load_types,id'],
                'items.*.billing_mode' => ['nullable', 'in:by_garment_type,by_weight'],
                'items.*.weight_kg' => ['nullable', 'numeric', 'min:0'],
                'items.*.observations' => ['nullable', 'string', 'max:2000'],
                'items.*.extra_cost' => ['nullable', 'numeric', 'min:0'],
                'items.*.precio_unitario_override' => ['nullable', 'numeric', 'min:0'],
                'items.*.is_informational' => ['nullable', 'boolean'],
                'is_delivery' => ['sometimes', 'boolean'],
                'delivery_address' => ['nullable', 'string', 'max:2000'],
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'errors' => $validator->errors(),
                ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
            }

            $itemsIn = $request->input('items');
            foreach ($itemsIn as $idx => $item) {
                if (OrdenLavanderiaDetalleLineValidator::isInformationalItem($item)) {
                    continue;
                }
                $hasProducto = ! empty($item['producto_id']);
                $hasSaleProduct = ! empty($item['laundry_sale_product_id']);
                if ($hasProducto && $hasSaleProduct) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.laundry_sale_product_id" => ['No combine producto_id (legacy) con laundry_sale_product_id.'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if ($hasProducto && ! empty($item['etapa_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.producto_id" => ['No combine producto_id con etapa_id en la misma línea. Use catálogo productos O línea solo lavandería TIBU.'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if ($hasSaleProduct && ! empty($item['garment_type_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.garment_type_id" => ['No combine garment_type_id con laundry_sale_product_id (catálogo POS).'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if (! $hasProducto && ! $hasSaleProduct && empty($item['etapa_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.producto_id" => ['Cada línea debe incluir producto_id (legacy), laundry_sale_product_id (catálogo POS) o etapa_id (TIBU por prenda).'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
            }

            OrdenLavanderiaDetalleLineValidator::validateItems($itemsIn);

            $idsProducto = collect($itemsIn)->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();
            foreach ($itemsIn as $idx => $item) {
                if (empty($item['producto_id'])) {
                    continue;
                }
                $p = $productos->firstWhere('id', (int) $item['producto_id']);
                if ($p && ! $p->es_servicio && $p->stock < (int) round((float) $item['cantidad'], 0)) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.cantidad" => ["Stock insuficiente para {$p->nombre}. Disponible: {$p->stock}."],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
            }

            $data = $this->mergeClienteCatalogoEnPayload($validator->validated());
            $items = $data['items'];
            unset($data['items']);
            $cajaAbierta = $request->attributes->get('caja_abierta');
            if ($cajaAbierta) {
                $data['caja_id'] = $cajaAbierta->id;
            }
            $finalizar = (bool) ($data['finalizar'] ?? false);
            try {
                $orden = $this->service->crearOrdenDesdeCarrito($data, $items, Auth::id());
            } catch (DomainException $e) {
                return response()->json([
                    'errors' => ['orden' => [$e->getMessage()]],
                ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
            }

            return $this->respuestaOrdenConImpresion($orden, $finalizar, $request);
        } else {
            $validator = Validator::make($request->all(), [
                'cliente_id' => ['required', 'integer', 'exists:clientes,id'],
                'cliente_nombre' => ['required', 'string', 'max:255'],
                'cliente_email' => ['nullable', 'email', 'max:255'],
                'tipo_servicio' => ['nullable', 'string', 'max:150'],
                'detalles_ropa' => ['nullable', 'string'],
                'instrucciones' => ['nullable', 'string'],
                'mostrar_observaciones_en_ticket' => ['sometimes', 'boolean'],
                'impresora_id' => ['nullable', 'integer', 'exists:impresoras,id'],
                'total' => ['nullable', 'numeric', 'min:0'],
                'is_priority' => ['sometimes', 'boolean'],
                'tipo_servicio_id' => ['nullable', 'integer', 'exists:tipos_servicio,id'],
                'fecha_entrega' => ['nullable', 'date'],
                'semaphore_hours_red' => ['nullable', 'integer', 'min:0', 'max:168'],
                'semaphore_hours_yellow' => ['nullable', 'integer', 'min:0', 'max:168'],
                'metodo_pago' => ['nullable', 'in:efectivo,tarjeta,transferencia'],
                'payments' => ['nullable', 'array'],
                'payments.*.metodo_pago' => ['required', 'in:efectivo,tarjeta,transferencia'],
                'payments.*.monto' => ['required', 'numeric', 'min:0.01'],
                'finalizar' => ['sometimes', 'boolean'],
                'is_delivery' => ['sometimes', 'boolean'],
                'delivery_address' => ['nullable', 'string', 'max:2000'],
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'errors' => $validator->errors(),
                ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
            }

            $data = $this->mergeClienteCatalogoEnPayload($validator->validated());
            $cajaAbierta = $request->attributes->get('caja_abierta');
            if ($cajaAbierta) {
                $data['caja_id'] = $cajaAbierta->id;
            }
            $finalizar = (bool) ($data['finalizar'] ?? false);
            try {
                $orden = $this->service->crearOrden($data, Auth::id());
            } catch (DomainException $e) {
                return response()->json([
                    'errors' => ['orden' => [$e->getMessage()]],
                ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
            }

            return $this->respuestaOrdenConImpresion($orden, $finalizar, $request);
        }
    }

    public function show(OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('view', $orden);

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

        return response()->json([
            'data' => $orden,
        ], JsonResponse::HTTP_OK);
    }

    public function putBolsas(Request $request, OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('update', $orden);

        $validator = Validator::make($request->all(), [
            'cantidad_bolsas' => ['required', 'integer', 'min:0', 'max:500'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        try {
            $this->service->sincronizarBolsasPacking($orden, (int) $request->input('cantidad_bolsas'));
        } catch (DomainException $e) {
            return response()->json([
                'errors' => ['orden' => [$e->getMessage()]],
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

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

        return response()->json([
            'data' => $orden,
        ], JsonResponse::HTTP_OK);
    }

    /**
     * PDF de etiquetas por bolsa (código de barras único). Query opcional: indice=N para una sola etiqueta.
     */
    public function bolsasPdf(Request $request, OrdenLavanderia $orden): Response|JsonResponse
    {
        $this->authorize('ticketPdf', $orden);

        $validator = Validator::make($request->all(), [
            'indice' => ['nullable', 'integer', 'min:1', 'max:500'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $orden->load(['bolsas', 'slaTipoServicio', 'cliente']);
        $all = $orden->bolsas->sortBy('indice')->values();
        if ($all->isEmpty()) {
            return response()->json([
                'message' => 'No hay bolsas. Indique la cantidad y guarde primero.',
            ], JsonResponse::HTTP_NOT_FOUND);
        }

        $total = $all->count();
        $bolsas = $all;
        if ($request->filled('indice')) {
            $idx = (int) $request->input('indice');
            $bolsas = $all->filter(fn (OrdenLavanderiaBolsa $b): bool => (int) $b->indice === $idx)->values();
            if ($bolsas->isEmpty()) {
                return response()->json([
                    'message' => 'No existe la bolsa indicada para esta orden.',
                ], JsonResponse::HTTP_NOT_FOUND);
            }
        }

        $folioBarcode = LaundryScanResolver::barcodePayloadForOrder($orden);
        $etiquetas = [];
        foreach ($bolsas as $b) {
            $etiquetas[] = [
                'indice' => (int) $b->indice,
                'total_bolsas' => $total,
                'codigo' => $b->codigo,
                'barcode_display' => $folioBarcode,
                'barcode_data_uri' => $folioBarcode !== ''
                    ? SeguimientoBarcode::pngDataUri($folioBarcode)
                    : null,
            ];
        }

        try {
            $labelMeta = LaundryBagLabel::metaDesdeOrden($orden);
            $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.bolsas_etiquetas', [
                'orden' => $orden,
                'etiquetas' => $etiquetas,
                'labelMeta' => $labelMeta,
            ]);
            $pdf->setPaper([0, 0, 227.56, 400], 'portrait');
            $filename = 'bolsas-'.$orden->folio_unico.'.pdf';

            return $pdf->stream($filename);
        } catch (\Throwable $e) {
            return response()->json([
                'message' => 'No se pudo generar el PDF: '.$e->getMessage(),
            ], 500);
        }
    }

    public function update(Request $request, OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('update', $orden);

        $validator = Validator::make($request->all(), [
            'cliente_id' => [
                Rule::requiredIf(fn (): bool => $request->has('items') || $request->has('finalizar') || $request->has('payments')),
                'integer',
                'exists:clientes,id',
            ],
            'cliente_nombre' => ['sometimes', 'required', 'string', 'max:255'],
            'cliente_email' => ['nullable', 'email', 'max:255'],
            'detalles_ropa' => ['nullable', 'string'],
            'instrucciones' => ['nullable', 'string'],
            'mostrar_observaciones_en_ticket' => ['sometimes', 'boolean'],
            'impresora_id' => ['nullable', 'integer', 'exists:impresoras,id'],
            'is_priority' => ['sometimes', 'boolean'],
            'tipo_servicio_id' => ['nullable', 'integer', 'exists:tipos_servicio,id'],
            'fecha_entrega' => ['nullable', 'date'],
            'semaphore_hours_red' => ['nullable', 'integer', 'min:0', 'max:168'],
            'semaphore_hours_yellow' => ['nullable', 'integer', 'min:0', 'max:168'],
            'metodo_pago' => ['nullable', 'in:efectivo,tarjeta,transferencia'],
            'is_delivery' => ['sometimes', 'boolean'],
            'delivery_address' => ['nullable', 'string', 'max:2000'],
            'items' => ['sometimes', 'array'],
            'items.*.producto_id' => ['nullable', 'integer', 'exists:productos,id'],
            'items.*.laundry_sale_product_id' => ['nullable', 'integer', 'exists:laundry_sale_products,id'],
            'items.*.etapa_id' => ['nullable', 'integer', 'exists:etapas,id'],
            'items.*.precio_unitario' => ['nullable', 'numeric', 'min:0'],
            'items.*.cantidad' => ['required_with:items', 'numeric', 'min:0.001'],
            'items.*.garment_type_id' => ['nullable', 'integer', 'exists:garment_types,id'],
            'items.*.load_type_id' => ['nullable', 'integer', 'exists:load_types,id'],
            'items.*.billing_mode' => ['nullable', 'in:by_garment_type,by_weight'],
            'items.*.weight_kg' => ['nullable', 'numeric', 'min:0'],
            'items.*.observations' => ['nullable', 'string', 'max:2000'],
            'items.*.extra_cost' => ['nullable', 'numeric', 'min:0'],
            'items.*.precio_unitario_override' => ['nullable', 'numeric', 'min:0'],
            'items.*.is_informational' => ['nullable', 'boolean'],
            'payments' => ['nullable', 'array'],
            'payments.*.metodo_pago' => ['required', 'in:efectivo,tarjeta,transferencia'],
            'payments.*.monto' => ['required', 'numeric', 'min:0.01'],
            'finalizar' => ['sometimes', 'boolean'],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $data = $this->mergeClienteCatalogoEnPayload($validator->validated());
        $items = null;
        if ($request->exists('items')) {
            $items = $request->input('items');
            foreach ($items as $idx => $item) {
                if (OrdenLavanderiaDetalleLineValidator::isInformationalItem($item)) {
                    continue;
                }
                $hasProducto = ! empty($item['producto_id']);
                $hasSaleProduct = ! empty($item['laundry_sale_product_id']);
                if ($hasProducto && $hasSaleProduct) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.laundry_sale_product_id" => ['No combine producto_id (legacy) con laundry_sale_product_id.'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if ($hasProducto && ! empty($item['etapa_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.producto_id" => ['No combine producto_id con etapa_id en la misma línea. Use catálogo productos O línea solo lavandería TIBU.'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if ($hasSaleProduct && ! empty($item['garment_type_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.garment_type_id" => ['No combine garment_type_id con laundry_sale_product_id (catálogo POS).'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
                if (! $hasProducto && ! $hasSaleProduct && empty($item['etapa_id'])) {
                    return response()->json([
                        'errors' => [
                            "items.{$idx}.producto_id" => ['Cada línea debe incluir producto_id (legacy), laundry_sale_product_id (catálogo POS) o etapa_id (TIBU por prenda).'],
                        ],
                    ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
                }
            }
            OrdenLavanderiaDetalleLineValidator::validateItems($items);
            unset($data['items']);
        }

        $finalizar = (bool) ($data['finalizar'] ?? false);
        try {
            $updated = $this->service->actualizarOrdenEnRecepcion($orden, $data, $items, Auth::id());
        } catch (DomainException $e) {
            return response()->json([
                'errors' => ['orden' => [$e->getMessage()]],
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        return $this->respuestaOrdenConImpresion($updated, $finalizar, $request, JsonResponse::HTTP_OK);
    }

    /**
     * Reimprime ticket de venta en impresora térmica ESC/POS.
     */
    public function reimprimirTicket(Request $request, OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('ticketPdf', $orden);

        $validator = Validator::make($request->all(), [
            'impresora_id' => ['nullable', 'integer', 'exists:impresoras,id'],
            'pos_reception' => ['sometimes', 'boolean'],
        ]);
        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $impresoraId = $request->input('impresora_id');
        $resultado = $this->ticketService->imprimirTicket(
            $orden,
            $impresoraId !== null ? (int) $impresoraId : null,
            $request->user(),
            $request->boolean('pos_reception')
        );

        return response()->json([
            'success' => (bool) ($resultado['success'] ?? false),
            'message' => $resultado['message'] ?? ($resultado['error'] ?? null),
            'impresion' => $resultado,
        ], ($resultado['success'] ?? false) ? JsonResponse::HTTP_OK : JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
    }

    /**
     * Imprime etiqueta de bolsa (cliente + código de barras) directo en impresora térmica ESC/POS.
     */
    public function imprimirEtiquetaBolsa(Request $request, OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('ticketPdf', $orden);

        $validator = Validator::make($request->all(), [
            'indice' => ['nullable', 'integer', 'min:1', 'max:500'],
            'impresora_id' => ['nullable', 'integer', 'exists:impresoras,id'],
            'pos_reception' => ['sometimes', 'boolean'],
            'todas' => ['sometimes', 'boolean'],
        ]);
        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $indice = $request->filled('indice') ? (int) $request->input('indice') : null;
        if (! $request->boolean('todas') && $indice === null) {
            $indice = 1;
        }

        $impresoraId = $request->input('impresora_id');
        $resultado = $this->ticketService->imprimirEtiquetaBolsa(
            $orden,
            $request->boolean('todas') ? null : $indice,
            $impresoraId !== null ? (int) $impresoraId : null,
            $request->boolean('pos_reception')
        );

        return response()->json([
            'success' => (bool) ($resultado['success'] ?? false),
            'message' => $resultado['message'] ?? ($resultado['error'] ?? null),
            'impresion' => $resultado,
        ], ($resultado['success'] ?? false) ? JsonResponse::HTTP_OK : JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
    }

    /**
     * Ticket PDF optimizado para impresora térmica 80mm (logo, datos fiscales, folio, ítems, QR seguimiento, políticas).
     */
    public function ticketPdf(OrdenLavanderia $orden): Response|JsonResponse
    {
        $this->authorize('ticketPdf', $orden);

        $orden->load([
            'detalles.producto',
            'detalles.etapa',
            'detalles.garmentType',
            'detalles.laundrySaleProduct',
            'detalles.loadType',
            'slaTipoServicio',
            'shippingOrder',
            'recepcionista',
            'pagos',
        ]);
        $config = ConfiguracionEmpresa::obtenerConfiguracion();
        $appUrl = rtrim(config('app.url'), '/');
        $urlSeguimiento = $appUrl.'/seguimiento/'.urlencode($orden->folio_unico);

        $ticketObservations = TicketObservationLines::forOrder($orden, $orden->detalles);
        $barcodePayload = \App\Support\LaundryScanResolver::barcodePayloadForOrder($orden);
        $barcodeSeguimientoDataUri = $barcodePayload !== ''
            ? SeguimientoBarcode::pngDataUri($barcodePayload)
            : '';

        $data = [
            'orden' => $orden,
            'config' => $config,
            'urlSeguimiento' => $urlSeguimiento,
            'barcodeSeguimientoDataUri' => $barcodeSeguimientoDataUri,
            'barcodeSeguimientoPayload' => $barcodePayload,
            'detalles' => $orden->detalles,
            'ticketObservations' => $ticketObservations,
            'ticketObsTitlePrenda' => TicketObservationLines::TITLE_OBSERVACIONES_POR_PRENDA,
        ];

        try {
            $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.ticket_termico_80mm', $data);
            $pdf->setPaper([0, 0, 227.56, 841], 'portrait'); // 80mm width, ~297mm height
            $filename = 'ticket-'.$orden->folio_unico.'.pdf';

            return $pdf->stream($filename);
        } catch (\Throwable $e) {
            return response()->json([
                'message' => 'No se pudo generar el PDF: '.$e->getMessage(),
            ], 500);
        }
    }

    public function avanzar(OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('advance', $orden);

        $ordenActualizada = $this->service->avanzarStep($orden, Auth::id());

        return response()->json([
            'data' => $ordenActualizada,
        ], JsonResponse::HTTP_OK);
    }

    public function marcarEntregada(OrdenLavanderia $orden): JsonResponse
    {
        $ordenActualizada = $this->service->marcarEntregada($orden, Auth::id());

        return response()->json([
            'data' => $ordenActualizada,
        ], JsonResponse::HTTP_OK);
    }

    public function cancelar(Request $request, OrdenLavanderia $orden): JsonResponse
    {
        $this->authorize('cancel', $orden);

        $validator = Validator::make($request->all(), [
            'motivo_cancelacion' => ['required', 'string', 'max:1000'],
        ], [
            'motivo_cancelacion.required' => 'El motivo de cancelación es obligatorio.',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        if (in_array($orden->estatus, [
            OrdenLavanderia::ESTATUS_TERMINADO,
            OrdenLavanderia::ESTATUS_ENTREGADO,
            OrdenLavanderia::ESTATUS_CANCELADO,
        ])) {
            return response()->json([
                'errors' => ['orden' => ['No se puede cancelar una orden ya terminada, entregada o cancelada.']],
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $ordenActualizada = $this->service->cancelarOrden(
            $orden,
            $request->input('motivo_cancelacion'),
            Auth::id()
        );

        return response()->json([
            'data' => $ordenActualizada,
        ], JsonResponse::HTTP_OK);
    }

    /**
     * Endpoint para dashboard tipo semáforo.
     */
    public function dashboard(): JsonResponse
    {
        $this->authorize('dashboard', OrdenLavanderia::class);

        $ordenes = $this->ordenes->getPendientesEnProceso();

        $now = now();

        $data = $ordenes->map(function (OrdenLavanderia $orden) use ($now) {
            $startedAt = $orden->current_step_started_at ?? $orden->created_at;
            $diffMinutes = $startedAt ? $startedAt->diffInMinutes($now) : 0;
            $promisedAt = \App\Support\LaundrySemaphoreColorResolver::promisedAt($orden);
            $horasParaEntrega = round($now->floatDiffInHours($promisedAt, false), 2);

            return [
                'id' => $orden->id,
                'folio_unico' => $orden->folio_unico,
                'cliente_nombre' => $orden->cliente_nombre,
                'tipo_servicio' => $orden->tipo_servicio,
                'detalles_ropa' => $orden->detalles_ropa,
                'instrucciones' => $orden->instrucciones,
                'is_priority' => $orden->is_priority,
                'current_step' => $orden->current_step,
                'estatus' => $orden->estatus,
                'current_step_started_at' => $orden->current_step_started_at,
                'fecha_entrega' => $orden->fecha_entrega ?? $promisedAt,
                'minutos_en_etapa' => $diffMinutes,
                'horas_para_entrega' => $horasParaEntrega,
                'color' => \App\Support\LaundrySemaphoreColorResolver::resolveColor($orden, $now),
            ];
        });

        return response()->json([
            'data' => $data,
        ], JsonResponse::HTTP_OK);
    }

    /**
     * Escanear QR o folio para obtener orden (para operadores).
     */
    public function escanearQR(Request $request): JsonResponse
    {
        $this->authorize('escanearQR', OrdenLavanderia::class);

        $validator = Validator::make($request->all(), [
            'codigo_qr' => ['required_without:folio_unico', 'string'],
            'folio_unico' => ['required_without:codigo_qr', 'string'],
        ], [
            'codigo_qr.required_without' => 'Debe proporcionar el código QR o folio.',
            'folio_unico.required_without' => 'Debe proporcionar el código QR o folio.',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'errors' => $validator->errors(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        $raw = (string) ($request->input('codigo_qr') ?? $request->input('folio_unico'));
        $orden = \App\Support\LaundryScanResolver::findOrden($raw);
        $bolsa = null;
        if ($orden !== null) {
            $normalized = LaundryScanInput::normalize($raw);
            $bolsa = OrdenLavanderiaBolsa::query()
                ->where('codigo', $normalized)
                ->where('orden_lavanderia_id', $orden->id)
                ->first();
        }

        if (! $orden) {
            return response()->json([
                'errors' => ['orden' => ['Orden no encontrada.']],
                'encontrada' => false,
            ], JsonResponse::HTTP_NOT_FOUND);
        }

        if (in_array($orden->estatus, [
            OrdenLavanderia::ESTATUS_TERMINADO,
            OrdenLavanderia::ESTATUS_ENTREGADO,
            OrdenLavanderia::ESTATUS_CANCELADO,
        ])) {
            return response()->json([
                'errors' => ['orden' => ['La orden ya está terminada, entregada o cancelada.']],
                'encontrada' => false,
                'estatus' => $orden->estatus,
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
        }

        return response()->json([
            'data' => $orden,
            'encontrada' => true,
            'bolsa' => $bolsa !== null
                ? [
                    'id' => $bolsa->id,
                    'indice' => $bolsa->indice,
                    'codigo' => $bolsa->codigo,
                ]
                : null,
        ], JsonResponse::HTTP_OK);
    }

    private function respuestaOrdenConImpresion(
        OrdenLavanderia $orden,
        bool $finalizar,
        Request $request,
        int $status = JsonResponse::HTTP_CREATED
    ): JsonResponse {
        $payload = [
            'data' => $orden,
            'meta' => null,
            'errors' => null,
        ];

        if ($finalizar) {
            $payload['impresion_venta'] = $this->intentarImpresionTicket($orden, $request);
        }

        return response()->json($payload, $status);
    }

    /**
     * @return array{success: bool, message?: string, error?: string}
     */
    private function intentarImpresionTicket(OrdenLavanderia $orden, Request $request): array
    {
        try {
            $impresoraId = $request->input('impresora_id');

            return $this->ticketService->imprimirTicket(
                $orden,
                $impresoraId !== null && $impresoraId !== '' ? (int) $impresoraId : null,
                $request->user(),
                true
            );
        } catch (\Throwable $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }

    /**
     * Alinea nombre/correo de la orden con el catálogo cuando el POS envía cliente_id.
     *
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function mergeClienteCatalogoEnPayload(array $data): array
    {
        if (empty($data['cliente_id'])) {
            return $data;
        }

        $cliente = Cliente::query()->find((int) $data['cliente_id']);
        if ($cliente === null) {
            return $data;
        }

        $data['cliente_nombre'] = $cliente->nombre;
        if ($cliente->email) {
            $data['cliente_email'] = $cliente->email;
        }

        return $data;
    }
}
