<?php

namespace App\Http\Controllers;

use App\Models\AperturaCaja;
use App\Models\ConfiguracionEmpresa;
use App\Models\Cliente;
use App\Models\Devolucion;
use App\Models\Factura;
use App\Models\FacturaDetalle;
use App\Models\MetodoPago;
use App\Models\MovimientoCaja;
use App\Models\Producto;
use App\Models\Venta;
use App\Models\VentaDetalle;
use App\Models\VentaPago;
use App\Services\EnvioService;
use App\Services\InventoryFlowService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class PosController extends Controller
{
    private const POS_SALE_MOVEMENT_PREFIX = 'Venta POS #';

    private const POS_SALE_REVERSAL_PREFIX = 'Anulacion venta POS #';

    public function __construct(
        private readonly InventoryFlowService $inventoryFlowService,
        private readonly EnvioService $envioService,
    ) {}


    public function index(): View
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $userId = (int) (auth_user()?->id ?? 0);
        $tenantSucursalId = tenant_sucursal_id();

        $opening = AperturaCaja::query()
            ->where('empresa_id', $empresaId)
            ->where('usuario_id', $userId)
            ->where('estado', 'abierta')
            ->with(['caja.sucursal'])
            ->orderByDesc('id')
            ->first();

        $hasProductVariantsTable = Schema::hasTable('producto_variantes');

        $productsQuery = Producto::query()
            ->where('empresa_id', $empresaId)
            ->where('activo', 1)
            ->when($tenantSucursalId, static function ($query, int $sucursalId): void {
                $query->where(static function ($nested) use ($sucursalId): void {
                    $nested->whereNull('sucursal_id')
                        ->orWhere('sucursal_id', $sucursalId);
                });
            })
            ->with(['categoria:id,nombre']);

        if ($hasProductVariantsTable) {
            $productsQuery->with([
                'variantes' => static function ($query): void {
                    $query->select([
                        'id',
                        'empresa_id',
                        'producto_id',
                        'nombre',
                        'precio_venta',
                        'cantidad',
                        'activo',
                    ])
                        ->orderBy('nombre');
                },
            ]);
        }

        $products = $productsQuery
            ->orderBy('nombre')
            ->limit(150)
            ->get([
                'id',
                'empresa_id',
                'categoria_id',
                'nombre',
                'sku',
                'codigo_barras',
                'imagen',
                'precio_venta',
                'stock_actual',
                'impuesto_porcentaje',
            ]);

        $productsPayload = [];
        $productVariantsMap = [];

        foreach ($products as $product) {
            $productsPayload[] = [
                'id' => (int) $product->id,
                'nombre' => (string) $product->nombre,
                'sku' => (string) ($product->sku ?? ''),
                'codigo_barras' => (string) ($product->codigo_barras ?? ''),
                'categoria' => (string) ($product->categoria?->nombre ?? ''),
                'precio_venta' => (float) $product->precio_venta,
                'stock_actual' => (float) $product->stock_actual,
                'impuesto_porcentaje' => (float) ($product->impuesto_porcentaje ?? 0),
                'imagen' => (string) ($product->imagen ?? ''),
            ];

            $variants = [];
            $variantRows = $hasProductVariantsTable
                ? $product->variantes
                : collect();
            foreach ($variantRows as $variant) {
                if ((int) ($variant->activo ?? 0) !== 1) {
                    continue;
                }

                $variants[] = [
                    'id' => (int) $variant->id,
                    'nombre' => (string) $variant->nombre,
                    'precio_venta' => (float) ($variant->precio_venta ?? 0),
                    'cantidad' => $variant->cantidad === null ? null : (int) $variant->cantidad,
                    'activo' => (int) ($variant->activo ?? 0),
                ];
            }

            if ($variants !== []) {
                $productVariantsMap[(int) $product->id] = $variants;
            }
        }

        $customers = Cliente::query()
            ->where('empresa_id', $empresaId)
            ->where('activo', 1)
            ->orderBy('nombre')
            ->limit(300)
            ->get(['id', 'nombre', 'documento', 'telefono', 'email']);

        $customersPayload = $customers->map(static fn (Cliente $customer): array => [
            'id' => (int) $customer->id,
            'nombre' => (string) $customer->nombre,
            'documento' => (string) ($customer->documento ?? ''),
            'telefono' => (string) ($customer->telefono ?? ''),
            'email' => (string) ($customer->email ?? ''),
        ])->values()->all();

        $paymentMethods = MetodoPago::query()
            ->where('activo', 1)
            ->where(static function ($query) use ($empresaId): void {
                $query->where('empresa_id', $empresaId)
                    ->orWhereNull('empresa_id');
            })
            ->orderByRaw('CASE WHEN empresa_id IS NULL THEN 1 ELSE 0 END')
            ->orderBy('nombre')
            ->get(['empresa_id', 'codigo', 'nombre']);

        if ($paymentMethods->isEmpty()) {
            $paymentMethods = collect([
                ['codigo' => 'efectivo', 'nombre' => 'Efectivo'],
                ['codigo' => 'tarjeta', 'nombre' => 'Tarjeta'],
                ['codigo' => 'transferencia', 'nombre' => 'Transferencia'],
            ]);
        }

        $paymentMethodsPayload = $paymentMethods
            ->map(static function ($method): array {
                if (is_array($method)) {
                    return [
                        'codigo' => strtolower(trim((string) ($method['codigo'] ?? 'efectivo'))),
                        'nombre' => trim((string) ($method['nombre'] ?? 'Metodo')),
                    ];
                }

                return [
                    'codigo' => strtolower(trim((string) ($method->codigo ?? 'efectivo'))),
                    'nombre' => trim((string) ($method->nombre ?? 'Metodo')),
                ];
            })
            ->unique('codigo')
            ->values()
            ->all();

        $recentSales = Venta::query()
            ->where('empresa_id', $empresaId)
            ->when($tenantSucursalId, static function ($query, int $sucursalId): void {
                $query->where(static function ($nested) use ($sucursalId): void {
                    $nested->whereNull('sucursal_id')
                        ->orWhere('sucursal_id', $sucursalId);
                });
            })
            ->with(['cliente', 'usuario', 'pagos'])
            ->orderByDesc('id')
            ->limit(20)
            ->get();

        $posEnabled = $opening !== null;
        $posBlockReason = '';
        if (! $posEnabled) {
            $posBlockReason = 'Debes abrir caja para registrar ventas en POS.';
        }

        $autoPosEnabled = ConfiguracionEmpresa::query()
            ->where('empresa_id', $empresaId)
            ->where('clave', 'envios.auto_pos')
            ->value('valor') === '1';

        return view('modules.pos.index', [
            'opening' => $opening?->toArray(),
            'products' => $productsPayload,
            'product_variants_map' => $productVariantsMap,
            'customers' => $customersPayload,
            'payment_methods' => $paymentMethodsPayload,
            'user_branch_id' => $tenantSucursalId,
            'invoice_range_alert' => null,
            'pos_enabled' => $posEnabled,
            'pos_block_reason' => $posBlockReason,
            'recentSales' => $recentSales,
            'canViewSaleTickets' => has_permission('ventas.ver'),
            'canAnnulSales' => has_permission('ventas.anular'),
            'paymentMethods' => $paymentMethods,
            'posEnabled' => $posEnabled,
            'posBlockReason' => $posBlockReason,
            'autoPosEnabled' => $autoPosEnabled,
        ]);
    }

    public function sell(Request $request): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $usuarioId = (int) (auth_user()?->id ?? 0);
        abort_if($usuarioId <= 0, 403, 'Usuario invalido para registrar venta.');

        $tenantSucursalId = tenant_sucursal_id();

        $validated = $request->validate([
            'cliente_id' => ['nullable', 'integer'],
            'generar_envio' => ['nullable', 'boolean'],
            'tipo_venta' => ['nullable', 'string', 'in:contado,credito'],
            'metodo_pago' => ['required', 'string', 'max:50'],
            'descuento' => ['nullable', 'numeric', 'min:0'],
            'referencia_pago' => ['nullable', 'string', 'max:120'],
            'pago_cliente' => ['nullable', 'numeric', 'min:0'],
            'abono_inicial' => ['nullable', 'numeric', 'min:0'],
            'lineas' => ['nullable', 'array', 'min:1'],
            'lineas.*.producto_id' => ['required_with:lineas', 'integer'],
            'lineas.*.cantidad' => ['required_with:lineas', 'numeric', 'gt:0'],
            'lineas.*.precio_unitario' => ['nullable', 'numeric', 'gt:0'],
            'lineas.*.descuento_linea' => ['nullable', 'numeric', 'min:0'],
            'lineas.*.descuento_linea_tipo' => ['nullable', 'string', 'in:valor,porcentaje'],
            'producto_id' => ['nullable', 'array'],
            'producto_id.*' => ['nullable', 'integer'],
            'cantidad' => ['nullable', 'array'],
            'cantidad.*' => ['nullable', 'numeric', 'gt:0'],
            'precio_unitario' => ['nullable', 'array'],
            'precio_unitario.*' => ['nullable', 'numeric', 'gt:0'],
            'descuento_linea' => ['nullable', 'array'],
            'descuento_linea.*' => ['nullable', 'numeric', 'min:0'],
            'descuento_linea_tipo' => ['nullable', 'array'],
            'descuento_linea_tipo.*' => ['nullable', 'string', 'in:valor,porcentaje'],
        ]);

        $lineas = $this->normalizeSaleLines($validated);
        $descuentoGlobal = (float) ($validated['descuento'] ?? 0);
        $tipoVenta = strtolower(trim((string) ($validated['tipo_venta'] ?? 'contado')));
        if (! in_array($tipoVenta, ['contado', 'credito'], true)) {
            $tipoVenta = 'contado';
        }

        $clienteId = isset($validated['cliente_id']) && is_numeric($validated['cliente_id'])
            ? (int) $validated['cliente_id']
            : null;

        if ($clienteId) {
            $clienteExiste = Cliente::query()
                ->whereKey($clienteId)
                ->where('empresa_id', $empresaId)
                ->where('activo', 1)
                ->exists();

            if (! $clienteExiste) {
                throw ValidationException::withMessages([
                    'cliente_id' => 'El cliente seleccionado no pertenece a la empresa activa.',
                ]);
            }
        }

        if ($tipoVenta === 'credito' && ! $clienteId) {
            throw ValidationException::withMessages([
                'cliente_id' => 'Debes seleccionar un cliente para registrar una venta a credito.',
            ]);
        }

        $productoIds = $lineas
            ->pluck('producto_id')
            ->filter(static fn ($id): bool => is_numeric($id))
            ->map(static fn ($id): int => (int) $id)
            ->unique()
            ->values()
            ->all();

        $ventaIdPosted = 0;
        $saleTotalPosted = 0.0;
        $salePagoClientePosted = 0.0;
        $saleCambioPosted = 0.0;
        $saleMetodoPagoPosted = '';

        DB::transaction(function () use (
            $empresaId,
            $usuarioId,
            $tenantSucursalId,
            $clienteId,
            $validated,
            $descuentoGlobal,
            $lineas,
            $productoIds,
            &$ventaIdPosted,
            &$saleTotalPosted,
            &$salePagoClientePosted,
            &$saleCambioPosted,
            &$saleMetodoPagoPosted,
            $tipoVenta
        ): void {
            $apertura = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('usuario_id', $usuarioId)
                ->where('estado', 'abierta')
                ->lockForUpdate()
                ->orderByDesc('id')
                ->first();

            if (! $apertura) {
                throw ValidationException::withMessages([
                    'pos' => 'Debes abrir caja antes de registrar una venta.',
                ]);
            }

            $productos = Producto::query()
                ->where('empresa_id', $empresaId)
                ->where('activo', 1)
                ->whereIn('id', $productoIds)
                ->when($tenantSucursalId, static function ($query, int $sucursalId): void {
                    $query->where(static function ($nested) use ($sucursalId): void {
                        $nested->whereNull('sucursal_id')
                            ->orWhere('sucursal_id', $sucursalId);
                    });
                })
                ->lockForUpdate()
                ->get()
                ->keyBy('id');

            $detalleRows = [];
            $movimientos = [];
            $subtotal = 0.0;
            $descuentoLineas = 0.0;
            $impuestos = 0.0;

            foreach ($lineas as $index => $linea) {
                $productoId = (int) $linea['producto_id'];
                $producto = $productos->get($productoId);

                if (! $producto) {
                    throw ValidationException::withMessages([
                        "lineas.$index.producto_id" => 'El producto no pertenece a la empresa o sucursal activa.',
                    ]);
                }

                $cantidad = (float) $linea['cantidad'];
                $precioUnitarioSolicitado = isset($linea['precio_unitario']) && is_numeric($linea['precio_unitario'])
                    ? (float) $linea['precio_unitario']
                    : null;
                $precioUnitario = $precioUnitarioSolicitado !== null && $precioUnitarioSolicitado > 0
                    ? $precioUnitarioSolicitado
                    : (float) $producto->precio_venta;
                $impuestoPct = (float) ($producto->impuesto_porcentaje ?? 0);
                $subtotalLinea = round($cantidad * $precioUnitario, 2);
                $descuentoLinea = $this->resolveLineDiscountAmount(
                    lineSubtotal: $subtotalLinea,
                    discountValue: (float) ($linea['descuento_linea'] ?? 0),
                    discountType: (string) ($linea['descuento_linea_tipo'] ?? 'valor')
                );
                $baseGravableLinea = max(0, round($subtotalLinea - $descuentoLinea, 2));
                $impuestoLinea = round($baseGravableLinea * ($impuestoPct / 100), 2);
                $totalLinea = round($baseGravableLinea + $impuestoLinea, 2);

                $subtotal += $subtotalLinea;
                $descuentoLineas += $descuentoLinea;
                $impuestos += $impuestoLinea;

                $detalleRows[] = [
                    'empresa_id' => $empresaId,
                    'producto_id' => $productoId,
                    'cantidad' => $cantidad,
                    'precio_unitario' => $precioUnitario,
                    'impuesto' => $impuestoLinea,
                    'descuento' => $descuentoLinea,
                    'total_linea' => $totalLinea,
                ];

                $movimientos[] = [
                    'producto' => $producto,
                    'cantidad' => $cantidad,
                ];
            }

            $subtotalNeto = max(0, round($subtotal - $descuentoLineas, 2));
            if ($descuentoGlobal > $subtotalNeto) {
                throw ValidationException::withMessages([
                    'descuento' => 'El descuento global no puede superar el subtotal de la venta.',
                ]);
            }

            $descuentoTotal = round($descuentoLineas + $descuentoGlobal, 2);
            $total = round(($subtotalNeto + $impuestos) - $descuentoGlobal, 2);
            if ($total <= 0) {
                throw ValidationException::withMessages([
                    'lineas' => 'El total de la venta debe ser mayor a cero.',
                ]);
            }

            $metodoPago = strtolower(trim((string) $validated['metodo_pago']));
            $pagoCliente = isset($validated['pago_cliente']) && is_numeric($validated['pago_cliente'])
                ? (float) $validated['pago_cliente']
                : 0.0;
            $abonoInicial = isset($validated['abono_inicial']) && is_numeric($validated['abono_inicial'])
                ? round((float) $validated['abono_inicial'], 2)
                : 0.0;

            if ($tipoVenta === 'contado' && $metodoPago === 'efectivo') {
                if (round($pagoCliente, 2) < round($total, 2)) {
                    throw ValidationException::withMessages([
                        'pago_cliente' => 'El pago del cliente no puede ser menor al total de la venta.',
                    ]);
                }
            }

            if ($tipoVenta === 'credito' && $abonoInicial > round($total, 2)) {
                throw ValidationException::withMessages([
                    'abono_inicial' => 'El abono inicial no puede superar el total de la venta.',
                ]);
            }

            $montoPagadoInicial = $tipoVenta === 'credito'
                ? max(0.0, $abonoInicial)
                : round($total, 2);
            $estadoVenta = $montoPagadoInicial <= 0
                ? 'pendiente'
                : ($montoPagadoInicial + 0.0001 >= round($total, 2) ? 'pagada' : 'parcial');

            $saleMetodoPagoPosted = $metodoPago;
            $saleTotalPosted = (float) $total;
            $salePagoClientePosted = $tipoVenta === 'credito'
                ? $montoPagadoInicial
                : (float) $pagoCliente;
            $saleCambioPosted = $tipoVenta === 'contado' && $metodoPago === 'efectivo'
                ? max(0.0, round($pagoCliente - $total, 2))
                : 0.0;

            /** @var Venta $venta */
            $venta = Venta::query()->create([
                'empresa_id' => $empresaId,
                'sucursal_id' => $tenantSucursalId,
                'usuario_id' => $usuarioId,
                'cliente_id' => $clienteId,
                'apertura_caja_id' => $apertura->id,
                'subtotal' => $subtotal,
                'descuento' => $descuentoTotal,
                'impuestos' => $impuestos,
                'total' => $total,
                'estado' => $estadoVenta,
                'fecha' => now(),
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            $rows = [];
            foreach ($detalleRows as $detalleRow) {
                $rows[] = array_merge($detalleRow, [
                    'venta_id' => $venta->id,
                ]);
            }
            VentaDetalle::query()->insert($rows);

            if ($montoPagadoInicial > 0) {
                VentaPago::query()->create([
                    'venta_id' => $venta->id,
                    'empresa_id' => $empresaId,
                    'metodo_pago' => strtolower(trim((string) $validated['metodo_pago'])),
                    'monto' => $montoPagadoInicial,
                    'referencia' => $validated['referencia_pago'] ?? null,
                    'created_at' => now(),
                ]);
            }

            $facturaNumero = $this->reserveInvoiceNumber($empresaId, $tenantSucursalId, (int) $venta->id);
            /** @var Factura $factura */
            $factura = Factura::query()->create([
                'empresa_id' => $empresaId,
                'sucursal_id' => $tenantSucursalId,
                'venta_id' => $venta->id,
                'cliente_id' => $clienteId,
                'tipo_documento' => 'factura',
                'numero' => $facturaNumero,
                'subtotal' => $subtotal,
                'impuestos' => $impuestos,
                'total' => $total,
                'estado' => 'emitida',
                'fecha_emision' => now(),
            ]);

            $facturaDetalleRows = [];
            foreach ($detalleRows as $detalleRow) {
                $producto = $productos->get((int) $detalleRow['producto_id']);
                $descripcion = $producto ? (string) ($producto->nombre ?? 'Producto') : 'Producto';
                $facturaDetalleRows[] = [
                    'factura_id' => $factura->id,
                    'producto_id' => $detalleRow['producto_id'],
                    'descripcion' => mb_substr($descripcion, 0, 255),
                    'cantidad' => $detalleRow['cantidad'],
                    'precio_unitario' => $detalleRow['precio_unitario'],
                    'impuesto' => $detalleRow['impuesto'],
                    'total_linea' => $detalleRow['total_linea'],
                ];
            }
            if ($facturaDetalleRows !== []) {
                FacturaDetalle::query()->insert($facturaDetalleRows);
            }

            foreach ($movimientos as $entry) {
                /** @var Producto $producto */
                $producto = $entry['producto'];
                $cantidad = (float) $entry['cantidad'];

                $this->inventoryFlowService->applyMovement(
                    empresaId: $empresaId,
                    sucursalId: $tenantSucursalId,
                    producto: $producto,
                    tipo: 'salida',
                    cantidad: $cantidad,
                    costoUnitario: 0.0,
                    referencia: 'POS-VENTA-'.$venta->id
                );
            }

            if ($montoPagadoInicial > 0) {
                MovimientoCaja::query()->create([
                    'empresa_id' => $empresaId,
                    'apertura_id' => $apertura->id,
                    'usuario_id' => $usuarioId,
                    'tipo' => 'ingreso',
                    'concepto' => self::POS_SALE_MOVEMENT_PREFIX.$venta->id,
                    'monto' => $montoPagadoInicial,
                    'observacion' => 'origen=venta_pos;tipo_venta='.$tipoVenta.';metodo='.strtolower(trim((string) $validated['metodo_pago'])),
                    'created_at' => now(),
                ]);
            }

            $ventaIdPosted = (int) $venta->id;
        });

        // Create shipping record only when the cashier explicitly requests it
        if ($ventaIdPosted > 0 && $request->boolean('generar_envio')) {
            try {
                $venta = Venta::find($ventaIdPosted);
                if ($venta) {
                    $this->envioService->crearDesdeVenta($venta);
                }
            } catch (\Throwable) {
                // Envio creation failure must never break the POS flow
            }
        }

        return redirect()->route('pos.index')
            ->with('sale_posted_id', $ventaIdPosted)
            ->with('sale_posted_total', $saleTotalPosted)
            ->with('sale_posted_pago_cliente', $salePagoClientePosted)
            ->with('sale_posted_cambio', $saleCambioPosted)
            ->with('sale_posted_metodo_pago', $saleMetodoPagoPosted);
    }

    public function storeCustomer(Request $request): JsonResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $validated = $request->validate([
            'nombre' => ['required', 'string', 'max:160'],
            'documento' => ['nullable', 'string', 'max:80'],
            'email' => ['nullable', 'email', 'max:180'],
            'telefono' => ['nullable', 'string', 'max:60'],
            'direccion' => ['nullable', 'string', 'max:255'],
        ]);

        $customer = Cliente::query()->create([
            'empresa_id' => $empresaId,
            'nombre' => $validated['nombre'],
            'documento' => $validated['documento'] ?? null,
            'email' => $validated['email'] ?? null,
            'telefono' => $validated['telefono'] ?? null,
            'direccion' => $validated['direccion'] ?? null,
            'saldo_pendiente' => 0,
            'credito_maximo' => 0,
            'activo' => 1,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return response()->json([
            'ok' => true,
            'message' => 'Cliente creado correctamente.',
            'customer' => [
                'id' => (int) $customer->id,
                'nombre' => (string) $customer->nombre,
                'documento' => (string) ($customer->documento ?? ''),
                'telefono' => (string) ($customer->telefono ?? ''),
                'email' => (string) ($customer->email ?? ''),
            ],
        ]);
    }

    public function ticket(Venta $venta): View
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $tenantSucursalId = tenant_sucursal_id();

        $sale = $this->resolveSaleForTenant(
            ventaId: (int) $venta->id,
            empresaId: $empresaId,
            tenantSucursalId: $tenantSucursalId,
            forUpdate: false
        );

        if (! $sale) {
            abort(404, 'La venta no pertenece al tenant activo.');
        }

        $sale->load([
            'empresa',
            'sucursal',
            'cliente',
            'usuario',
            'detalles.producto',
            'pagos',
        ]);

        return view('modules.pos.ticket', [
            'sale' => $sale,
        ]);
    }

    public function annul(Request $request, Venta $venta): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $usuarioId = (int) (auth_user()?->id ?? 0);
        abort_if($usuarioId <= 0, 403, 'Usuario invalido para anular venta.');

        $tenantSucursalId = tenant_sucursal_id();

        $validated = $request->validate([
            'motivo' => ['nullable', 'string', 'max:255'],
        ]);

        DB::transaction(function () use ($empresaId, $usuarioId, $tenantSucursalId, $venta, $validated): void {
            $sale = $this->resolveSaleForTenant(
                ventaId: (int) $venta->id,
                empresaId: $empresaId,
                tenantSucursalId: $tenantSucursalId,
                forUpdate: true
            );

            if (! $sale) {
                abort(404, 'La venta no pertenece al tenant activo.');
            }

            if ((string) $sale->estado === 'anulada') {
                throw ValidationException::withMessages([
                    'venta' => 'La venta ya se encuentra anulada.',
                ]);
            }

            if (! is_numeric($sale->apertura_caja_id) || (int) $sale->apertura_caja_id <= 0) {
                throw ValidationException::withMessages([
                    'venta' => 'La venta no tiene una apertura de caja asociada para reversar.',
                ]);
            }

            $opening = AperturaCaja::query()
                ->where('empresa_id', $empresaId)
                ->whereKey((int) $sale->apertura_caja_id)
                ->lockForUpdate()
                ->first();

            if (! $opening) {
                throw ValidationException::withMessages([
                    'venta' => 'No se encontró la apertura de caja asociada a la venta.',
                ]);
            }

            if ((string) $opening->estado !== 'abierta') {
                throw ValidationException::withMessages([
                    'venta' => 'No puedes anular la venta porque el turno de caja ya está cerrado.',
                ]);
            }

            $hasReversalMovement = MovimientoCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_id', $opening->id)
                ->where('concepto', self::POS_SALE_REVERSAL_PREFIX.$sale->id)
                ->exists();

            if ($hasReversalMovement) {
                throw ValidationException::withMessages([
                    'venta' => 'La venta ya tiene una reversa de caja registrada.',
                ]);
            }

            $sale->load('detalles');
            if ($sale->detalles->isEmpty()) {
                throw ValidationException::withMessages([
                    'venta' => 'La venta no tiene líneas para devolver a inventario.',
                ]);
            }

            $productIds = $sale->detalles
                ->pluck('producto_id')
                ->filter(static fn ($id): bool => is_numeric($id))
                ->map(static fn ($id): int => (int) $id)
                ->unique()
                ->values()
                ->all();

            $products = Producto::query()
                ->where('empresa_id', $empresaId)
                ->whereIn('id', $productIds)
                ->lockForUpdate()
                ->get()
                ->keyBy('id');

            $saleSucursalId = is_numeric($sale->sucursal_id) ? (int) $sale->sucursal_id : null;

            foreach ($sale->detalles as $index => $line) {
                $product = $products->get((int) $line->producto_id);

                if (! $product) {
                    throw ValidationException::withMessages([
                        'venta' => 'No se encontró un producto de la venta para reversar inventario.',
                    ]);
                }

                $quantity = (float) $line->cantidad;
                if ($quantity <= 0) {
                    throw ValidationException::withMessages([
                        'venta' => 'La línea '.($index + 1).' tiene una cantidad inválida para devolución.',
                    ]);
                }

                $this->inventoryFlowService->applyMovement(
                    empresaId: $empresaId,
                    sucursalId: $saleSucursalId,
                    producto: $product,
                    tipo: 'entrada',
                    cantidad: $quantity,
                    costoUnitario: (float) ($product->precio_compra ?? 0),
                    referencia: 'DEV-VENTA-'.$sale->id
                );
            }

            $originalIncome = (float) MovimientoCaja::query()
                ->where('empresa_id', $empresaId)
                ->where('apertura_id', $opening->id)
                ->where('concepto', self::POS_SALE_MOVEMENT_PREFIX.$sale->id)
                ->sum('monto');

            $reversalAmount = $originalIncome > 0
                ? $originalIncome
                : (float) $sale->total;

            $motivo = trim((string) ($validated['motivo'] ?? ''));
            if ($motivo === '') {
                $motivo = 'Devolución total por anulación de venta POS.';
            }

            MovimientoCaja::query()->create([
                'empresa_id' => $empresaId,
                'apertura_id' => $opening->id,
                'usuario_id' => $usuarioId,
                'tipo' => 'egreso',
                'concepto' => self::POS_SALE_REVERSAL_PREFIX.$sale->id,
                'monto' => $reversalAmount,
                'observacion' => 'reversa_venta_id='.$sale->id.';motivo='.$motivo,
                'created_at' => now(),
            ]);

            if (Schema::hasTable('devoluciones')) {
                Devolucion::query()->create([
                    'empresa_id' => $empresaId,
                    'venta_id' => $sale->id,
                    'usuario_id' => $usuarioId,
                    'motivo' => $motivo,
                    'total' => (float) $sale->total,
                    'fecha' => now(),
                ]);
            }

            if (Schema::hasTable('facturas')) {
                Factura::query()
                    ->where('empresa_id', $empresaId)
                    ->where('venta_id', $sale->id)
                    ->update([
                        'estado' => 'anulada',
                        'updated_at' => now(),
                    ]);
            }

            $sale->fill([
                'estado' => 'anulada',
                'updated_at' => now(),
            ]);
            $sale->save();
        });

        return redirect()->route('pos.index')
            ->with('status', 'Venta anulada y reversada correctamente (inventario + caja).');
    }

    private function normalizeSaleLines(array $validated): \Illuminate\Support\Collection
    {
        $lineasRaw = $validated['lineas'] ?? null;
        if (is_array($lineasRaw) && $lineasRaw !== []) {
            return collect($lineasRaw)
                ->map(static function (array $line): array {
                    return [
                        'producto_id' => (int) ($line['producto_id'] ?? 0),
                        'cantidad' => (float) ($line['cantidad'] ?? 0),
                        'precio_unitario' => isset($line['precio_unitario']) && is_numeric($line['precio_unitario'])
                            ? (float) $line['precio_unitario']
                            : null,
                        'descuento_linea' => (float) ($line['descuento_linea'] ?? 0),
                        'descuento_linea_tipo' => (string) ($line['descuento_linea_tipo'] ?? 'valor'),
                    ];
                })
                ->values();
        }

        $productoIds = is_array($validated['producto_id'] ?? null) ? array_values($validated['producto_id']) : [];
        $cantidades = is_array($validated['cantidad'] ?? null) ? array_values($validated['cantidad']) : [];
        $preciosUnitarios = is_array($validated['precio_unitario'] ?? null) ? array_values($validated['precio_unitario']) : [];
        $descuentosLinea = is_array($validated['descuento_linea'] ?? null) ? array_values($validated['descuento_linea']) : [];
        $descuentosLineaTipo = is_array($validated['descuento_linea_tipo'] ?? null) ? array_values($validated['descuento_linea_tipo']) : [];

        if ($productoIds === [] || $cantidades === []) {
            throw ValidationException::withMessages([
                'lineas' => 'Debes agregar al menos un producto para registrar la venta.',
            ]);
        }

        $maxItems = max(count($productoIds), count($cantidades));
        if (count($productoIds) !== count($cantidades)) {
            throw ValidationException::withMessages([
                'lineas' => 'Las líneas del carrito están incompletas. Vuelve a cargar la página e intenta de nuevo.',
            ]);
        }

        $lineas = collect();
        for ($index = 0; $index < $maxItems; $index++) {
            $productoId = $productoIds[$index] ?? null;
            $cantidad = $cantidades[$index] ?? null;

            if (! is_numeric($productoId) || ! is_numeric($cantidad)) {
                continue;
            }

            $lineas->push([
                'producto_id' => (int) $productoId,
                'cantidad' => (float) $cantidad,
                'precio_unitario' => isset($preciosUnitarios[$index]) && is_numeric($preciosUnitarios[$index])
                    ? (float) $preciosUnitarios[$index]
                    : null,
                'descuento_linea' => isset($descuentosLinea[$index]) && is_numeric($descuentosLinea[$index])
                    ? (float) $descuentosLinea[$index]
                    : 0.0,
                'descuento_linea_tipo' => isset($descuentosLineaTipo[$index]) && in_array($descuentosLineaTipo[$index], ['valor', 'porcentaje'], true)
                    ? (string) $descuentosLineaTipo[$index]
                    : 'valor',
            ]);
        }

        if ($lineas->isEmpty()) {
            throw ValidationException::withMessages([
                'lineas' => 'Debes agregar al menos un producto para registrar la venta.',
            ]);
        }

        return $lineas->values();
    }

    private function resolveLineDiscountAmount(float $lineSubtotal, float $discountValue, string $discountType): float
    {
        if ($lineSubtotal <= 0 || $discountValue <= 0) {
            return 0.0;
        }

        if ($discountType === 'porcentaje') {
            $pct = max(0, min(100, $discountValue));

            return round($lineSubtotal * ($pct / 100), 2);
        }

        return round(min($lineSubtotal, max(0, $discountValue)), 2);
    }

    private function resolveSaleForTenant(int $ventaId, int $empresaId, ?int $tenantSucursalId, bool $forUpdate): ?Venta
    {
        $query = Venta::query()
            ->where('empresa_id', $empresaId)
            ->whereKey($ventaId)
            ->when($tenantSucursalId, static function ($builder, int $sucursalId): void {
                $builder->where(static function ($nested) use ($sucursalId): void {
                    $nested->whereNull('sucursal_id')
                        ->orWhere('sucursal_id', $sucursalId);
                });
            });

        if ($forUpdate) {
            $query->lockForUpdate();
        }

        return $query->first();
    }

    private function reserveInvoiceNumber(int $empresaId, ?int $sucursalId, int $ventaId): string
    {
        $branchId = $sucursalId ?? 0;

        $numbering = DB::table('numeraciones_documentos')
            ->where('empresa_id', $empresaId)
            ->where('tipo_documento', 'factura')
            ->where('activo', 1)
            ->where(static function ($query) use ($sucursalId): void {
                $query->whereNull('sucursal_id');
                if ($sucursalId !== null) {
                    $query->orWhere('sucursal_id', $sucursalId);
                }
            })
            ->orderByRaw('CASE WHEN sucursal_id = ? THEN 0 ELSE 1 END', [$sucursalId])
            ->orderBy('id')
            ->lockForUpdate()
            ->first();

        if (! $numbering) {
            return mb_substr('POS-'.$branchId.'-'.$ventaId, 0, 80);
        }

        $current = (int) ($numbering->consecutivo_actual ?? 0);
        $final = $numbering->consecutivo_final !== null ? (int) $numbering->consecutivo_final : null;
        $prefix = trim((string) ($numbering->prefijo ?? ''));

        if ($current <= 0) {
            throw ValidationException::withMessages([
                'lineas' => 'La numeración activa tiene consecutivo inválido.',
            ]);
        }
        if ($final !== null && $current > $final) {
            throw ValidationException::withMessages([
                'lineas' => 'La numeración activa ya alcanzó su consecutivo final.',
            ]);
        }

        DB::table('numeraciones_documentos')
            ->where('id', $numbering->id)
            ->where('empresa_id', $empresaId)
            ->update([
                'consecutivo_actual' => $current + 1,
                'updated_at' => now(),
            ]);

        return mb_substr($prefix.(string) $current, 0, 80);
    }
}
