<?php
declare(strict_types=1);

require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/importadores.php';

header('Content-Type: application/json; charset=utf-8');

function responder(array $payload, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($payload, JSON_UNESCAPED_UNICODE);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    responder(['ok' => false, 'error' => 'Método no permitido.'], 405);
}

// Si el archivo (o la petición completa) supera post_max_size, PHP descarta $_POST y
// $_FILES por completo antes de que el script se ejecute. Se detecta aqui para no
// confundir al usuario con un falso error de CSRF cuando el problema real es el
// limite de subida del servidor.
if (empty($_POST) && empty($_FILES) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
    $postMaxSize = ini_get('post_max_size') ?: '(desconocido)';
    responder([
        'ok' => false,
        'error' => "El archivo es demasiado grande para el límite actual del servidor (post_max_size = $postMaxSize). "
            . 'Pide a tu proveedor de hosting que aumente post_max_size y upload_max_filesize (ver README).',
    ], 413);
}

if (!csrfValidate()) {
    responder(['ok' => false, 'error' => 'Token CSRF inválido. Recargue la página e intente de nuevo.'], 419);
}

$tipo = $_POST['tipo'] ?? '';
if (!isset($_FILES['archivo']) || $_FILES['archivo']['error'] !== UPLOAD_ERR_OK) {
    responder(['ok' => false, 'error' => 'No se recibió un archivo válido.'], 400);
}

$archivoTmp = $_FILES['archivo']['tmp_name'];
$nombreOriginal = $_FILES['archivo']['name'];

if ($_FILES['archivo']['size'] > MAX_UPLOAD_MB * 1024 * 1024) {
    responder(['ok' => false, 'error' => 'El archivo supera el límite de ' . MAX_UPLOAD_MB . 'MB.'], 400);
}
$ext = strtolower((string) pathinfo($nombreOriginal, PATHINFO_EXTENSION));
if ($ext !== 'csv') {
    responder(['ok' => false, 'error' => 'Solo se permiten archivos .csv'], 400);
}

try {
    [$aliases, $requeridos] = configImportacion($tipo);
} catch (InvalidArgumentException $e) {
    responder(['ok' => false, 'error' => $e->getMessage()], 400);
}

// Copiar a carpeta de uploads con nombre unico (trazabilidad)
if (!is_dir(UPLOAD_DIR)) {
    mkdir(UPLOAD_DIR, 0755, true);
}
$nombreGuardado = date('Ymd_His') . '_' . $tipo . '_' . preg_replace('/[^A-Za-z0-9._-]/', '_', $nombreOriginal);
$rutaGuardada = UPLOAD_DIR . $nombreGuardado;
move_uploaded_file($archivoTmp, $rutaGuardada);

$conn = db();
$filasLeidas = 0;
$insertadas = 0;
$actualizadas = 0;
$errores = [];

$conn->begin_transaction();

try {
    // Ventas se procesa en streaming (dos pasadas sobre el archivo, en lotes) para
    // soportar archivos grandes sin cargar todas las filas en memoria a la vez.
    // Los demas tipos son maestros pequeños/medianos y se procesan como antes.
    if ($tipo === 'ventas') {
        [$filasLeidas, $insertadas, $actualizadas, $errores] = importarVentasStream($conn, $rutaGuardada, $aliases, $requeridos);
    } else {
        $lectura = leerCsvConAlias($rutaGuardada, $aliases, $requeridos);
        if (!empty($lectura['faltantes'])) {
            throw new RuntimeException('El archivo no tiene las columnas requeridas: ' . implode(', ', $lectura['faltantes']));
        }
        $filas = $lectura['filas'];
        $filasLeidas = count($filas);
        switch ($tipo) {
            case 'clientes':
                [$insertadas, $actualizadas, $errores] = importarClientes($conn, $filas);
                break;
            case 'productos':
                [$insertadas, $actualizadas, $errores] = importarProductos($conn, $filas);
                break;
            case 'vendedores':
                [$insertadas, $actualizadas, $errores] = importarVendedores($conn, $filas);
                break;
            case 'cartera':
                [$insertadas, $actualizadas, $errores] = importarCartera($conn, $filas);
                break;
            case 'recaudos':
                [$insertadas, $actualizadas, $errores] = importarRecaudos($conn, $filas);
                break;
        }
    }
    $conn->commit();
} catch (RuntimeException $e) {
    $conn->rollback();
    responder(['ok' => false, 'error' => $e->getMessage()], 400);
} catch (Throwable $e) {
    $conn->rollback();
    responder(['ok' => false, 'error' => 'Error durante la importación: ' . $e->getMessage()], 500);
}

$stmtLog = $conn->prepare(
    'INSERT INTO importaciones_log (tipo, archivo, filas_leidas, filas_insertadas, filas_actualizadas, filas_error, detalle_errores, usuario)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
);
$detalleErrores = $errores ? implode("\n", array_slice($errores, 0, 200)) : null;
$filasError = count($errores);
$usuario = $_SERVER['PHP_AUTH_USER'] ?? 'admin';
$stmtLog->bind_param('ssiiiiss', $tipo, $nombreOriginal, $filasLeidas, $insertadas, $actualizadas, $filasError, $detalleErrores, $usuario);
$stmtLog->execute();
$stmtLog->close();

responder([
    'ok' => true,
    'filas_leidas' => $filasLeidas,
    'insertadas' => $insertadas,
    'actualizadas' => $actualizadas,
    'errores' => $errores,
]);

// =====================================================================
// Funciones de importación por tipo
// =====================================================================

function importarClientes(mysqli $conn, array $filas): array
{
    $vendedoresValidos = precargarSet($conn, 'vendedores', 'cod_vendedor');
    $errores = [];
    $insertadas = 0;
    $actualizadas = 0;

    $stmt = $conn->prepare(
        'INSERT INTO clientes (codigo_cliente, nit, nombre, nombre_negocio, direccion, cod_vendedor, telefono,
                                tipologia, ciudad, barrio, cod_regional, fecha_creado, ruta, dias_cartera)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE nit=VALUES(nit), nombre=VALUES(nombre), nombre_negocio=VALUES(nombre_negocio),
             direccion=VALUES(direccion), cod_vendedor=VALUES(cod_vendedor), telefono=VALUES(telefono),
             tipologia=VALUES(tipologia), ciudad=VALUES(ciudad), barrio=VALUES(barrio), cod_regional=VALUES(cod_regional),
             fecha_creado=VALUES(fecha_creado), ruta=VALUES(ruta), dias_cartera=VALUES(dias_cartera)'
    );

    foreach ($filas as $i => $f) {
        $numFila = $i + 2;
        $codigo = parseCodigo($f['codigo_cliente']);
        $nombre = trim((string) $f['nombre']);
        if ($codigo === null || $nombre === '') {
            $errores[] = "Fila $numFila: codigo_cliente o nombre vacío, se omitió.";
            continue;
        }
        $codVendedor = parseCodigo($f['cod_vendedor'] ?? null);
        if ($codVendedor !== null && !isset($vendedoresValidos[$codVendedor])) {
            $errores[] = "Fila $numFila: vendedor '$codVendedor' no existe, se guardó el cliente sin vendedor asignado.";
            $codVendedor = null;
        }
        $nit = parseCodigo($f['nit'] ?? null);
        $nombreNegocio = trim((string) ($f['nombre_negocio'] ?? '')) ?: null;
        $direccion = trim((string) ($f['direccion'] ?? '')) ?: null;
        $telefono = trim((string) ($f['telefono'] ?? '')) ?: null;
        $tipologia = trim((string) ($f['tipologia'] ?? '')) ?: null;
        $ciudad = trim((string) ($f['ciudad'] ?? '')) ?: null;
        $barrio = trim((string) ($f['barrio'] ?? '')) ?: null;
        $codRegional = parseCodigo($f['cod_regional']);
        $fechaCreado = parseFecha($f['fecha_creado'] ?? null);
        $ruta = trim((string) ($f['ruta'] ?? '')) ?: null;
        $diasCartera = (int) parseDecimal($f['dias_cartera'] ?? '0');

        $stmt->bind_param(
            'sssssssssssssi',
            $codigo, $nit, $nombre, $nombreNegocio, $direccion, $codVendedor, $telefono,
            $tipologia, $ciudad, $barrio, $codRegional, $fechaCreado, $ruta, $diasCartera
        );
        $stmt->execute();
        $conn->affected_rows === 1 ? $insertadas++ : $actualizadas++;
    }
    $stmt->close();
    return [$insertadas, $actualizadas, $errores];
}

function importarProductos(mysqli $conn, array $filas): array
{
    $errores = [];
    $insertadas = 0;
    $actualizadas = 0;

    $stmt = $conn->prepare(
        'INSERT INTO productos (codigo, titulo, unidad, iva, categoria, subcategoria, nombre_comercial, marca, factor_kg, gramaje)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE titulo=VALUES(titulo), unidad=VALUES(unidad), iva=VALUES(iva),
             categoria=VALUES(categoria), subcategoria=VALUES(subcategoria), nombre_comercial=VALUES(nombre_comercial),
             marca=VALUES(marca), factor_kg=VALUES(factor_kg), gramaje=VALUES(gramaje)'
    );

    foreach ($filas as $i => $f) {
        $numFila = $i + 2;
        $codigo = parseCodigo($f['codigo']);
        $titulo = trim((string) $f['titulo']);
        if ($codigo === null || $titulo === '') {
            $errores[] = "Fila $numFila: codigo o titulo vacío, se omitió.";
            continue;
        }
        $unidad = trim((string) ($f['unidad'] ?? '')) ?: null;
        $iva = parseDecimal($f['iva'] ?? '0');
        $categoria = trim((string) ($f['categoria'] ?? '')) ?: null;
        $subcategoria = trim((string) ($f['subcategoria'] ?? '')) ?: null;
        $nombreComercial = trim((string) ($f['nombre_comercial'] ?? '')) ?: null;
        $marca = trim((string) ($f['marca'] ?? '')) ?: null;
        $factorKg = parseDecimal($f['factor_kg'] ?? '0');
        $gramaje = parseDecimal($f['gramaje'] ?? '0');

        $stmt->bind_param('sssdssssdd', $codigo, $titulo, $unidad, $iva, $categoria, $subcategoria, $nombreComercial, $marca, $factorKg, $gramaje);
        $stmt->execute();
        $conn->affected_rows === 1 ? $insertadas++ : $actualizadas++;
    }
    $stmt->close();
    return [$insertadas, $actualizadas, $errores];
}

function importarVendedores(mysqli $conn, array $filas): array
{
    $errores = [];
    $insertadas = 0;
    $actualizadas = 0;

    $stmt = $conn->prepare(
        'INSERT INTO vendedores (cod_vendedor, nombre, direccion, telefono, cod_regional, canal)
         VALUES (?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), direccion=VALUES(direccion), telefono=VALUES(telefono),
             cod_regional=VALUES(cod_regional), canal=VALUES(canal)'
    );

    foreach ($filas as $i => $f) {
        $numFila = $i + 2;
        $codigo = parseCodigo($f['cod_vendedor']);
        $nombre = trim((string) $f['nombre']);
        if ($codigo === null || $nombre === '') {
            $errores[] = "Fila $numFila: cod_vendedor o nombre vacío, se omitió.";
            continue;
        }
        $direccion = trim((string) ($f['direccion'] ?? '')) ?: null;
        $telefono = trim((string) ($f['telefono'] ?? '')) ?: null;
        $codRegional = parseCodigo($f['cod_regional']);
        $canal = trim((string) ($f['canal'] ?? '')) ?: null;

        $stmt->bind_param('ssssss', $codigo, $nombre, $direccion, $telefono, $codRegional, $canal);
        $stmt->execute();
        $conn->affected_rows === 1 ? $insertadas++ : $actualizadas++;
    }
    $stmt->close();
    return [$insertadas, $actualizadas, $errores];
}

/**
 * Importa ventas leyendo el archivo en DOS pasadas (streaming), sin cargar nunca todas
 * las filas en memoria: la primera pasada solo arma los encabezados de factura (livianos,
 * uno por factura distinta); la segunda inserta las lineas de detalle en lotes de 500 y
 * descarta cada lote de memoria apenas se inserta. Necesario porque un archivo de ventas
 * de decenas de miles de filas agotaba el memory_limit del servidor si se cargaba entero.
 * Retorna [filasLeidas, insertadas, facturasAfectadas, errores].
 */
function importarVentasStream(mysqli $conn, string $rutaArchivo, array $aliases, array $requeridos): array
{
    $clientesValidos = precargarSet($conn, 'clientes', 'codigo_cliente');
    $vendedoresValidos = precargarSet($conn, 'vendedores', 'cod_vendedor');
    $productosValidos = precargarSet($conn, 'productos', 'codigo');

    // --- Pasada 1: encabezados de factura (cliente/fecha/vendedor/regional) -------------
    $abierto = abrirCsvConAlias($rutaArchivo, $aliases);
    $faltantes = array_values(array_diff($requeridos, array_keys($abierto['indicePorCampo'])));
    if (!empty($faltantes)) {
        fclose($abierto['handle']);
        throw new RuntimeException('El archivo no tiene las columnas requeridas: ' . implode(', ', $faltantes));
    }

    $facturasAgg = [];
    $filasLeidas = 0;
    while (($filaRaw = fgetcsv($abierto['handle'], 0, $abierto['delimitador'])) !== false) {
        if (count($filaRaw) === 1 && trim((string) $filaRaw[0]) === '') {
            continue;
        }
        $filasLeidas++;
        $f = mapearFilaCsv($filaRaw, $abierto['indicePorCampo']);
        $factura = parseCodigo($f['factura'] ?? null);
        if ($factura === null) {
            continue; // se cuenta y reporta como error en la pasada 2
        }
        $cliente = parseCodigo($f['codigo_cliente'] ?? null);
        if ($cliente !== null && !isset($clientesValidos[$cliente])) {
            $cliente = null;
        }
        $vendedor = parseCodigo($f['cod_vendedor'] ?? null);
        if ($vendedor !== null && !isset($vendedoresValidos[$vendedor])) {
            $vendedor = null;
        }
        $fecha = parseFecha($f['fecha'] ?? null);
        $codRegional = parseCodigo($f['cod_regional'] ?? null);

        if (!isset($facturasAgg[$factura])) {
            $facturasAgg[$factura] = ['cliente' => $cliente, 'fecha' => $fecha, 'vendedor' => $vendedor, 'codRegional' => $codRegional];
        }
        if ($cliente !== null) { $facturasAgg[$factura]['cliente'] = $cliente; }
        if ($fecha !== null) { $facturasAgg[$factura]['fecha'] = $fecha; }
        if ($vendedor !== null) { $facturasAgg[$factura]['vendedor'] = $vendedor; }
    }
    fclose($abierto['handle']);

    // Encabezados de factura placeholder (montos en 0; se recalculan al final desde las
    // lineas reales). Deben existir antes de insertar lineas por la FK de ventas_detalle.
    foreach (array_chunk(array_keys($facturasAgg), 500) as $chunk) {
        $valoresSql = [];
        $params = [];
        foreach ($chunk as $facturaKey) {
            $agg = $facturasAgg[$facturaKey];
            $valoresSql[] = '(?, ?, ?, ?, ?, 0, 0, 0, 0)';
            // $facturaKey viene de una clave de array: PHP convierte silenciosamente las
            // claves numericas (ej. "1807224") a int, asi que se castea de vuelta a string.
            array_push($params, (string) $facturaKey, $agg['cliente'], $agg['fecha'], $agg['vendedor'], $agg['codRegional']);
        }
        $sql = 'INSERT IGNORE INTO facturas (factura, codigo_cliente, fecha, cod_vendedor, cod_regional, valor_neto, valor_iva, valor_total, num_items)
                VALUES ' . implode(', ', $valoresSql);
        $stmt = $conn->prepare($sql);
        $stmt->execute($params);
        $stmt->close();
    }

    // --- Pasada 2: lineas de detalle, en lotes de 500 (idempotente via linea_hash) ------
    $abierto2 = abrirCsvConAlias($rutaArchivo, $aliases);
    $errores = [];
    $insertadas = 0;
    $buffer = [];
    $numFila = 1;

    while (($filaRaw = fgetcsv($abierto2['handle'], 0, $abierto2['delimitador'])) !== false) {
        $numFila++;
        if (count($filaRaw) === 1 && trim((string) $filaRaw[0]) === '') {
            continue;
        }
        $f = mapearFilaCsv($filaRaw, $abierto2['indicePorCampo']);
        $factura = parseCodigo($f['factura'] ?? null);
        $producto = parseCodigo($f['codigo_producto'] ?? null);
        if ($factura === null) {
            if (count($errores) < 2000) {
                $errores[] = "Fila $numFila: número de factura vacío, se omitió.";
            }
            continue;
        }
        if ($producto !== null && !isset($productosValidos[$producto])) {
            if (count($errores) < 2000) {
                $errores[] = "Fila $numFila: producto '$producto' no existe en el maestro, se guardó la línea sin producto vinculado.";
            }
            $producto = null;
        }
        $cliente = parseCodigo($f['codigo_cliente'] ?? null);
        if ($cliente !== null && !isset($clientesValidos[$cliente])) {
            if (count($errores) < 2000) {
                $errores[] = "Fila $numFila: cliente '$cliente' no existe en el maestro, factura sin cliente vinculado.";
            }
            $cliente = null;
        }
        $vendedor = parseCodigo($f['cod_vendedor'] ?? null);
        if ($vendedor !== null && !isset($vendedoresValidos[$vendedor])) {
            if (count($errores) < 2000) {
                $errores[] = "Fila $numFila: vendedor '$vendedor' no existe, línea sin vendedor vinculado.";
            }
            $vendedor = null;
        }

        $fecha = parseFecha($f['fecha'] ?? null);
        $cantidad = parseDecimal($f['cantidad'] ?? '0');
        $precio = parseDecimal($f['precio'] ?? '0');
        $valorNeto = parseDecimal($f['valor_neto'] ?? '0');
        $tarifaIva = parseDecimal($f['tarifa_iva'] ?? '0');
        $valorIva = parseDecimal($f['valor_iva'] ?? '0');
        $codRegional = parseCodigo($f['cod_regional'] ?? null);

        $buffer[] = compact('factura', 'cliente', 'producto', 'fecha', 'cantidad', 'precio', 'valorNeto', 'tarifaIva', 'valorIva', 'vendedor', 'codRegional');

        if (count($buffer) >= 500) {
            $insertadas += insertarLoteVentasDetalle($conn, $buffer);
            $buffer = [];
        }
    }
    if ($buffer) {
        $insertadas += insertarLoteVentasDetalle($conn, $buffer);
    }
    fclose($abierto2['handle']);

    // --- Recalcular totales de las facturas afectadas a partir de sus lineas reales ----
    recalcularFacturasEnLote($conn, array_keys($facturasAgg));

    return [$filasLeidas, $insertadas, count($facturasAgg), $errores];
}

/** Inserta un lote de lineas de venta (INSERT IGNORE, idempotente via linea_hash). Retorna filas insertadas. */
function insertarLoteVentasDetalle(mysqli $conn, array $lineas): int
{
    $valoresSql = [];
    $params = [];
    foreach ($lineas as $l) {
        $hash = sha1($l['factura'] . '|' . $l['producto'] . '|' . $l['cantidad'] . '|' . $l['precio'] . '|' . $l['fecha']);
        $valoresSql[] = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
        array_push(
            $params,
            $l['factura'], $l['cliente'], $l['producto'], $l['fecha'], $l['cantidad'], $l['precio'],
            $l['valorNeto'], $l['tarifaIva'], $l['valorIva'], $l['vendedor'], $l['codRegional'], $hash
        );
    }
    $sql = 'INSERT IGNORE INTO ventas_detalle
            (factura, codigo_cliente, codigo_producto, fecha, cantidad, precio, valor_neto, tarifa_iva, valor_iva, cod_vendedor, cod_regional, linea_hash)
            VALUES ' . implode(', ', $valoresSql);
    $stmt = $conn->prepare($sql);
    $stmt->execute($params);
    $insertadas = $stmt->affected_rows > 0 ? $stmt->affected_rows : 0;
    $stmt->close();
    return $insertadas;
}

function importarCartera(mysqli $conn, array $filas): array
{
    $clientesValidos = precargarSet($conn, 'clientes', 'codigo_cliente');
    $vendedoresValidos = precargarSet($conn, 'vendedores', 'cod_vendedor');
    $errores = [];
    $insertadas = 0;
    $actualizadas = 0;

    $stmt = $conn->prepare(
        'INSERT INTO cartera (factura, codigo_cliente, cod_vendedor, valor_documento, saldo, fecha_factura, dias_vencidos, retefuente, descuento)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE codigo_cliente=VALUES(codigo_cliente), cod_vendedor=VALUES(cod_vendedor),
             valor_documento=VALUES(valor_documento), saldo=VALUES(saldo), fecha_factura=VALUES(fecha_factura),
             dias_vencidos=VALUES(dias_vencidos), retefuente=VALUES(retefuente), descuento=VALUES(descuento)'
    );

    foreach ($filas as $i => $f) {
        $numFila = $i + 2;
        $factura = parseCodigo($f['factura']);
        if ($factura === null) {
            $errores[] = "Fila $numFila: número de factura vacío, se omitió.";
            continue;
        }
        $cliente = parseCodigo($f['codigo_cliente'] ?? null);
        if ($cliente !== null && !isset($clientesValidos[$cliente])) {
            $errores[] = "Fila $numFila: cliente '$cliente' no existe, saldo sin cliente vinculado.";
            $cliente = null;
        }
        $vendedor = parseCodigo($f['cod_vendedor'] ?? null);
        if ($vendedor !== null && !isset($vendedoresValidos[$vendedor])) {
            $errores[] = "Fila $numFila: vendedor '$vendedor' no existe, saldo sin vendedor vinculado.";
            $vendedor = null;
        }
        $valorDocumento = parseDecimal($f['valor_documento'] ?? '0');
        $saldo = parseDecimal($f['saldo'] ?? '0');
        $fechaFactura = parseFecha($f['fecha_factura'] ?? null);
        $diasVencidos = (int) parseDecimal($f['dias_vencidos'] ?? '0');
        $retefuente = parseDecimal($f['retefuente'] ?? '0');
        $descuento = parseDecimal($f['descuento'] ?? '0');

        $stmt->bind_param('sssddsidd', $factura, $cliente, $vendedor, $valorDocumento, $saldo, $fechaFactura, $diasVencidos, $retefuente, $descuento);
        $stmt->execute();
        $conn->affected_rows === 1 ? $insertadas++ : $actualizadas++;
    }
    $stmt->close();
    return [$insertadas, $actualizadas, $errores];
}

function importarRecaudos(mysqli $conn, array $filas): array
{
    $clientesValidos = precargarSet($conn, 'clientes', 'codigo_cliente');
    $vendedoresValidos = precargarSet($conn, 'vendedores', 'cod_vendedor');
    $errores = [];
    $insertadas = 0;

    $stmt = $conn->prepare(
        'INSERT IGNORE INTO recaudos
            (fecha_recaudo, codigo_cliente, nombre_cliente, cod_vendedor, factura, valor_recaudo, id_tipo_pago, tipo_pago, retefuente, descuento, linea_hash)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
    );

    foreach ($filas as $i => $f) {
        $numFila = $i + 2;
        $fecha = parseFecha($f['fecha_recaudo'] ?? null);
        $factura = parseCodigo($f['factura']);
        $valor = parseDecimal($f['valor_recaudo'] ?? '0');
        if ($fecha === null || $factura === null) {
            $errores[] = "Fila $numFila: fecha_recaudo o factura vacío, se omitió.";
            continue;
        }
        $cliente = parseCodigo($f['codigo_cliente'] ?? null);
        if ($cliente !== null && !isset($clientesValidos[$cliente])) {
            $errores[] = "Fila $numFila: cliente '$cliente' no existe, recaudo sin cliente vinculado.";
            $cliente = null;
        }
        $vendedor = parseCodigo($f['cod_vendedor'] ?? null);
        if ($vendedor !== null && !isset($vendedoresValidos[$vendedor])) {
            $errores[] = "Fila $numFila: vendedor '$vendedor' no existe, recaudo sin vendedor vinculado.";
            $vendedor = null;
        }
        $nombreCliente = trim((string) ($f['nombre_cliente'] ?? '')) ?: null;
        $idTipoPago = parseCodigo($f['id_tipo_pago'] ?? null);
        $tipoPago = trim((string) ($f['tipo_pago'] ?? '')) ?: null;
        $retefuente = parseDecimal($f['retefuente'] ?? '0');
        $descuento = parseDecimal($f['descuento'] ?? '0');
        $hash = sha1($fecha . '|' . $cliente . '|' . $factura . '|' . $valor . '|' . $tipoPago . '|' . $retefuente . '|' . $descuento);

        $stmt->bind_param('sssssdssdds', $fecha, $cliente, $nombreCliente, $vendedor, $factura, $valor, $idTipoPago, $tipoPago, $retefuente, $descuento, $hash);
        $stmt->execute();
        if ($conn->affected_rows > 0) { $insertadas++; }
    }
    $stmt->close();
    return [$insertadas, 0, $errores];
}
