import { BadGatewayException, BadRequestException, Injectable, Logger, NotFoundException, PreconditionFailedException } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { AuditService } from 'src/audit/audit.service';
import { PagosProveedorService } from 'src/pagos-proveedor/pagos-proveedor.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { MarangatuConfigService } from './marangatu-config.service';
import { CompraEstado } from 'src/compras/enums/compra-estado.enum';
import { envs } from 'src/config';
import { XMLParser } from 'fast-xml-parser';

export interface SyncDocumentosDto {
  anho: number;
  mes: number;
  cdcs?: string[];
}

/** Error HTTP del bot Marangatu, con el status y el `message` que devolvió. */
export class MicroservicioError extends Error {
  constructor(
    readonly path: string,
    readonly status: number,
    readonly detalle: string,
  ) {
    super(`Microservicio ${path} respondió ${status}: ${detalle}`);
  }
}

async function microPost(path: string, body: unknown): Promise<unknown> {
  const api_base = (envs.botMarangatuBaseUrl ?? '').replace(/\/$/, '');
  const res = await fetch(`${api_base}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    let detalle = text;
    try {
      const json = JSON.parse(text) as { message?: string | string[] };
      if (json?.message) detalle = Array.isArray(json.message) ? json.message.join('; ') : json.message;
    } catch {
      /* no era JSON: queda el texto crudo */
    }
    throw new MicroservicioError(path, res.status, detalle);
  }
  return res.json();
}

const MSG_SIN_CONFIG = 'Esta empresa no tiene credenciales de Marangatu cargadas. Configuralas en Configuración → Empresa → Marangatu.';

/** Traduce un fallo del bot a una respuesta HTTP con su mensaje, en vez de un 500 pelado. */
function errorDelBot(err: unknown): never {
  if (err instanceof MicroservicioError) {
    throw new BadGatewayException(`Bot Marangatu: ${err.detalle || `respondió ${err.status}`}`);
  }
  throw err;
}

/** "11/05/2026" (DD/MM/YYYY) o "2026-05-11" → Date */
function parseDate(raw: string | null | undefined): Date | null {
  if (!raw) return null;
  if (raw.includes('/')) {
    const [d, m, y] = raw.split('/');
    return new Date(Number(y), Number(m) - 1, Number(d));
  }
  return new Date(raw);
}

/** Tipos de pago SET Manual 150 — iTiPago */
const TIPO_PAGO_DESC: Record<number, string> = {
  1: 'Efectivo',
  2: 'Cheque',
  3: 'Tarjeta de crédito',
  4: 'Tarjeta de débito',
  5: 'Transferencia bancaria',
  6: 'Giro',
  7: 'Billetera electrónica',
  8: 'Tarjeta empresarial',
  9: 'Vale',
  10: 'Otro',
};

/** "013-001-1672908" → { establecimiento, puntoExpedicion, numeroFactura } */
function parseNumeroComprobante(numero: string | null | undefined) {
  const parts = (numero ?? '').split('-');
  return {
    establecimiento: parts[0] ?? '',
    puntoExpedicion: parts[1] ?? '',
    numeroFactura: parts[2] ?? '',
  };
}

/** Timbrado desde xmlParsed.DE.gTimb.dNumTim o protocolo */
function extractTimbrado(doc: any): string {
  return doc.xmlParsed?.DE?.gTimb?.dNumTim ?? doc.protocolo ?? '';
}

/**
 * Detecta evento de cancelación: si xContEv.rEve o xContEv.rRetEnviEventoDe
 * tiene contenido, se considera documento cancelado (ECAN).
 */
function detectarEventoCancelacion(doc: any): {
  esCancelado: boolean;
  mensajeEvento: string | null;
  fechaEvento: Date | null;
} {
  const ev = doc.xContEv;
  if (!ev) return { esCancelado: false, mensajeEvento: null, fechaEvento: null };

  const rEve = ev.rEve;
  const rRet = ev.rRetEnviEventoDe;

  const esCancelado = !!(rEve || rRet);

  const mensajeEvento = rRet?.dMsgRes ?? rRet?.mensaje ?? rEve?.dMotEve ?? null;
  const fechaRaw = rRet?.dFecProc ?? rRet?.fecha_retorno ?? ev.fechaEvento ?? null;
  const fechaEvento = fechaRaw ? parseDate(fechaRaw) : null;

  return { esCancelado, mensajeEvento, fechaEvento };
}

@Injectable()
export class MarangatuSyncService {
  private readonly logger = new Logger(MarangatuSyncService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly configService: MarangatuConfigService,
    private readonly audit: AuditService,
    private readonly pagosProveedor: PagosProveedorService,
  ) {}

  async registrarEmpresa(empresaId: string) {
    const cfg = await this.configService.getFullConfig(empresaId);
    const password = await this.configService.getPassword(empresaId);

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, ruc: true, dv: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');

    await microPost('/api/empresas/alta-integrada', {
      empresa: {
        razon_social: empresa.razon_social,
        ruc: empresa.ruc,
        dv: empresa.dv?.toString(),
        activo: true,
      },
      marangatu: { username: cfg.username, password },
    });

    this.logger.log(`Empresa ${empresa.ruc} registrada en microservicio Marangatu`);
  }

  /**
   * Llama al bot con la empresa; si el bot no la conoce (404) y acá sí hay
   * credenciales, la da de alta y reintenta una vez. El alta que se dispara al
   * guardar la config falla en silencio (`.catch(() => null)` en el
   * controller), así que sin esto la primera captura de una empresa recién
   * configurada podía quedar en un 500 sin explicación.
   */
  private async postConEmpresaRegistrada(empresaId: string, path: string, body: unknown): Promise<unknown> {
    if (!(await this.configService.getConfig(empresaId))) {
      throw new PreconditionFailedException(MSG_SIN_CONFIG);
    }
    try {
      return await microPost(path, body);
    } catch (err) {
      if (!(err instanceof MicroservicioError) || err.status !== 404) errorDelBot(err);
      this.logger.warn(`El bot no conoce la empresa ${empresaId}; se la registra y se reintenta ${path}`);
      await this.registrarEmpresa(empresaId).catch(errorDelBot);
      return microPost(path, body).catch(errorDelBot);
    }
  }

  async triggerSync(empresaId: string, tipoRegistro: 'COMPRAS' | 'VENTAS', dto: SyncDocumentosDto) {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { ruc: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');

    const tipoApi = tipoRegistro === 'COMPRAS' ? 'COMPRA' : 'VENTA';
    await this.postConEmpresaRegistrada(empresaId, '/api/sincronizacion-documentos', {
      empresaRuc: empresa.ruc,
      tipoRegistro: tipoApi,
      anho: String(dto.anho),
      mes: String(dto.mes),
    });

    this.logger.log(`Sync ${tipoRegistro} disparado para empresa ${empresa.ruc} ${dto.anho}/${dto.mes}`);
    return { message: `Sincronización ${tipoRegistro} iniciada para ${dto.anho}/${dto.mes}` };
  }

  async consultarDocumentos(empresaId: string, tipoRegistro: 'COMPRAS' | 'VENTAS', dto: SyncDocumentosDto) {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { ruc: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');

    const tipoApi = tipoRegistro === 'COMPRAS' ? 'COMPRA' : 'VENTA';
    const pad = (n: number) => String(n).padStart(2, '0');
    const lastDay = new Date(dto.anho, dto.mes, 0).getDate();
    const fechaDesde = `${dto.anho}-${pad(dto.mes)}-01`;
    const fechaHasta = `${dto.anho}-${pad(dto.mes)}-${lastDay}`;

    const botResult = (await this.postConEmpresaRegistrada(empresaId, '/api/consulta-documentos', {
      empresaRuc: empresa.ruc,
      tipoRegistro: tipoApi,
      fechaDesde,
      fechaHasta,
    })) as any;

    const docs: any[] = Array.isArray(botResult?.data)
      ? botResult.data
      : Array.isArray(botResult?.documentos)
        ? botResult.documentos
        : Array.isArray(botResult)
          ? botResult
          : [];

    if (docs.length === 0) return botResult;

    // Marcado automático de electrónicos: acá ya tenemos la lista completa que la DNIT
    // tiene del período, que es justo lo que define si un comprobante hay que declarar
    // o no. Aprovecharla no cuesta una llamada extra y evita depender de que alguien se
    // acuerde de apretar el botón antes de sacar el Libro IVA Compras.
    // Fail-soft: si esto falla, la consulta del panel tiene que funcionar igual.
    if (tipoRegistro === 'COMPRAS') {
      this.aplicarMarcasElectronicos(empresaId, docs).catch((e) =>
        this.logger.error(`No se pudieron marcar los electrónicos automáticamente: ${(e as Error).message}`),
      );
    }

    // Cruzar CDCs y números contra la DB para marcar ya importados
    const cdcs = docs.map((d: any) => d.cdc).filter(Boolean);
    const getNumero = (d: any) => {
      if (d.numeroComprobante) return d.numeroComprobante;
      const t = d.xmlParsed?.DE?.gTimb ?? {};
      return [t.dEst, t.dPunExp, t.dNumDoc].filter(Boolean).join('-');
    };

    let existentesCdc = new Set<string>();
    let existentesNumero = new Set<string>();

    if (tipoRegistro === 'VENTAS') {
      const rows = await this.prisma.factura_cab.findMany({
        where: { empresa_id: empresaId, ...(cdcs.length > 0 ? { cdc: { in: cdcs } } : {}) },
        select: { cdc: true, dest: true, dpunexp: true, dnumdoc: true },
      });
      existentesCdc = new Set(rows.map((r) => r.cdc).filter(Boolean));
      existentesNumero = new Set(rows.map((r) => [r.dest, r.dpunexp, r.dnumdoc].filter(Boolean).join('-')));
    } else {
      // compra_cab no tiene cdc — identificar por establecimiento-punto_expedicion-numero_factura
      const numerosFactura = docs
        .map((d: any) => parseNumeroComprobante(d.numeroComprobante).numeroFactura)
        .filter(Boolean);
      const rows = await this.prisma.compra_cab.findMany({
        where: {
          empresa_id: empresaId,
          ...(numerosFactura.length > 0 ? { numero_factura: { in: numerosFactura } } : {}),
        },
        select: { establecimiento: true, punto_expedicion: true, numero_factura: true },
      });
      existentesNumero = new Set(
        rows.map((r) => [r.establecimiento, r.punto_expedicion, r.numero_factura].filter(Boolean).join('-')),
      );
    }

    // Cruzar también contra nota_credito_cab por CDC: las NC viajan en el
    // feed de COMPRA (cuando la empresa las emite) o VENTA, pero se guardan
    // en su propia tabla. Sin este cruce, las NC ya importadas aparecen
    // siempre como "nuevas".
    const ncCdcs = docs
      .filter((d: any) => {
        const tipo = String(d?.tipoComprobante ?? '')
          .toUpperCase()
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '');
        const iTiDE = Number(d?.xmlParsed?.DE?.gTimb?.iTiDE ?? 0);
        return tipo.includes('NOTA DE CREDITO') || iTiDE === 5;
      })
      .map((d: any) => d.cdc)
      .filter(Boolean);

    let existentesNcCdc = new Set<string>();
    if (ncCdcs.length > 0) {
      const ncRows = await this.prisma.nota_credito_cab.findMany({
        where: { empresa_id: empresaId, cdc: { in: ncCdcs } },
        select: { cdc: true },
      });
      existentesNcCdc = new Set(ncRows.map((r) => r.cdc).filter(Boolean));
    }

    const enriched = docs.map((d: any) => ({
      ...d,
      yaImportado:
        (d.cdc && existentesCdc.has(d.cdc)) ||
        existentesNumero.has(getNumero(d)) ||
        (d.cdc && existentesNcCdc.has(d.cdc)),
    }));

    if (Array.isArray(botResult?.data)) return { ...botResult, data: enriched };
    if (Array.isArray(botResult?.documentos)) return { ...botResult, documentos: enriched };
    return enriched;
  }

  /**
   * Marca como electrónicos los comprobantes del período que la DNIT ya tiene.
   *
   * El Libro IVA Compras solo declara los preimpresos: los electrónicos le llegan
   * solos a la DNIT por el documento del proveedor y volver a subirlos los duplica.
   *
   * Por qué existe esto aparte del import: importar una compra es una decisión
   * operativa ("quiero este documento registrado como compra en el ERP") y el panel
   * deja elegir cuáles. Que un comprobante sea electrónico es un hecho fiscal
   * independiente de eso. Deducir lo segundo de lo primero deja huecos — en el cierre
   * de julio de un cliente quedaron 27 gastos marcados como preimpresos cuando la
   * DNIT ya los tenía, porque solo se habían importado 28 de 62 documentos.
   *
   * Acá se consulta el período a la DNIT y se marcan las coincidencias, sin importar
   * nada ni tocar montos. Es idempotente: correrlo dos veces no cambia nada.
   */
  async sincronizarMarcasElectronicos(empresaId: string, dto: SyncDocumentosDto) {
    const result = await this.consultarDocumentos(empresaId, 'COMPRAS', dto);
    const docs: any[] = Array.isArray(result?.data)
      ? result.data
      : Array.isArray(result?.documentos)
        ? result.documentos
        : Array.isArray(result)
          ? result
          : [];

    const marcado = await this.aplicarMarcasElectronicos(empresaId, docs);
    return {
      periodo: `${String(dto.mes).padStart(2, '0')}/${dto.anho}`,
      documentos_dnit: docs.length,
      ...marcado,
    };
  }

  /**
   * Núcleo del marcado. Recibe los documentos ya traídos de la DNIT para que quien
   * los tenga en la mano (la consulta del panel) no tenga que volver a pedirlos.
   */
  private async aplicarMarcasElectronicos(empresaId: string, docs: any[]) {
    // Solo los que tienen CDC son documentos electrónicos. Un comprobante sin CDC en
    // el feed es de carga manual o virtual del propio contribuyente y sigue siendo
    // declarable.
    const claves = new Map<string, { establecimiento: string; puntoExpedicion: string; numeroFactura: string }>();
    for (const doc of docs) {
      if (!doc?.cdc || !String(doc.cdc).trim()) continue;
      const partes = parseNumeroComprobante(doc.numeroComprobante);
      if (!partes.numeroFactura) continue;
      claves.set(
        [partes.establecimiento, partes.puntoExpedicion, partes.numeroFactura].join('-'),
        partes,
      );
    }

    if (claves.size === 0) {
      return { electronicos_dnit: 0, gastos_marcados: 0, compras_marcadas: 0 };
    }

    const partesList = [...claves.values()];
    const numeros = [...new Set(partesList.map((p) => p.numeroFactura))];

    // Se filtra primero por numero_factura (que es lo indexable) y recién después se
    // exige la terna completa, para no traer toda la tabla.
    const [gastos, compras] = await Promise.all([
      this.prisma.gasto_cab.findMany({
        where: { empresa_id: empresaId, numero_factura: { in: numeros }, es_preimpreso: true },
        select: { id: true, establecimiento: true, punto_expedicion: true, numero_factura: true },
      }),
      this.prisma.compra_cab.findMany({
        where: { empresa_id: empresaId, numero_factura: { in: numeros }, es_preimpreso: true },
        select: { id: true, establecimiento: true, punto_expedicion: true, numero_factura: true },
      }),
    ]);

    const coincide = (r: { establecimiento: string | null; punto_expedicion: string | null; numero_factura: string | null }) =>
      claves.has([r.establecimiento, r.punto_expedicion, r.numero_factura].join('-'));

    const gastoIds = gastos.filter(coincide).map((r) => r.id);
    const compraIds = compras.filter(coincide).map((r) => r.id);

    if (gastoIds.length > 0) {
      await this.prisma.gasto_cab.updateMany({
        where: { id: { in: gastoIds } },
        data: { es_preimpreso: false, updated_at: new Date() },
      });
    }
    if (compraIds.length > 0) {
      await this.prisma.compra_cab.updateMany({
        where: { id: { in: compraIds } },
        data: { es_preimpreso: false, updated_at: new Date() },
      });
    }

    if (gastoIds.length > 0 || compraIds.length > 0) {
      this.logger.log(
        `Marcas electrónicas (empresa ${empresaId}): ${claves.size} documentos con CDC en la DNIT, ` +
          `${gastoIds.length} gastos y ${compraIds.length} compras marcados.`,
      );
    }

    return {
      electronicos_dnit: claves.size,
      gastos_marcados: gastoIds.length,
      compras_marcadas: compraIds.length,
    };
  }

  async importarCompras(empresaId: string, dto: SyncDocumentosDto) {
    const result = await this.consultarDocumentos(empresaId, 'COMPRAS', dto);
    let items: any[] = Array.isArray(result?.data)
      ? result.data
      : Array.isArray(result?.documentos)
        ? result.documentos
        : [];

    if (dto.cdcs && dto.cdcs.length > 0) {
      const set = new Set(dto.cdcs);
      items = items.filter((d) => d?.cdc && set.has(String(d.cdc)));
    }

    let importados = 0;
    let omitidos = 0;
    let yaExistentes = 0;
    let proveedoresCreados = 0;
    let ncFiltradas = 0;

    for (const doc of items) {
      try {
        // ── Filtrar NC del feed de COMPRAS ──────────────────────────────
        // El bot/SET devuelve las NC que la empresa EMITE bajo tipoRegistro=COMPRA.
        // Esas pertenecen al flujo de Ventas → NC (no de compras), así que las
        // ignoramos acá. Se importan vía importarNotasCredito() desde VENTAS.
        const tipoComp = String(doc.tipoComprobante ?? '')
          .toUpperCase()
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '');
        const iTiDE = Number(doc.xmlParsed?.DE?.gTimb?.iTiDE ?? 0);
        if (tipoComp.includes('NOTA DE CREDITO') || iTiDE === 5) {
          ncFiltradas++;
          continue;
        }

        const rucProveedor = doc.rucVendedor ?? doc.rucProveedor;
        const { proveedor, created } = await this.findOrCreateProveedor(
          empresaId,
          rucProveedor,
          doc.razonSocialVendedor ?? doc.razonSocial ?? `RUC ${rucProveedor}`,
        );
        if (created) proveedoresCreados++;

        const { establecimiento, puntoExpedicion, numeroFactura } = parseNumeroComprobante(doc.numeroComprobante);
        const timbrado = extractTimbrado(doc);
        const fechaEmision = parseDate(doc.fechaEmision);

        // Condición de pago: iCondOpe "1"=contado "2"=crédito (Manual SET 150)
        // gCamCond está en gDtipDE directamente, NO dentro de gCamFE
        const gCamCond = doc.xmlParsed?.DE?.gDtipDE?.gCamCond ?? {};
        const condOpe = Number(gCamCond.iCondOpe ?? 1);
        const gPagCred = gCamCond.gPagCred ?? {};
        const plazoCredito = condOpe === 2 ? Number(gPagCred.dPlazoCre ?? 0) : 0;

        // Extraer medios de pago contado (gPagCond puede ser objeto o array)
        const rawPagCond = gCamCond.gPagCond;
        const pagConds: any[] = Array.isArray(rawPagCond) ? rawPagCond : rawPagCond ? [rawPagCond] : [];
        const mediosPago = pagConds.map((p) => ({
          tipo: Number(p.iTiPago ?? 0),
          desc: p.dDPago ?? p.dDesMedPag ?? TIPO_PAGO_DESC[Number(p.iTiPago)] ?? 'Otro',
          monto: Number(p.dMonTiPag ?? 0),
          info: p.dInfoPag ?? null,
        }));

        // Cuotas para crédito
        const rawCuotas = gPagCred.gPagCredCuo ?? gPagCred.gPagCuo;
        const cuotas: any[] = Array.isArray(rawCuotas) ? rawCuotas : rawCuotas ? [rawCuotas] : [];
        const cuotasInfo = cuotas.map((q) => ({
          nro: Number(q.dNroCuo ?? 0),
          monto: Number(q.dMontCuota ?? q.dMontCuo ?? 0),
          vencimiento: q.dVencCuo ?? null,
        }));

        const pagoInfo = {
          condicion: condOpe === 2 ? 'credito' : 'contado',
          plazo: plazoCredito,
          cuotas: condOpe === 2 ? Number(gPagCred.dCuotas ?? cuotasInfo.length ?? 0) : 0,
          iCondCred: condOpe === 2 ? Number(gPagCred.iCondCred ?? 0) : 0,
          cuotasDetalle: cuotasInfo,
          medios: mediosPago,
        };
        const condLabel = condOpe === 2 ? `Crédito${plazoCredito > 0 ? ` ${plazoCredito}d` : ''}` : 'Contado';
        const observacionesStr = `Importado desde Marangatu · ${condLabel} · CDC ${doc.cdc ?? ''}\n---PAGO---\n${JSON.stringify(pagoInfo)}`;

        // gGrupTot está en gDtipDE directamente (sibling de gCamFE, gCamCond, gCamItem)
        const gTot = doc.xmlParsed?.DE?.gDtipDE?.gGrupTot ?? doc.xmlParsed?.DE?.gDtipDE?.gCamFE?.gGrupTot ?? {};
        const totalExento = Number(gTot.dTotExa ?? gTot.dTotExe ?? 0);
        const totalGrav5 = Number(gTot.dTotGravIVA5 ?? 0);
        const totalGrav10 = Number(gTot.dTotGravIVA10 ?? 0);
        const totalIva5 = Number(gTot.dTotIVA5 ?? 0);
        const totalIva10 = Number(gTot.dTotIVA10 ?? 0);
        const total = Number(gTot.dTotGe ?? gTot.dTotOpe ?? 0);

        // Verificar si ya existe
        const yaExiste = await this.prisma.compra_cab.findFirst({
          where: {
            empresa_id: empresaId,
            timbrado_proveedor: timbrado,
            establecimiento,
            punto_expedicion: puntoExpedicion,
            numero_factura: numeroFactura,
          },
          select: { id: true, estado: true },
        });

        // Si ya existe en cualquier estado, saltear — evita duplicar y preserva vinculaciones
        if (yaExiste) {
          yaExistentes++;
          continue;
        }

        const compra = await this.prisma.compra_cab.create({
          data: {
            empresa_id: empresaId,
            proveedor_id: proveedor.id,
            timbrado_proveedor: timbrado,
            establecimiento,
            punto_expedicion: puntoExpedicion,
            numero_factura: numeroFactura,
            fecha_emision: fechaEmision,
            total_exento: totalExento,
            total_gravado_5: totalGrav5,
            total_gravado_10: totalGrav10,
            total_iva_5: totalIva5,
            total_iva_10: totalIva10,
            total_iva: totalIva5 + totalIva10,
            total,
            plazo_credito: plazoCredito,
            estado: CompraEstado.IMPORTADO,
            observaciones: observacionesStr,
            // Lo que llega por acá es un documento electrónico que la DNIT ya tiene, así
            // que no se declara de nuevo en el Libro IVA. Va como columna propia y no
            // como estado/observación porque confirmarCompra reescribe ambos.
            es_preimpreso: false,
          },
        });

        // gCamItem está en gDtipDE directamente (Manual SET 150), NO dentro de gCamFE
        const camItems: any[] = doc.xmlParsed?.DE?.gDtipDE?.gCamItem ?? [];
        if (camItems.length > 0) {
          for (const it of camItems) {
            // Campos directos del ítem (Manual SET 150 §item)
            const codItem = it.dCodInt ?? null; // código interno ERP
            const descItem = it.dDesProSer ?? null; // descripción producto/servicio
            const cantidad = Number(it.dCantProSer ?? 1);

            // Valores monetarios: dentro de gValorItem
            const gVal = it.gValorItem ?? {};
            const precioUnitario = Number(gVal.dPUniProSer ?? 0); // precio unitario
            const subtotalBruto = Number(gVal.dTotBruOpeItem ?? 0); // total bruto ítem
            const subtotalNeto = Number(gVal.gValorRestaItem?.dTotOpeItem ?? subtotalBruto);

            // IVA: dentro de gCamIVA
            const gIVA = it.gCamIVA ?? {};
            const tasaIVA = Number(gIVA.dTasaIVA ?? 10); // tasa IVA (10 o 5)
            const montoIVA = Number(gIVA.dLiqIVAItem ?? 0); // monto IVA liquidado

            const producto = await this.findProductoByCodOrDesc(empresaId, codItem, descItem);
            await this.prisma.compra_det.create({
              data: {
                compra_id: compra.id,
                producto_id: producto?.id ?? null,
                codigo_producto: codItem,
                descripcion: descItem ?? 'Ítem importado',
                cantidad,
                precio_unitario: precioUnitario,
                porcentaje_iva: tasaIVA,
                monto_iva: montoIVA,
                subtotal: subtotalNeto,
                afecta_stock: producto?.maneja_inventario ?? false,
              },
            });
          }
        } else {
          await this.prisma.compra_det.create({
            data: {
              compra_id: compra.id,
              descripcion: `Factura Electrónica Marangatu · ${doc.razonSocialVendedor ?? rucProveedor}`,
              cantidad: 1,
              precio_unitario: total,
              porcentaje_iva: 10,
              monto_iva: totalIva10 + totalIva5,
              subtotal: total,
              afecta_stock: false,
            },
          });
        }

        importados++;
      } catch (err) {
        this.logger.warn(`Error importando ${doc.numeroComprobante}: ${err.message}`);
        omitidos++;
      }
    }

    await this.configService.updateSyncTimestamp(empresaId, 'compras');
    return { importados, omitidos, yaExistentes, ncFiltradas, total: items.length, proveedoresCreados };
  }

  async getPendientes(empresaId: string) {
    const compras = await this.prisma.compra_cab.findMany({
      where: { empresa_id: empresaId, estado: CompraEstado.IMPORTADO },
      include: {
        proveedores: { include: { personas: { select: { razon_social: true, ruc: true } } } },
        compra_det: {
          include: {
            productos: { select: { descripcion: true, cod_producto: true, maneja_inventario: true } },
          },
          orderBy: { created_at: 'asc' },
        },
      },
      orderBy: { fecha_emision: 'desc' },
    });
    return compras.map((c) => ({
      id: c.id,
      numero: [c.establecimiento, c.punto_expedicion, c.numero_factura].filter(Boolean).join('-'),
      timbrado: c.timbrado_proveedor,
      fecha_emision: c.fecha_emision,
      total: Number(c.total ?? 0) || c.compra_det.reduce((s, d) => s + Number(d.subtotal ?? 0), 0),
      total_iva: c.total_iva,
      total_exento: c.total_exento,
      total_gravado_5: c.total_gravado_5,
      total_gravado_10: c.total_gravado_10,
      estado: c.estado,
      proveedor: c.proveedores?.personas?.razon_social ?? '—',
      proveedor_ruc: c.proveedores?.personas?.ruc ?? null,
      condicion: (c.plazo_credito ?? 0) > 0 ? 'credito' : 'contado',
      plazo_credito: c.plazo_credito ?? 0,
      pago_info: (() => {
        try {
          const sep = '\n---PAGO---\n';
          const idx = (c.observaciones ?? '').indexOf(sep);
          if (idx === -1) return null;
          return JSON.parse((c.observaciones ?? '').slice(idx + sep.length));
        } catch {
          return null;
        }
      })(),
      items: c.compra_det.length,
      items_con_producto: c.compra_det.filter((d) => d.producto_id).length,
      observaciones: c.observaciones,
      detalle: c.compra_det.map((d) => ({
        id: d.id,
        descripcion: d.descripcion,
        codigo_producto: d.codigo_producto,
        cantidad: Number(d.cantidad),
        precio_unitario: Number(d.precio_unitario),
        subtotal: Number(d.subtotal),
        porcentaje_iva: Number(d.porcentaje_iva),
        producto_id: d.producto_id,
        producto_nombre: d.productos?.descripcion ?? null,
        producto_cod: d.productos?.cod_producto ?? null,
        maneja_inventario: d.productos?.maneja_inventario ?? false,
        afecta_stock: d.afecta_stock,
      })),
    }));
  }

  async confirmarCompra(compraId: string, empresaId: string, depositoId?: string, userId?: string) {
    const compra = await this.prisma.compra_cab.findFirst({
      where: { id: compraId, empresa_id: empresaId, estado: CompraEstado.IMPORTADO },
      include: { compra_det: true },
    });
    if (!compra) throw new Error('Compra no encontrada o ya procesada');

    const totals = this.calcularTotalesDesdeItems(compra.compra_det);
    const isCredito = Number(compra.plazo_credito ?? 0) > 0;

    const [condicion, monedaPyg, deposito, tipoMov] = await Promise.all([
      this.prisma.condicion_operacion.findFirst({ where: { codigo: isCredito ? 2 : 1 } }),
      this.prisma.moneda.findFirst({
        where: { OR: [{ codigo: 'PYG' }, { descripcion: { contains: 'Guaraní', mode: 'insensitive' } }] },
      }),
      this.resolveDeposito(empresaId, depositoId ?? compra.deposito_id),
      this.prisma.tipo_movimiento_inventario.findFirst({
        where: {
          OR: [
            { codigo: 'COMPRA' },
            { codigo: 'ENT_COMPRA' },
            { codigo: 'ENTRADA' },
            { descripcion: { contains: 'Compra', mode: 'insensitive' } },
          ],
        },
      }),
    ]);

    const numeroDoc = [compra.establecimiento, compra.punto_expedicion, compra.numero_factura]
      .filter(Boolean)
      .join('-');

    const obsLimpia =
      (compra.observaciones ?? '').split('\n---PAGO---\n')[0].trim() ||
      `Importado desde Marangatu · ${isCredito ? `Crédito ${compra.plazo_credito}d` : 'Contado'}`;

    const fechaEmision = compra.fecha_emision ?? new Date();

    // Parsear info de pago (cuotas/plazo) del JSON en observaciones
    let pagoInfo: any = null;
    try {
      const sep = '\n---PAGO---\n';
      const idx = (compra.observaciones ?? '').indexOf(sep);
      if (idx !== -1) pagoInfo = JSON.parse((compra.observaciones ?? '').slice(idx + sep.length));
    } catch {
      /* sin pago_info */
    }

    const cuotasDetalle: Array<{ nro: number; monto: number; vencimiento?: string }> = pagoInfo?.cuotasDetalle ?? [];
    const numeroCuotas = Number(pagoInfo?.cuotas ?? 0);
    const plazo = Number(compra.plazo_credito ?? pagoInfo?.plazo ?? 0);

    // Construir array de cuotas para cuentas_pagar
    type CuotaInput = { numero: number; total: number; monto: number; vencimiento: Date; obs: string };
    let cuotasInput: CuotaInput[];
    if (isCredito) {
      if (cuotasDetalle.length > 0) {
        const tot = cuotasDetalle.length;
        cuotasInput = cuotasDetalle.map((q, i) => ({
          numero: q.nro ?? i + 1,
          total: tot,
          monto: Number(q.monto ?? 0) || Math.round(totals.total / tot),
          vencimiento: q.vencimiento
            ? new Date(q.vencimiento)
            : new Date(fechaEmision.getTime() + (i + 1) * plazo * 86_400_000),
          obs: `Cuota ${q.nro ?? i + 1}/${tot} · ${numeroDoc}`,
        }));
      } else if (numeroCuotas > 1) {
        const montoCuota = Math.round(totals.total / numeroCuotas);
        const intervalo = plazo > 0 ? Math.round(plazo / numeroCuotas) : 30;
        cuotasInput = Array.from({ length: numeroCuotas }, (_, i) => ({
          numero: i + 1,
          total: numeroCuotas,
          monto: i === numeroCuotas - 1 ? totals.total - montoCuota * (numeroCuotas - 1) : montoCuota,
          vencimiento: new Date(fechaEmision.getTime() + (i + 1) * intervalo * 86_400_000),
          obs: `Cuota ${i + 1}/${numeroCuotas} · ${numeroDoc}`,
        }));
      } else {
        cuotasInput = [
          {
            numero: 1,
            total: 1,
            monto: totals.total,
            vencimiento: plazo > 0 ? new Date(fechaEmision.getTime() + plazo * 86_400_000) : fechaEmision,
            obs: plazo > 0 ? `Crédito ${plazo}d · ${numeroDoc}` : `Crédito · ${numeroDoc}`,
          },
        ];
      }
    } else {
      cuotasInput = [
        {
          numero: 1,
          total: 1,
          monto: totals.total,
          vencimiento: fechaEmision,
          obs: `Contado · ${numeroDoc}`,
        },
      ];
    }

    const medioPagoEfectivo = !isCredito
      ? await this.prisma.medio_pago.findFirst({
          where: { OR: [{ descripcion: { contains: 'Efectivo', mode: 'insensitive' } }, { codigo: 1 }] },
        })
      : null;

    let ordenPagoAutomaticaId: string | null = null;
    await this.prisma.$transaction(async (tx) => {
      const estadoFinal = !isCredito ? CompraEstado.PAGADA : CompraEstado.PENDIENTE;

      await tx.compra_cab.update({
        where: { id: compraId },
        data: {
          estado: estadoFinal,
          deposito_id: deposito.id,
          condicion_operacion_id: condicion?.id ?? null,
          moneda_id: monedaPyg?.id ?? null,
          total_exento: totals.total_exento,
          total_gravado_5: totals.total_gravado_5,
          total_gravado_10: totals.total_gravado_10,
          total_iva_5: totals.total_iva_5,
          total_iva_10: totals.total_iva_10,
          total_iva: totals.total_iva,
          subtotal: totals.subtotal,
          total: totals.total,
          observaciones: obsLimpia,
        },
      });

      await tx.compra_subtotales.deleteMany({ where: { compra_id: compraId } });
      await tx.compra_subtotales.createMany({
        data: totals.subtotalesPorIva.map((s) => ({
          compra_id: compraId,
          tipo_iva: s.tipo_iva,
          base_gravada: s.base_gravada,
          monto_iva: s.monto_iva,
        })),
      });

      await tx.cuentas_pagar.deleteMany({ where: { compra_id: compraId, empresa_id: empresaId } });
      const cuentasCreadas: Array<{ id: string; saldo_pendiente: any; moneda_id: any }> = [];
      for (const q of cuotasInput) {
        const c = await tx.cuentas_pagar.create({
          data: {
            empresa_id: empresaId,
            compra_id: compraId,
            proveedor_id: compra.proveedor_id,
            moneda_id: monedaPyg?.id ?? null,
            numero_cuota: q.numero,
            total_cuotas: q.total,
            fecha_emision: fechaEmision,
            fecha_vencimiento: q.vencimiento,
            monto_original: q.monto,
            monto_pagado: !isCredito ? q.monto : 0,
            saldo_pendiente: !isCredito ? 0 : q.monto,
            estado: !isCredito ? 'pagada' : 'pendiente',
            observaciones: q.obs,
            activo: true,
          },
        });
        cuentasCreadas.push(c);
      }
      const cuenta = cuentasCreadas[0]; // usado solo para pago contado (siempre 1 cuota)

      // Contado: registrar pago automático con Efectivo
      if (!isCredito && medioPagoEfectivo) {
        const sucursalId = (deposito as any).sucursal_id as string;
        const numeroOrden = await this.getNextNumeroOrdenPago(tx, empresaId, sucursalId);
        const orden = await tx.orden_pago_proveedor_cab.create({
          data: {
            empresa_id: empresaId,
            sucursal_id: sucursalId,
            proveedor_id: compra.proveedor_id,
            compra_id: compraId,
            moneda_id: monedaPyg?.id ?? null,
            numero_orden_pago: numeroOrden,
            fecha_pago: fechaEmision,
            monto_total: totals.total,
            cotizacion: 1,
            referencia: numeroDoc,
            observaciones: `Pago automático Marangatu · ${numeroDoc}`,
            origen: 'compra_auto',
            estado: 'PAGADO',
            anulado: false,
            activo: true,
          },
        });
        ordenPagoAutomaticaId = orden.id;
        await tx.orden_pago_proveedor_det.create({
          data: {
            orden_pago_id: orden.id,
            cuenta_pagar_id: cuenta.id,
            medio_pago_id: medioPagoEfectivo.id,
            monto: totals.total,
            monto_moneda: totals.total,
            cotizacion: 1,
            referencia: numeroDoc,
            observaciones: `Marangatu · ${numeroDoc}`,
          },
        });
        await tx.pagos_proveedor.create({
          data: {
            empresa_id: empresaId,
            cuenta_pagar_id: cuenta.id,
            proveedor_id: compra.proveedor_id,
            medio_pago_id: medioPagoEfectivo.id,
            moneda_id: monedaPyg?.id ?? null,
            numero_recibo: numeroOrden,
            fecha_pago: fechaEmision,
            monto: totals.total,
            monto_moneda: totals.total,
            cotizacion: 1,
            referencia: numeroDoc,
            observaciones: `Marangatu · ${numeroDoc}`,
            estado: 'aplicado',
            anulado: false,
            activo: true,
          },
        });
      }

      for (const det of compra.compra_det) {
        if (!det.producto_id || !det.afecta_stock) continue;
        const cantidad = Number(det.cantidad ?? 0);
        const costo = Number(det.precio_unitario ?? 0);
        if (cantidad <= 0) continue;

        await tx.stock_deposito.upsert({
          where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: det.producto_id } },
          create: { deposito_id: deposito.id, producto_id: det.producto_id, cantidad_disponible: cantidad },
          update: { cantidad_disponible: { increment: cantidad } },
        });

        if (tipoMov) {
          await tx.movimientos_inventario.create({
            data: {
              deposito_origen_id: deposito.id,
              producto_id: det.producto_id,
              tipo_movimiento_id: tipoMov.id,
              cantidad,
              costo_unitario: costo,
              precio_unitario: costo,
              documento_origen: 'factura_compra',
              documento_id: compraId,
              observaciones: `Marangatu · ${numeroDoc}`,
            },
          });
        }

        await tx.productos.update({
          where: { id: det.producto_id },
          data: { precio_costo: costo },
        });
      }
    });

    // Hook Tesorería — la orden automática (contado, "Efectivo") ya quedó creada;
    // syncTesoreriaOrdenPago no hace nada si no llegó a PAGADO/APLICADO.
    if (ordenPagoAutomaticaId) {
      this.pagosProveedor.syncTesoreriaOrdenPago(ordenPagoAutomaticaId, empresaId, userId).catch((err) => {
        this.logger.error(`Error registrando Tesorería para compra Marangatu ${compraId}: ${err.message}`);
      });
    }

    const conStock = compra.compra_det.filter((d) => d.producto_id && d.afecta_stock).length;

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'MAR_COMPRA_CONFIRMAR',
      entity_type: 'compra_cab',
      entity_id: compraId,
      descripcion: `Compra Marangatu confirmada — ${isCredito ? `Crédito ${plazo}d · ${cuotasInput.length} cuota(s)` : 'Contado'} · ${totals.total.toLocaleString('es-PY')} Gs.`,
      new_value: {
        comprobante: numeroDoc,
        proveedor_id: compra.proveedor_id,
        total: totals.total,
        es_credito: isCredito,
        plazo_dias: isCredito ? plazo : 0,
        cuotas: cuotasInput.length,
        deposito_id: deposito.id,
        items_totales: compra.compra_det.length,
        items_con_producto: compra.compra_det.filter((d) => d.producto_id).length,
        items_afecta_stock: conStock,
      },
    });

    return {
      ok: true,
      mensaje: `Compra confirmada · ${totals.total.toLocaleString('es-PY')} Gs.${conStock > 0 ? ` · ${conStock} ítem(s) con stock actualizado` : ''}`,
    };
  }

  private calcularTotalesDesdeItems(items: any[]) {
    let totalExento = 0,
      totalGravado5 = 0,
      totalGravado10 = 0;
    let totalIva5 = 0,
      totalIva10 = 0,
      subtotal = 0;
    for (const d of items) {
      const st = Number(d.subtotal ?? 0);
      const iva = Number(d.monto_iva ?? 0);
      const tasa = Number(d.porcentaje_iva ?? 10);
      subtotal += st;
      if (tasa === 0) {
        totalExento += st;
      } else if (tasa === 5) {
        totalGravado5 += st - iva;
        totalIva5 += iva;
      } else {
        totalGravado10 += st - iva;
        totalIva10 += iva;
      }
    }
    const r = (n: number) => Math.round(n * 100) / 100;
    return {
      total_exento: r(totalExento),
      total_gravado_5: r(totalGravado5),
      total_gravado_10: r(totalGravado10),
      total_iva_5: r(totalIva5),
      total_iva_10: r(totalIva10),
      total_iva: r(totalIva5 + totalIva10),
      subtotal: r(subtotal),
      total: r(subtotal),
      subtotalesPorIva: [
        { tipo_iva: 0, base_gravada: r(totalExento), monto_iva: 0 },
        { tipo_iva: 5, base_gravada: r(totalGravado5), monto_iva: r(totalIva5) },
        { tipo_iva: 10, base_gravada: r(totalGravado10), monto_iva: r(totalIva10) },
      ],
    };
  }

  async vincularDetalle(
    detId: string,
    empresaId: string,
    productoId: string | null,
    userId?: string,
  ) {
    const det = await this.prisma.compra_det.findFirst({
      where: { id: detId, compra_cab: { empresa_id: empresaId, estado: CompraEstado.IMPORTADO } },
      select: {
        id: true,
        descripcion: true,
        codigo_producto: true,
        producto_id: true,
        afecta_stock: true,
        compra_id: true,
        productos: { select: { descripcion: true, cod_producto: true } },
      },
    });
    if (!det) throw new Error('Ítem no encontrado o la compra ya fue procesada');

    let productoNuevo: { id: string; descripcion: string | null; cod_producto: string | null; maneja_inventario: boolean } | null = null;
    if (productoId) {
      const p = await this.prisma.productos.findFirst({
        where: { id: productoId, empresa_id: empresaId },
        select: { id: true, descripcion: true, cod_producto: true, maneja_inventario: true },
      });
      if (!p) throw new Error('Producto no encontrado');
      productoNuevo = {
        id: p.id,
        descripcion: p.descripcion,
        cod_producto: p.cod_producto,
        maneja_inventario: p.maneja_inventario ?? false,
      };
    }

    await this.prisma.compra_det.update({
      where: { id: detId },
      data: {
        producto_id: productoNuevo?.id ?? null,
        afecta_stock: productoNuevo?.maneja_inventario ?? false,
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: productoNuevo ? 'MAR_ITEM_VINCULAR' : 'MAR_ITEM_DESVINCULAR',
      entity_type: 'compra_det',
      entity_id: detId,
      descripcion: productoNuevo
        ? `Ítem Marangatu vinculado a producto ERP "${productoNuevo.descripcion ?? productoNuevo.cod_producto ?? productoNuevo.id}"`
        : 'Ítem Marangatu desvinculado del producto ERP',
      old_value: {
        producto_id: det.producto_id,
        producto_descripcion: det.productos?.descripcion ?? null,
        producto_cod: det.productos?.cod_producto ?? null,
        afecta_stock: det.afecta_stock,
      },
      new_value: {
        producto_id: productoNuevo?.id ?? null,
        producto_descripcion: productoNuevo?.descripcion ?? null,
        producto_cod: productoNuevo?.cod_producto ?? null,
        afecta_stock: productoNuevo?.maneja_inventario ?? false,
        compra_id: det.compra_id,
        item_descripcion_xml: det.descripcion,
        item_codigo_xml: det.codigo_producto,
      },
    });

    return { ok: true };
  }

  async cambiarCondicionCompra(
    compraId: string,
    empresaId: string,
    dto: {
      // Si viene condicion_pago_id, usamos la config del catálogo condiciones_pago
      // (tipo, dias_plazo, cuotas). Si no, se aceptan valores libres.
      condicion_pago_id?: string | null;
      es_credito?: boolean;
      plazo_dias?: number;
      cuotas?: number;
    },
    userId?: string,
  ) {
    const compra = await this.prisma.compra_cab.findFirst({
      where: { id: compraId, empresa_id: empresaId, estado: CompraEstado.IMPORTADO },
      select: {
        id: true,
        plazo_credito: true,
        observaciones: true,
        total: true,
        fecha_emision: true,
        establecimiento: true,
        punto_expedicion: true,
        numero_factura: true,
      },
    });
    if (!compra) throw new Error('Compra no encontrada o ya procesada');

    let esCredito = !!dto.es_credito;
    let plazo = Number(dto.plazo_dias ?? 0);
    let cuotas = Number(dto.cuotas ?? 1);

    if (dto.condicion_pago_id) {
      const cp = await this.prisma.condiciones_pago.findFirst({
        where: { id: dto.condicion_pago_id },
        select: {
          id: true,
          descripcion: true,
          dias_plazo: true,
          cuotas: true,
          cod_condicion_venta: true, // 1 = Contado, 2 = Crédito (código SET)
        },
      });
      if (!cp) throw new Error('Condición de pago no encontrada');
      esCredito = Number(cp.cod_condicion_venta ?? 1) === 2;
      plazo = Number(cp.dias_plazo ?? 0);
      cuotas = Number(cp.cuotas ?? 1);
    }

    if (esCredito && plazo < 0) plazo = 0;
    if (esCredito && cuotas < 1) cuotas = 1;

    // Actualizamos el JSON de observaciones/pago_info para que confirmarCompra
    // pueda recalcular las cuotas correctamente.
    const sep = '\n---PAGO---\n';
    const cabeceraObs = (compra.observaciones ?? '').split(sep)[0].trim();
    const pagoInfoNew = esCredito
      ? {
          condicion: 'credito',
          plazo,
          cuotas,
          iCondCred: cuotas > 1 ? 2 : 1,
          cuotasDetalle: [],
          medios: [],
        }
      : { condicion: 'contado', plazo: 0, cuotas: 1, cuotasDetalle: [], medios: [] };
    const obsNueva = `${cabeceraObs}${sep}${JSON.stringify(pagoInfoNew)}`;

    // `compra_cab` no tiene FK a `condiciones_pago` — la relación se preserva
    // en el JSON de pago_info dentro de observaciones y en `plazo_credito` (>0 = crédito).
    // El id del catálogo elegido queda en el audit para trazabilidad.
    await this.prisma.compra_cab.update({
      where: { id: compraId },
      data: {
        plazo_credito: esCredito ? plazo : 0,
        observaciones: obsNueva,
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'MAR_CONDICION_CAMBIAR',
      entity_type: 'compra_cab',
      entity_id: compraId,
      descripcion: `Condición de pago cambiada por el usuario a ${esCredito ? `crédito (${cuotas} cuota(s), ${plazo}d)` : 'contado'}`,
      old_value: {
        plazo_credito: compra.plazo_credito,
      },
      new_value: {
        es_credito: esCredito,
        plazo_dias: esCredito ? plazo : 0,
        cuotas: esCredito ? cuotas : 1,
        condicion_pago_id: dto.condicion_pago_id ?? null,
        comprobante: `${compra.establecimiento ?? ''}-${compra.punto_expedicion ?? ''}-${compra.numero_factura ?? ''}`,
      },
    });

    return { ok: true, mensaje: 'Condición de pago actualizada' };
  }

  async rechazarCompra(compraId: string, empresaId: string, motivo?: string, userId?: string) {
    const compra = await this.prisma.compra_cab.findFirst({
      where: { id: compraId, empresa_id: empresaId, estado: 'importado' },
      select: {
        id: true,
        establecimiento: true,
        punto_expedicion: true,
        numero_factura: true,
        proveedor_id: true,
        total: true,
      },
    });
    if (!compra) throw new Error('Compra no encontrada o ya procesada');

    await this.prisma.compra_cab.update({
      where: { id: compraId },
      data: {
        estado: CompraEstado.RECHAZADO,
        motivo_anulacion: motivo ?? 'Rechazado por el usuario desde panel Marangatu',
      },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'MAR_COMPRA_RECHAZAR',
      entity_type: 'compra_cab',
      entity_id: compraId,
      descripcion: `Compra Marangatu rechazada: ${motivo ?? 'sin motivo especificado'}`,
      new_value: {
        comprobante: `${compra.establecimiento ?? ''}-${compra.punto_expedicion ?? ''}-${compra.numero_factura ?? ''}`,
        proveedor_id: compra.proveedor_id,
        total: Number(compra.total ?? 0),
        motivo: motivo ?? null,
      },
    });

    return { ok: true, mensaje: 'Compra rechazada — no afectará stock ni costos' };
  }

  // ─────────────────────────────────────────────────────────────────────────
  //  IMPORTAR VENTAS — crea factura_cab + det + subtotales + cuotas
  // ─────────────────────────────────────────────────────────────────────────

  // ══════════════════════════════════════════════════════════════════════
  //  Recuperación de comprobantes históricos por CDC (consulta a SIFEN)
  // ══════════════════════════════════════════════════════════════════════

  /**
   * Trae de SIFEN el DE completo de cada CDC y lo importa como venta o nota de
   * crédito reusando el mismo mapeo que el import de Marangatu.
   *
   * Existe porque el bot de Marangatu solo alcanza los períodos que la DNIT tiene
   * publicados en el portal: los comprobantes viejos no se pueden recuperar por ahí.
   * SIFEN, en cambio, responde la consulta por CDC sin límite de antigüedad, así que
   * con la lista de CDC alcanza para reconstruir el histórico sin pedirle nada al
   * cliente ni cargar nada a mano.
   *
   * La consulta se firma con el certificado de la propia empresa. SIFEN valida que
   * quien pregunta esté habilitado, no que sea el emisor.
   *
   * Es idempotente: `procesarVentas` y `procesarNotasCredito` saltan lo que ya existe
   * por CDC, así que se puede correr de nuevo sobre el mismo lote sin duplicar.
   */
  async recuperarPorCdc(
    empresaId: string,
    cdcs: string[],
    opts: { loteSize?: number; dryRun?: boolean; sinCartera?: boolean } = {},
  ) {
    const unicos = Array.from(new Set(cdcs.map((c) => String(c).trim()).filter((c) => c.length > 0)));
    if (unicos.length === 0) {
      throw new BadRequestException('No se recibió ningún CDC para recuperar');
    }

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { id: true, ruc: true },
    });
    if (!empresa) throw new NotFoundException('Empresa no encontrada');

    // Lotes chicos: cada CDC es un round-trip firmado contra SIFEN, así que un lote
    // grande deja la request colgada varios minutos y arriesga timeout del proxy.
    const loteSize = Math.max(1, Math.min(opts.loteSize ?? 20, 50));
    const consultados: any[] = [];
    const fallidos: Array<{ cdc: string; motivo: string }> = [];

    for (let i = 0; i < unicos.length; i += loteSize) {
      const lote = unicos.slice(i, i + loteSize);
      let respuesta: any;
      try {
        respuesta = await this.consultarCdcSifen(empresa.ruc, lote);
      } catch (err: any) {
        // Un lote caído no puede tumbar la recuperación entera: se anota y sigue.
        for (const cdc of lote) fallidos.push({ cdc, motivo: err?.message ?? 'Error consultando SIFEN' });
        this.logger.warn(`Lote de CDC falló (${lote.length} docs): ${err?.message}`);
        continue;
      }

      for (const item of respuesta) {
        const cdc = String(item?.cdc ?? '');
        if (!item?.xml_documento) {
          fallidos.push({ cdc, motivo: item?.mensaje_sifen ?? `SIFEN no devolvió el XML (${item?.cod_respuesta ?? 's/cod'})` });
          continue;
        }
        const doc = this.documentoDesdeXmlSifen(cdc, item);
        if (!doc) {
          fallidos.push({ cdc, motivo: 'No se pudo interpretar el XML devuelto por SIFEN' });
          continue;
        }
        consultados.push(doc);
      }
    }

    const notasCredito = consultados.filter((d) => Number(d.xmlParsed?.DE?.gTimb?.iTiDE ?? 0) === 5);
    const ventas = consultados.filter((d) => Number(d.xmlParsed?.DE?.gTimb?.iTiDE ?? 0) !== 5);

    if (opts.dryRun) {
      // Se desglosa contado vs crédito porque es lo que decide si conviene correr con
      // sin_cartera: solo las de crédito abren saldo, y son las que pueden revivir como
      // deuda ya cobrada. Sin este número la decisión se toma a ciegas.
      let credito = 0;
      let contado = 0;
      let montoCredito = 0;
      for (const d of ventas) {
        const de = d.xmlParsed?.DE ?? {};
        if (Number(de.gDtipDE?.gCamCond?.iCondOpe ?? 1) === 2) {
          credito++;
          montoCredito += Number(de.gTotSub?.dTotGralOpe ?? 0);
        } else {
          contado++;
        }
      }

      return {
        dry_run: true,
        cdcs_pedidos: unicos.length,
        recuperados: consultados.length,
        facturas: ventas.length,
        facturas_contado: contado,
        facturas_credito: credito,
        monto_credito: montoCredito,
        notas_credito: notasCredito.length,
        fallidos,
      };
    }

    // Las facturas van primero: las NC referencian el documento asociado, así que si
    // se importan antes quedan sin vínculo con su factura.
    const resVentas = ventas.length > 0 ? await this.procesarVentas(empresaId, ventas) : null;
    const resNc = notasCredito.length > 0 ? await this.procesarNotasCredito(empresaId, notasCredito) : null;

    const cartera = opts.sinCartera
      ? await this.saldarRecuperadas(empresaId, ventas.map((d) => d.cdc))
      : null;

    return {
      cdcs_pedidos: unicos.length,
      recuperados: consultados.length,
      facturas: resVentas,
      notas_credito: resNc,
      cartera_saldada: cartera,
      fallidos,
    };
  }

  /**
   * Deja en cero el saldo de las facturas recuperadas.
   *
   * El XML trae el documento como se emitió, no lo que se cobró después: SIFEN no sabe
   * nada de cobranzas. Entonces una factura a crédito de un período viejo se importa con
   * el saldo completo abierto, y si en la vida real ya se cobró, queda cartera que no
   * existe — el cliente aparece debiendo plata que ya pagó.
   *
   * Cuando el histórico que se recupera es solo para efectos fiscales (Libro IVA Ventas,
   * declaración) y las cobranzas ya se gestionaron por otro lado, conviene saldar todo de
   * entrada. No toca `clientes.saldo_pendiente` porque el import tampoco lo mueve.
   */
  private async saldarRecuperadas(empresaId: string, cdcs: string[]) {
    const validos = cdcs.filter((c) => !!c);
    if (validos.length === 0) return { facturas: 0, cuentas: 0, cuotas: 0 };

    const facturas = await this.prisma.factura_cab.findMany({
      where: { empresa_id: empresaId, cdc: { in: validos } },
      select: { id: true },
    });
    const ids = facturas.map((f) => f.id);
    if (ids.length === 0) return { facturas: 0, cuentas: 0, cuotas: 0 };

    return this.prisma.$transaction(async (tx) => {
      const cuentas = await tx.cuentas_cobrar.updateMany({
        where: { empresa_id: empresaId, factura_venta_id: { in: ids } },
        data: { saldo_pendiente: 0, estado: 'cobrado' },
      });
      const cuotas = await tx.factura_cuotas.updateMany({
        where: { factura_cab_id: { in: ids } },
        data: { saldo_pendiente: 0 },
      });
      const cab = await tx.factura_cab.updateMany({
        where: { id: { in: ids } },
        data: { saldo_pendiente: 0 },
      });
      return { facturas: cab.count, cuentas: cuentas.count, cuotas: cuotas.count };
    });
  }

  /** Consulta un lote de CDC al middleware de SIFEN pidiendo el DE completo. */
  private async consultarCdcSifen(ruc: string, cdcs: string[]): Promise<any[]> {
    const base = (envs.middlewareSifenUrl ?? '').replace(/\/+$/, '');
    if (!base) throw new Error('MIDDLEWARE_SIFEN_URL no está configurado');

    // El router del middleware monta en /api: el resto del backend lo llama igual
    // (`${middlewareUrl}/api/auth`, `/api/maintenance`).
    const res = await fetch(`${base}/api/consultar-cdc`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        // Por RUC y no por id: el middleware corre contra otra base (e_factura_hub),
        // donde los ids de empresa no son los mismos que los del ERP.
        ruc: ruc,
        incluir_xml: true,
        documentos: cdcs.map((cdc) => ({ cdc })),
      }),
    });

    const json: any = await res.json().catch(() => null);
    if (!res.ok || json?.status === 'error') {
      throw new Error(json?.message ?? `Middleware SIFEN respondió ${res.status}`);
    }
    return Array.isArray(json?.response) ? json.response : [];
  }

  /**
   * Convierte el DE que devuelve SIFEN al mismo shape que entrega el bot de Marangatu,
   * que es lo que consumen procesarVentas / procesarNotasCredito.
   */
  private documentoDesdeXmlSifen(cdc: string, item: any): any | null {
    let parsed: any;
    try {
      parsed = new XMLParser({
        ignoreAttributes: false,
        parseTagValue: false, // los importadores hacen su propio Number(): parsear acá
        trimValues: true,     // rompe campos como dNumDoc '0000123' o RUCs con ceros.
      }).parse(String(item.xml_documento));
    } catch (err: any) {
      this.logger.warn(`CDC ${cdc}: XML ilegible (${err?.message})`);
      return null;
    }

    // SIFEN puede devolver el DE envuelto en rDE o suelto, según el documento.
    const contenedor = parsed?.rDE ?? parsed;
    const de = contenedor?.DE;
    if (!de?.gTimb) return null;

    const timb = de.gTimb;
    const gral = de.gDatGralOpe ?? {};
    const emis = gral.gEmis ?? {};
    const rec = gral.gDatRec ?? {};
    const pad = (v: any, n: number) => String(v ?? '').padStart(n, '0');

    const iTiDE = Number(timb.iTiDE ?? 0);
    // Sin DV: personas.ruc es VarChar(8) y el digito verificador va en su propia
    // columna. Pegarlo desborda la columna y el alta de cliente revienta.
    const soloRuc = (ruc: any) => {
      const r = String(ruc ?? '').trim().split('-')[0];
      return r || null;
    };

    return {
      cdc,
      xmlParsed: { DE: de },
      tipoComprobante: iTiDE === 5 ? 'NOTA DE CREDITO' : String(timb.dDesTiDE ?? 'FACTURA'),
      numeroComprobante: `${pad(timb.dEst, 3)}-${pad(timb.dPunExp, 3)}-${pad(timb.dNumDoc, 7)}`,
      fechaEmision: String(gral.dFeEmiDE ?? '').slice(0, 10),
      moneda: String(gral.gOpeCom?.cMoneOpe ?? 'PYG'),
      // El importador resuelve el cliente como `rucVendedor ?? rucReceptor`: eso sirve
      // para compras, donde la contraparte es el proveedor. Acá son ventas emitidas por
      // la propia empresa, asi que la contraparte es el RECEPTOR y los campos de
      // vendedor van vacios — si no, el cliente terminaria siendo la empresa misma.
      rucVendedor: null,
      razonSocialVendedor: null,
      rucReceptor: soloRuc(rec.dRucRec),
      razonSocialReceptor: String(rec.dNomRec ?? ''),
      // El emisor se conserva aparte: procesarVentas lo valida contra la empresa.
      rucEmisor: soloRuc(emis.dRucEm),
      razonSocialEmisor: String(emis.dNomEmi ?? ''),
      // SIFEN solo devuelve el DE si el documento está aprobado, así que llega firme.
      estadoSifen: item.estado ?? 'Aprobado',
      estadoXML: 'OK',
      estado_xml: 'OK',
    };
  }

  async importarVentas(empresaId: string, dto: SyncDocumentosDto) {
    const result = await this.consultarDocumentos(empresaId, 'VENTAS', dto);
    let items: any[] = Array.isArray(result?.data)
      ? result.data
      : Array.isArray(result?.documentos)
        ? result.documentos
        : [];

    if (dto.cdcs && dto.cdcs.length > 0) {
      const set = new Set(dto.cdcs);
      items = items.filter((d) => d?.cdc && set.has(String(d.cdc)));
    }

    return this.procesarVentas(empresaId, items);
  }

  /**
   * Procesa documentos de venta ya traídos y los persiste. Se separó de
   * importarVentas para poder alimentarlo desde otra fuente además del bot de
   * Marangatu — hoy también desde la consulta por CDC contra SIFEN, que sirve para
   * recuperar comprobantes históricos que el bot no tiene sincronizados.
   *
   * Cada documento debe traer al menos: cdc, xmlParsed.DE, tipoComprobante,
   * numeroComprobante y los datos de emisor/receptor.
   */
  private async procesarVentas(empresaId: string, items: any[]) {
    // ── Refs estáticos (carga única) ─────────────────────────────────────
    const [empresa, productoGen, tipoTransaccion, indicadorPresencia, tipoImpuesto, mediosPagoList] = await Promise.all(
      [
        this.prisma.empresas.findUnique({ where: { id: empresaId }, select: { ruc: true } }),
        this.prisma.productos.findFirst({
          where: { empresa_id: empresaId, cod_producto: 'GEN0001', deleted: false },
          select: { id: true },
        }),
        this.prisma.tipo_transaccion.findFirst({ where: { codigo: 1 }, select: { id: true } }),
        this.prisma.indicador_presencia.findFirst({ where: { codigo: 1 }, select: { id: true } }),
        this.prisma.tipo_impuesto.findFirst({ where: { codigo: 1 }, select: { id: true } }),
        this.prisma.medio_pago.findMany({ select: { id: true, codigo: true } }),
      ],
    );
    const mediosPagoMap = new Map(mediosPagoList.map((m) => [m.codigo, m.id]));

    if (!productoGen) {
      throw new Error('Producto genérico GEN0001 no encontrado en la empresa. Créalo antes de importar.');
    }
    if (!empresa?.ruc) {
      throw new Error('La empresa no tiene RUC configurado.');
    }
    const empresaRuc = String(empresa.ruc).trim();

    let importados = 0;
    let omitidos = 0;
    let sinXml = 0;
    let yaExistentes = 0;
    let clientesCreados = 0;
    let rucEmisorNoCoincide = 0;

    for (const doc of items) {
      try {
        const cdc = doc.cdc ?? null;

        // ── Saltar NC (se importan vía importarNotasCredito) ─────────────
        const tipoComprobante = String(doc.tipoComprobante ?? '').toUpperCase();
        const iTiDE = Number(doc.xmlParsed?.DE?.gTimb?.iTiDE ?? 0);
        if (tipoComprobante === 'NOTA DE CREDITO' || iTiDE === 5) {
          continue;
        }

        // ── Saltar si xmlParsed no disponible ────────────────────────────
        if (!doc.xmlParsed) {
          sinXml++;
          continue;
        }

        // ── Saltar si estado XML es PENDIENTE_XML ─────────────────────────
        const estadoXml = (doc.estadoXML ?? doc.estado_xml ?? '').toUpperCase();
        if (estadoXml === 'PENDIENTE_XML') {
          sinXml++;
          continue;
        }

        // ── Idempotencia por CDC ──────────────────────────────────────────
        if (cdc) {
          const existe = await this.prisma.factura_cab.findFirst({
            where: { empresa_id: empresaId, cdc },
            select: { id: true },
          });
          if (existe) {
            yaExistentes++;
            continue;
          }
        }

        // ── Validación NC venta: el emisor debe ser la propia empresa ─────
        // Para Notas de Crédito de ventas, gDatGralOpe.dRucEm tiene que
        // coincidir con empresas.ruc. Esto evita importar como venta una NC
        // que en realidad nos emitió un proveedor (NC compra).
        const tipoComp = String(doc.tipoComprobante ?? '')
          .toUpperCase()
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '');
        if (tipoComp.includes('NOTA DE CREDITO')) {
          const rucEmisor = String(doc.xmlParsed?.DE?.gDatGralOpe?.gEmis?.dRucEm ?? '').trim();
          if (rucEmisor !== empresaRuc) {
            this.logger.warn(
              `NC venta omitida — RUC emisor ${rucEmisor || '(vacío)'} ≠ empresa ${empresaRuc} (CDC ${cdc ?? ''})`,
            );
            rucEmisorNoCoincide++;
            continue;
          }
        }

        // ── Parsear campos del documento ──────────────────────────────────
        const gTimb = doc.xmlParsed?.DE?.gTimb ?? {};
        const gDtipDE = doc.xmlParsed?.DE?.gDtipDE ?? {};
        const gCamCond = gDtipDE.gCamCond ?? {};
        const gPagCred = gCamCond.gPagCred ?? {};
        const gTotSub = doc.xmlParsed?.DE?.gTotSub ?? {};

        const dest = gTimb.dEst ?? '001';
        const dpunexp = gTimb.dPunExp ?? '001';
        const dnumdoc = gTimb.dNumDoc ?? '';
        const fechaEmision = parseDate(doc.fechaEmision) ?? new Date();
        const condOpe = Number(gCamCond.iCondOpe ?? 1); // 1=contado 2=crédito
        const isCredito = condOpe === 2;

        // ── Totales desde gTotSub ─────────────────────────────────────────
        const total = Number(gTotSub.dTotGralOpe ?? 0);
        const sub10 = Number(gTotSub.dSub10 ?? 0);
        const iva10 = Number(gTotSub.dIVA10 ?? 0);
        const baseGrav10 = Number(gTotSub.dBaseGrav10 ?? 0);
        const liqTotIva10 = Number(gTotSub.dLiqTotIVA10 ?? iva10);
        const sub5 = Number(gTotSub.dSub5 ?? 0);
        const iva5 = Number(gTotSub.dIVA5 ?? 0);
        const baseGrav5 = Number(gTotSub.dBaseGrav5 ?? 0);
        const liqTotIva5 = Number(gTotSub.dLiqTotIVA5 ?? iva5);
        const subExe = Number(gTotSub.dSubExe ?? 0);
        const subExo = Number(gTotSub.dSubExo ?? 0);
        const totalIva = iva10 + iva5;
        const totalGs = Number(gTotSub.dTotalGs ?? 0);
        const totDesc = Number(gTotSub.dTotDesc ?? 0);
        const totDescGlotem = Number(gTotSub.dTotDescGlotem ?? 0);
        const totAntItem = Number(gTotSub.dTotAntItem ?? 0);
        const totAnt = Number(gTotSub.dTotAnt ?? 0);
        const porcDescTotal = Number(gTotSub.dPorcDescTotal ?? 0);
        const anticipoSub = Number(gTotSub.dAnticipo ?? 0);
        const redon = Number(gTotSub.dRedon ?? 0);
        const comi = Number(gTotSub.dComi ?? 0);
        const ivaComi = Number(gTotSub.dIVAComi ?? 0);
        const descTotal = Number(gTotSub.dDescTotal ?? 0);

        // ── Crédito: plazo y cuotas ───────────────────────────────────────
        let plazoDias = 0;
        if (isCredito) {
          const plazoStr = String(gPagCred.dPlazoCre ?? '');
          const match = plazoStr.match(/\d+/);
          plazoDias = match ? Number(match[0]) : 30;
        }

        // Cuotas detalladas del XML (modo cuotas)
        const rawCuoXml = gPagCred.gPagCredCuo ?? gPagCred.gPagCuo;
        const cuotasXml: any[] = Array.isArray(rawCuoXml) ? rawCuoXml : rawCuoXml ? [rawCuoXml] : [];
        const nroCuotas = cuotasXml.length > 0 ? cuotasXml.length : Math.max(1, Number(gPagCred.dCuotas ?? 1) || 1);

        // ── Moneda ────────────────────────────────────────────────────────
        const monedaIso = doc.moneda ?? 'PYG';
        const moneda = await this.prisma.moneda.findFirst({
          where: { codigo: monedaIso },
          select: { id: true },
        });

        // ── Condición operación ───────────────────────────────────────────
        const condicion = await this.prisma.condicion_operacion.findFirst({
          where: { codigo: condOpe },
          select: { id: true },
        });

        // ── Cliente ───────────────────────────────────────────────────────
        const rucCliente = doc.rucVendedor ?? doc.rucReceptor ?? null;
        const nombreCliente = doc.razonSocialVendedor ?? doc.razonSocialReceptor ?? `RUC ${rucCliente}`;
        const { cliente, created } = await this.findOrCreateCliente(empresaId, rucCliente, nombreCliente);
        if (created) clientesCreados++;

        // ── Evento de cancelación ─────────────────────────────────────────
        const { esCancelado, mensajeEvento, fechaEvento } = detectarEventoCancelacion(doc);

        // ── Items del documento ───────────────────────────────────────────
        const camItems: any[] = gDtipDE.gCamItem ?? [];

        await this.prisma.$transaction(async (tx) => {
          // 1. factura_cab
          const factura = await tx.factura_cab.create({
            data: {
              empresa_id: empresaId,
              cliente_id: cliente.id,
              dest,
              dpunexp,
              dnumdoc,
              dfeemide: fechaEmision,
              cdc,
              estado: esCancelado ? 'Anulado' : 'Aprobado',
              estado_sifen: esCancelado ? 'Cancelado' : (doc.estadoSifen ?? 'Aprobado'),
              evento_aplicado: esCancelado ? 'ECAN' : null,
              estado_evento: esCancelado ? 'Aprobado' : null,
              mensaje_evento: esCancelado ? mensajeEvento : null,
              fecha_evento: esCancelado ? fechaEvento : null,
              condicion_operacion_id: condicion?.id ?? null,
              tipo_transaccion_id: tipoTransaccion?.id ?? null,
              indicador_presencia_id: indicadorPresencia?.id ?? null,
              tipo_impuesto_id: tipoImpuesto?.id ?? null,
              moneda_id: moneda?.id ?? null,
              icondcred: isCredito ? Number(gPagCred.iCondCred ?? 1) : null,
              dplazocre: isCredito ? String(gPagCred.dPlazoCre ?? '') : null,
              dcuotas: isCredito ? nroCuotas : null,
              total_factura: total,
              saldo_pendiente: isCredito && !esCancelado ? total : 0,
              dinfadic: `MARANGATU_VENTA | CDC:${cdc ?? ''} | ${doc.numeroComprobante ?? ''}`,
            },
          });

          // 2. factura_det — un registro por ítem (producto GEN0001, descripción real)
          let itemNum = 1;
          if (camItems.length > 0) {
            for (const it of camItems) {
              const gVal = it.gValorItem ?? {};
              const gIVA = it.gCamIVA ?? {};
              const gResta = gVal.gValorRestaItem ?? {};
              const desc = it.dDesProSer ?? 'Ítem importado';
              const cantidad = Number(it.dCantProSer ?? 1);
              const uniPro = Number(gVal.dPUniProSer ?? 0);
              const totBruto = Number(gVal.dTotBruOpeItem ?? 0);
              const totNeto = Number(gResta.dTotOpeItem ?? totBruto);
              const totOpeGs = Number(gResta.dTotOpeGs ?? gVal.dTotOpeGs ?? 0) || null;
              const tasaIva = Number(gIVA.dTasaIVA ?? 10);
              const basGrav = Number(gIVA.dBasGravIVA ?? 0);
              const liqIva = Number(gIVA.dLiqIVAItem ?? 0);
              const iAfecIva = Number(gIVA.iAfecIVA ?? 1);
              const basExe = Number(gIVA.dBasExe ?? 0) || null;
              const descItem = Number(gVal.dDescItem ?? 0) || null;
              const porcDesIt = Number(gVal.dPorcDesIt ?? 0) || null;
              const descGloItem = Number(gVal.dDescGloItem ?? 0) || null;
              const antPreUniIt = Number(gVal.dAntPreUniIt ?? 0) || null;
              const antGloPre = Number(gVal.dAntGloPreUniIt ?? 0) || null;
              const tiCamIt = Number(it.dTiCamIt ?? 0) || null;

              await tx.factura_det.create({
                data: {
                  factura_cab_id: factura.id,
                  producto_id: productoGen.id,
                  ddesproser: desc,
                  cunimed: it.dDesUniMed ?? 'UNI',
                  dcantproser: cantidad,
                  duniproser: uniPro,
                  cpaisorig: it.cPaisOrig ?? null,
                  dinfitem: it.dInfItem ?? null,
                  dcdcanticipo: it.dCDCAnticipo ?? null,
                  dticamit: tiCamIt,
                  dtotbruopeitem: totBruto,
                  ddescitem: descItem,
                  dporcdesit: porcDesIt,
                  ddescgloitem: descGloItem,
                  dantpreuniit: antPreUniIt,
                  dantglopreuniit: antGloPre,
                  dtotopeitem: totNeto,
                  dtotopegs: totOpeGs,
                  iafeciva: iAfecIva,
                  dpropiva: Number(gIVA.dPropIVA ?? 100),
                  dtasiva: tasaIva,
                  dbasgraviva: basGrav,
                  dliqivaitem: liqIva,
                  dbasexe: basExe,
                  item: itemNum++,
                },
              });
            }
          } else {
            // Sin ítems en XML: crear uno genérico con el total
            await tx.factura_det.create({
              data: {
                factura_cab_id: factura.id,
                producto_id: productoGen.id,
                ddesproser: `Factura Electrónica Marangatu · ${nombreCliente}`,
                cunimed: 'UNI',
                dcantproser: 1,
                duniproser: total,
                dtotbruopeitem: total,
                dtotopeitem: total,
                iafeciva: 1,
                dpropiva: 100,
                dtasiva: 10,
                dbasgraviva: Math.round(total / 1.1),
                dliqivaitem: total - Math.round(total / 1.1),
                item: 1,
              },
            });
          }

          // 3. factura_subtotales
          await tx.factura_subtotales.create({
            data: {
              factura_cab_id: factura.id,
              dsub10: sub10,
              dbasegrav10: baseGrav10,
              diva10: iva10,
              dliqtotiva10: liqTotIva10,
              dsub5: sub5,
              dbasegrav5: baseGrav5,
              diva5: iva5,
              dliqtotiva5: liqTotIva5,
              dsubexe: subExe,
              dsubexo: subExo,
              dtotope: total,
              dtotgralope: total,
              dtbasgraiva: baseGrav10 + baseGrav5,
              dtotiva: totalIva,
              dtotalgs: totalGs || null,
              dtotdesc: totDesc,
              dtotdescglotem: totDescGlotem,
              dtotantitem: totAntItem,
              dtotant: totAnt,
              dporcdesctotal: porcDescTotal,
              danticipo: anticipoSub,
              dredon: redon,
              dcomi: comi,
              divacomi: ivaComi,
              ddesctotal: descTotal,
            },
          });

          // 4. Formas de pago (contado) desde gPaConEIni
          if (!isCredito) {
            const rawPagos = gCamCond.gPaConEIni ?? [];
            const pagosArr: any[] = Array.isArray(rawPagos) ? rawPagos : rawPagos ? [rawPagos] : [];
            for (const p of pagosArr) {
              const tiPago = Number(p.iTiPago ?? 1);
              await tx.factura_forma_pagos.create({
                data: {
                  factura_cab_id: factura.id,
                  empresa_id: empresaId,
                  medio_pago_id: mediosPagoMap.get(tiPago) ?? null,
                  dmontipag: Number(p.dMonTiPag ?? total),
                  cmonetipag: p.cMoneTiPag ?? monedaIso,
                  dticamtipag: p.dTiCamTiPag ? Number(p.dTiCamTiPag) : null,
                  dnomtit: p.dNomTit ?? null,
                  dnumtarj: p.dNumTarj ? String(p.dNumTarj).slice(-4) : null,
                  dcodauope: p.dCodAuOpe ? Number(p.dCodAuOpe) : null,
                  ddesdentarj: p.dDenTarj ?? null,
                  dnumcheq: p.dNumCheq ?? null,
                  dbcoemi: p.dBcoEmi ?? null,
                  drsprotar: p.dRSProTar ?? null,
                  drucprotar: p.dRucProTar ?? null,
                },
              });
            }
          }

          // 5. Si crédito y no cancelado → cuentas_cobrar + factura_cuotas
          if (isCredito && !esCancelado) {
            const fechaVenc = new Date(fechaEmision);
            fechaVenc.setDate(fechaVenc.getDate() + plazoDias);

            const cuenta = await tx.cuentas_cobrar.create({
              data: {
                empresa_id: empresaId,
                cliente_id: cliente.id,
                factura_venta_id: factura.id,
                fecha_emision: fechaEmision,
                fecha_vencimiento: fechaVenc,
                moneda_iso: monedaIso,
                monto_total: total,
                saldo_pendiente: total,
                estado: 'pendiente',
              },
            });

            if (cuotasXml.length > 0) {
              // Cuotas detalladas del XML
              await tx.factura_cuotas.createMany({
                data: cuotasXml.map((q: any, i: number) => ({
                  factura_cab_id: factura.id,
                  cuenta_id: cuenta.id,
                  nro_cuota: Number(q.dNroCuo ?? q.dNroCuota ?? i + 1),
                  cmonecuo: monedaIso,
                  dmoncuota: Number(q.dMontCuota ?? q.dMontCuo ?? 0),
                  saldo_pendiente: Number(q.dMontCuota ?? q.dMontCuo ?? 0),
                  dvenccuo: parseDate(q.dVencCuo) ?? fechaVenc,
                  estado: 'pendiente',
                })),
              });
            } else {
              // Una sola cuota con el plazo calculado
              await tx.factura_cuotas.create({
                data: {
                  factura_cab_id: factura.id,
                  cuenta_id: cuenta.id,
                  nro_cuota: 1,
                  cmonecuo: monedaIso,
                  dmoncuota: total,
                  saldo_pendiente: total,
                  dvenccuo: fechaVenc,
                  estado: 'pendiente',
                },
              });
            }
          }
        });

        importados++;
      } catch (err: any) {
        this.logger.warn(`Error importando venta ${doc.numeroComprobante}: ${err.message}`);
        omitidos++;
      }
    }

    await this.configService.updateSyncTimestamp(empresaId, 'ventas');
    return {
      importados,
      omitidos,
      sinXml,
      yaExistentes,
      clientesCreados,
      rucEmisorNoCoincide,
      total: items.length,
    };
  }

  private async findOrCreateCliente(
    empresaId: string,
    ruc: string | null,
    razonSocial: string,
  ): Promise<{ cliente: any; created: boolean }> {
    let created = false;

    // Buscar persona por RUC o razón social
    let persona = ruc
      ? await this.prisma.personas.findFirst({ where: { empresa_id: empresaId, ruc, deleted: false } })
      : null;

    if (!persona && razonSocial) {
      persona = await this.prisma.personas.findFirst({
        where: { empresa_id: empresaId, razon_social: { equals: razonSocial, mode: 'insensitive' }, deleted: false },
      });
    }

    if (!persona) {
      const naturaleza = await this.prisma.naturaleza_receptor.findFirst({
        where: { codigo: ruc ? 1 : 2 },
        select: { id: true },
      });
      persona = await this.prisma.personas.create({
        data: {
          empresa_id: empresaId,
          razon_social: razonSocial,
          ruc: ruc || null,
          naturaleza_id: naturaleza?.id ?? null,
          active: true,
          deleted: false,
        },
      });
    }

    // Buscar o crear cliente
    let cliente = await this.prisma.clientes.findFirst({
      where: { persona_id: persona.id, deleted: false },
    });

    if (!cliente) {
      const tipoOp = await this.prisma.tipo_operacion.findFirst({
        where: { codigo: ruc ? 1 : 2 }, // B2B=1 si tiene RUC, B2C=2 si no
        select: { id: true },
      });
      cliente = await this.prisma.clientes.create({
        data: {
          persona_id: persona.id,
          tipo_operacion_id: tipoOp?.id ?? null,
          active: true,
          deleted: false,
        },
      });
      created = true;
      this.logger.log(`Cliente creado: ${razonSocial} (RUC ${ruc ?? 'sin RUC'})`);
    }

    return { cliente, created };
  }

  private async getNextNumeroOrdenPago(tx: any, empresaId: string, sucursalId: string): Promise<string> {
    const rows = await tx.$queryRaw<
      Array<{
        id: string;
        numero_actual: number;
        numero_final: number | null;
        punto_expedicion: string | null;
        punto_establecimiento: string | null;
      }>
    >`
      SELECT nd.id, nd.numero_actual, nd.numero_final, epe.punto_expedicion, es.punto_establecimiento
      FROM numeraciones_documento nd
      INNER JOIN tipo_documento td ON td.id = nd.tipo_documento_id
      INNER JOIN empresas_puntos_expedicion epe ON epe.id = nd.punto_expedicion_id
      INNER JOIN empresas_sucursales es ON es.id = epe.sucursal_id
      WHERE td.codigo = 400
        AND nd.empresa_id = ${empresaId}::uuid
        AND epe.sucursal_id = ${sucursalId}::uuid
        AND nd.active = true
      ORDER BY nd.created_at ASC
      LIMIT 1
      FOR UPDATE
    `;
    if (!rows.length) {
      // Fallback: timestamp-based numero when no numeración configured
      return `MAR-${Date.now()}`;
    }
    const n = rows[0];
    const num = Number(n.numero_actual || 0);
    const dest = (n.punto_establecimiento || '001').padStart(3, '0').slice(0, 3);
    const pexp = (n.punto_expedicion || '001').padStart(3, '0').slice(0, 3);
    const numeroOrden = `${dest}-${pexp}-${String(num).padStart(7, '0')}`;
    await tx.numeraciones_documento.update({ where: { id: n.id }, data: { numero_actual: num + 1 } });
    return numeroOrden;
  }

  private async resolveDeposito(empresaId: string, depositoId?: string | null) {
    if (depositoId) {
      const d = await this.prisma.depositos.findFirst({
        where: { id: depositoId, empresa_id: empresaId, active: true },
      });
      if (d) return d;
    }
    const principal = await this.prisma.depositos.findFirst({
      where: { empresa_id: empresaId, es_principal: true, active: true },
    });
    if (principal) return principal;
    const any = await this.prisma.depositos.findFirst({ where: { empresa_id: empresaId, active: true } });
    if (any) return any;
    throw new Error('No hay depósito disponible para la empresa');
  }

  /** Busca proveedor por RUC → razon_social → crea si no existe */
  private async findOrCreateProveedor(
    empresaId: string,
    ruc: string,
    razonSocial: string,
  ): Promise<{ proveedor: any; created: boolean }> {
    let created = false;

    // 1. Buscar persona por RUC exacto
    let persona = ruc
      ? await this.prisma.personas.findFirst({ where: { empresa_id: empresaId, ruc, deleted: false } })
      : null;

    // 2. Fallback: buscar por razon_social si no hay RUC coincidente
    if (!persona && razonSocial) {
      persona = await this.prisma.personas.findFirst({
        where: { empresa_id: empresaId, razon_social: { equals: razonSocial, mode: 'insensitive' }, deleted: false },
      });
    }

    // 3. Crear persona si no existe
    if (!persona) {
      persona = await this.prisma.personas.create({
        data: { empresa_id: empresaId, razon_social: razonSocial, ruc: ruc || null, active: true, deleted: false },
      });
    }

    // 4. Buscar proveedor vinculado a la persona
    let proveedor = await this.prisma.proveedores.findFirst({
      where: { empresa_id: empresaId, persona_id: persona.id },
    });

    // 5. Crear proveedor si no existe
    if (!proveedor) {
      proveedor = await this.prisma.proveedores.create({
        data: {
          empresa_id: empresaId,
          persona_id: persona.id,
          activo: true,
          observaciones: 'Creado automáticamente · importación Marangatu',
        },
      });
      created = true;
      this.logger.log(`Proveedor creado: ${razonSocial} (RUC ${ruc})`);
    }

    return { proveedor, created };
  }

  /** Busca producto por cod_producto → descripcion (insensitive); null si no existe */
  private async findProductoByCodOrDesc(empresaId: string, cod: string | null, desc: string | null) {
    if (!cod && !desc) return null;

    if (cod) {
      const p = await this.prisma.productos.findFirst({
        where: { empresa_id: empresaId, cod_producto: cod, deleted: false, active: true },
        select: { id: true, maneja_inventario: true },
      });
      if (p) return p;
    }

    if (desc) {
      return this.prisma.productos.findFirst({
        where: {
          empresa_id: empresaId,
          descripcion: { equals: desc, mode: 'insensitive' },
          deleted: false,
          active: true,
        },
        select: { id: true, maneja_inventario: true },
      });
    }

    return null;
  }

  // ─────────────────────────────────────────────────────────────────────────
  //  IMPORTAR NOTAS DE CRÉDITO (VENTAS) — crea nota_credito_cab + det + subtotal
  //  - Sin stock ni asientos contables
  //  - Descuenta saldo_disponible de la factura asociada (vía dCdCDERef)
  //  - Hereda cliente_id de la factura asociada
  //  - Idempotente por CDC; omite si factura asociada no existe
  // ─────────────────────────────────────────────────────────────────────────
  async importarNotasCredito(empresaId: string, dto: SyncDocumentosDto) {
    // Las NC que la empresa EMITE vienen en el feed con tipoRegistro=COMPRA.
    // Por eso consultamos COMPRAS y filtramos solo NC dentro del loop.
    const result: any = await this.consultarDocumentos(empresaId, 'COMPRAS', dto);
    let items: any[] = Array.isArray(result?.data)
      ? result.data
      : Array.isArray(result?.documentos)
        ? result.documentos
        : [];

    if (dto.cdcs && dto.cdcs.length > 0) {
      const set = new Set(dto.cdcs);
      items = items.filter((d) => d?.cdc && set.has(String(d.cdc)));
    }

    return this.procesarNotasCredito(empresaId, items);
  }

  /**
   * Procesa notas de crédito ya traídas. Separado de importarNotasCredito por el
   * mismo motivo que procesarVentas: permitir alimentarlo desde la consulta por CDC
   * contra SIFEN además del feed del bot de Marangatu.
   */
  private async procesarNotasCredito(empresaId: string, items: any[]) {
    const productoGen = await this.prisma.productos.findFirst({
      where: { empresa_id: empresaId, cod_producto: 'GEN0001', deleted: false },
      select: { id: true },
    });
    if (!productoGen) {
      throw new Error('Producto genérico GEN0001 no encontrado en la empresa. Créalo antes de importar.');
    }

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { ruc: true },
    });
    if (!empresa?.ruc) {
      throw new Error('La empresa no tiene RUC configurado.');
    }
    const empresaRuc = String(empresa.ruc).trim();

    let importadas = 0;
    let omitidas = 0;
    let sinXml = 0;
    let yaExistentes = 0;
    let rucEmisorNoCoincide = 0;
    const omitidasDetalle: Array<{ cdc: string | null; numero: string | null; motivo: string }> = [];

    for (const doc of items) {
      const cdc = doc.cdc ?? null;
      const numero = doc.numeroComprobante ?? null;

      try {
        // ── Filtrar solo NC ─────────────────────────────────────────────
        const tipoComprobante = String(doc.tipoComprobante ?? '')
          .toUpperCase()
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '');
        const iTiDE = Number(doc.xmlParsed?.DE?.gTimb?.iTiDE ?? 0);
        if (tipoComprobante !== 'NOTA DE CREDITO' && iTiDE !== 5) continue;

        // ── Validación: la NC de venta debe haberla emitido la propia empresa ──
        // gDatGralOpe.dRucEm tiene que coincidir con empresas.ruc. Si no, es
        // una NC de COMPRA (un proveedor nos la emitió) que el bot devolvió
        // cruzada en el feed de VENTAS — la omitimos.
        const rucEmisor = String(doc.xmlParsed?.DE?.gDatGralOpe?.gEmis?.dRucEm ?? '').trim();
        if (rucEmisor !== empresaRuc) {
          this.logger.warn(
            `NC venta omitida — RUC emisor ${rucEmisor || '(vacío)'} ≠ empresa ${empresaRuc} (CDC ${cdc ?? ''})`,
          );
          rucEmisorNoCoincide++;
          omitidasDetalle.push({
            cdc,
            numero,
            motivo: `NC omitida — emitida por RUC ${rucEmisor || '(vacío)'}, no por la empresa ${empresaRuc} (probablemente es NC de compra)`,
          });
          continue;
        }

        if (!doc.xmlParsed) {
          sinXml++;
          continue;
        }
        const estadoXml = (doc.estadoXML ?? doc.estado_xml ?? '').toUpperCase();
        if (estadoXml === 'PENDIENTE_XML') {
          sinXml++;
          continue;
        }

        // ── Idempotencia por CDC ───────────────────────────────────────
        if (cdc) {
          const existe = await this.prisma.nota_credito_cab.findFirst({
            where: { empresa_id: empresaId, cdc },
            select: { id: true },
          });
          if (existe) {
            yaExistentes++;
            continue;
          }
        }

        // ── Buscar factura asociada por CDC en gCamDEAsoc ──────────────
        const gCamDEAsocRaw = doc.xmlParsed?.DE?.gCamDEAsoc;
        const gCamDEAsocList: any[] = Array.isArray(gCamDEAsocRaw)
          ? gCamDEAsocRaw
          : gCamDEAsocRaw
            ? [gCamDEAsocRaw]
            : [];
        const cdcRef = gCamDEAsocList[0]?.dCdCDERef ?? null;
        if (!cdcRef) {
          omitidas++;
          omitidasDetalle.push({ cdc, numero, motivo: 'NC sin CDC asociado en gCamDEAsoc' });
          continue;
        }

        const facturaAsociada = await this.prisma.factura_cab.findFirst({
          where: { empresa_id: empresaId, cdc: cdcRef },
        });

        // Si la factura no está en el ERP (típico al importar NC anteriores a la fecha de
        // corte de migración), importamos igualmente la NC sin factura_cab_id, para que
        // quede disponible para aplicarse desde Recibos Multi-Facturas.
        let clienteIdResuelto: string | null = facturaAsociada?.cliente_id ?? null;
        if (!facturaAsociada) {
          const rucReceptor =
            doc.xmlParsed?.DE?.gDatGralOpe?.gDatRec?.dRucRec ??
            doc.xmlParsed?.DE?.gDatGralOpe?.gDatRec?.dRUCRec ??
            null;
          if (rucReceptor) {
            const rucStr = String(rucReceptor).trim();
            const rucBase = rucStr.split('-')[0]; // RUC sin DV
            const cli = await this.prisma.clientes.findFirst({
              where: {
                personas: {
                  empresa_id: empresaId,
                  OR: [{ ruc: rucBase }, { ruc: rucStr }, { nro_documento: rucStr }, { nro_documento: rucBase }],
                },
              },
              select: { id: true },
            });
            clienteIdResuelto = cli?.id ?? null;
          }
          omitidasDetalle.push({
            cdc,
            numero,
            motivo: 'NC importada sin factura asociada (disponible para Recibos Multi-Facturas)',
          });
        }

        // ── Parsear NC ───────────────────────────────────────────────────
        const gTimb = doc.xmlParsed?.DE?.gTimb ?? {};
        const gDtipDE = doc.xmlParsed?.DE?.gDtipDE ?? {};
        const gTotSub = doc.xmlParsed?.DE?.gTotSub ?? {};

        const dest = gTimb.dEst ?? '001';
        const dpunexp = gTimb.dPunExp ?? '001';
        const dnumdoc = gTimb.dNumDoc ?? '';
        const fechaEmision = parseDate(doc.fechaEmision) ?? new Date();

        // Totales
        const totalNC = Number(gTotSub.dTotGralOpe ?? 0);
        const sub5 = Number(gTotSub.dSub5 ?? 0);
        const sub10 = Number(gTotSub.dSub10 ?? 0);
        const iva5 = Number(gTotSub.dIVA5 ?? 0);
        const iva10 = Number(gTotSub.dIVA10 ?? 0);
        const liqTotIva5 = Number(gTotSub.dLiqTotIVA5 ?? iva5);
        const liqTotIva10 = Number(gTotSub.dLiqTotIVA10 ?? iva10);
        const baseGrav5 = Number(gTotSub.dBaseGrav5 ?? 0);
        const baseGrav10 = Number(gTotSub.dBaseGrav10 ?? 0);
        const subExe = Number(gTotSub.dSubExe ?? 0);
        const subExo = Number(gTotSub.dSubExo ?? 0);
        const totIva = iva5 + iva10;
        const totGralOpe = totalNC;
        const totalGs = Number(gTotSub.dTotalGs ?? 0);
        const totDesc = Number(gTotSub.dTotDesc ?? 0);
        const totDescGlotem = Number(gTotSub.dTotDescGlotem ?? 0);
        const totAntItem = Number(gTotSub.dTotAntItem ?? 0);
        const totAnt = Number(gTotSub.dTotAnt ?? 0);
        const porcDescTotal = Number(gTotSub.dPorcDescTotal ?? 0);
        const anticipoSub = Number(gTotSub.dAnticipo ?? 0);
        const redon = Number(gTotSub.dRedon ?? 0);
        const comi = Number(gTotSub.dComi ?? 0);
        const ivaComi = Number(gTotSub.dIVAComi ?? 0);
        const totOpe = Number(gTotSub.dTotOpe ?? totalNC);

        // Moneda
        const monedaIso = doc.moneda ?? doc.xmlParsed?.DE?.gDatGralOpe?.gOpeCom?.cMoneOpe ?? 'PYG';
        const moneda = await this.prisma.moneda.findFirst({
          where: { codigo: monedaIso },
          select: { id: true },
        });

        const camItems: any[] = gDtipDE.gCamItem ?? [];

        await this.prisma.$transaction(async (tx) => {
          // 1. nota_credito_cab
          const ncCab = await tx.nota_credito_cab.create({
            data: {
              empresa_id: empresaId,
              cliente_id: clienteIdResuelto,
              factura_cab_id: facturaAsociada?.id ?? null,
              moneda_id: moneda?.id ?? null,
              dest,
              dpunexp,
              dnumdoc,
              dfeemide: fechaEmision,
              cdc,
              estado: 'Aprobado',
              estado_sifen: doc.estadoSifen ?? 'Aprobado',
              mensaje_sifen: doc.mensajeSifen ?? null,
              fecha_registro_sifen: doc.fechaProceso ? new Date(doc.fechaProceso) : null,
              rec_modo_aplicacion: facturaAsociada ? 'ESTRICTO' : 'FLEXIBLE',
              dinfadic:
                `MARANGATU_NC | CDC:${cdc ?? ''} | ${numero ?? ''}` +
                (facturaAsociada ? '' : ' | sin factura asociada'),
            },
          });

          // 2. nota_credito_det
          let itemNum = 1;
          if (camItems.length > 0) {
            for (const it of camItems) {
              const gVal = it.gValorItem ?? {};
              const gIVA = it.gCamIVA ?? {};
              const gResta = gVal.gValorRestaItem ?? {};
              await tx.nota_credito_det.create({
                data: {
                  nota_credito_cab_id: ncCab.id,
                  producto_id: productoGen.id,
                  ddesproser: it.dDesProSer ?? 'Ítem NC',
                  cunimed: it.dDesUniMed ?? 'UNI',
                  dcantproser: Number(it.dCantProSer ?? 1),
                  duniproser: Number(gVal.dPUniProSer ?? 0),
                  dtotbruopeitem: Number(gVal.dTotBruOpeItem ?? 0),
                  dtotopeitem: Number(gResta.dTotOpeItem ?? gVal.dTotBruOpeItem ?? 0),
                  iafeciva: Number(gIVA.iAfecIVA ?? 1),
                  dpropiva: Number(gIVA.dPropIVA ?? 100),
                  dtasiva: Number(gIVA.dTasaIVA ?? 10),
                  dbasgraviva: Number(gIVA.dBasGravIVA ?? 0),
                  dliqivaitem: Number(gIVA.dLiqIVAItem ?? 0),
                  dbasexe: Number(gIVA.dBasExe ?? 0) || null,
                  ddescitem: Number(gVal.dDescItem ?? 0) || null,
                  dporcdesit: Number(gVal.dPorcDesIt ?? 0) || null,
                  dinfitem: it.dInfItem ?? null,
                  item: itemNum++,
                },
              });
            }
          }

          // 3. nota_credito_subtotal
          await tx.nota_credito_subtotal.create({
            data: {
              nota_credito_cab_id: ncCab.id,
              dsubexe: subExe,
              dsubexo: subExo,
              dsub5: sub5,
              dsub10: sub10,
              dtotope: totOpe,
              dtotdesc: totDesc,
              dtotdescglotem: totDescGlotem,
              dtotantitem: totAntItem,
              dtotant: totAnt,
              dporcdesctotal: porcDescTotal,
              danticipo: anticipoSub,
              dredon: redon,
              dcomi: comi,
              dtotgralope: totGralOpe,
              diva5: iva5,
              diva10: iva10,
              dliqtotiva5: liqTotIva5,
              dliqtotiva10: liqTotIva10,
              divacomi: ivaComi,
              dtotiva: totIva,
              dbasegrav5: baseGrav5,
              dbasegrav10: baseGrav10,
              dtbasgraiva: baseGrav5 + baseGrav10,
              dtotalgs: totalGs || null,
            },
          });

          // 4. Descontar saldo en factura_cab (cabecera únicamente) — solo si la factura
          // está en el ERP. Si la NC se importa huérfana (caso migración con NC
          // anteriores), queda con saldo libre para aplicarse desde Recibos Multi-Facturas.
          if (facturaAsociada) {
            const totalFactura = Number(facturaAsociada.total_factura ?? 0);
            const totalNCActual = Number((facturaAsociada as any).total_notas_credito ?? 0);
            const nuevoTotalNC = totalNCActual + totalNC;

            await tx.factura_cab.update({
              where: { id: facturaAsociada.id },
              data: {
                total_notas_credito: nuevoTotalNC,
                saldo_disponible: totalFactura - nuevoTotalNC,
                updated_at: new Date(),
              } as any,
            });
          }
        });

        importadas++;
      } catch (err: any) {
        this.logger.warn(`Error importando NC ${doc.numeroComprobante}: ${err.message}`);
        omitidas++;
        omitidasDetalle.push({ cdc, numero, motivo: err.message ?? 'error desconocido' });
      }
    }

    return {
      importadas,
      omitidas,
      sinXml,
      yaExistentes,
      rucEmisorNoCoincide,
      total: items.length,
      omitidasDetalle,
    };
  }

  @Cron('0 */6 * * *')
  async cronSyncCompras() {
    const now = new Date();
    const configs = await this.prisma.marangatu_empresa_config.findMany({
      where: { activo_compras: true },
    });

    for (const cfg of configs) {
      try {
        await this.triggerSync(cfg.empresa_id, 'COMPRAS', {
          anho: now.getFullYear(),
          mes: now.getMonth() + 1,
        });
      } catch (err) {
        this.logger.error(`Cron sync COMPRAS empresa ${cfg.empresa_id}: ${err.message}`);
      }
    }
  }
}
