import AdmZip from 'adm-zip';
import { XMLParser } from 'fast-xml-parser';
import type { TipoCfdi, EstadoCfdi } from '@horux/shared';

interface CfdiParsed {
  uuid: string;
  type: TipoCfdi;
  tipoComprobante: string;
  serie: string | null;
  folio: string | null;
  status: EstadoCfdi;
  fechaEmision: Date;
  fechaCertSat: Date | null;
  rfcEmisor: string;
  nombreEmisor: string;
  rfcReceptor: string;
  nombreReceptor: string;
  subtotal: number;
  descuento: number;
  total: number;
  moneda: string;
  tipoCambio: number;
  metodoPago: string | null;
  formaPago: string | null;
  usoCfdi: string | null;
  pac: string | null;
  // Impuestos del comprobante
  ivaTraslado: number;
  isrRetencion: number;
  ivaRetencion: number;
  iepsTraslado: number;
  iepsRetencion: number;
  // Impuestos locales
  impuestosLocalesTrasladado: number;
  impuestosLocalesRetenidos: number;
  // Complemento de pagos
  montoPago: number;
  fechaPagoP: string | null;
  numParcialidad: string | null;
  uuidRelacionado: string | null;
  saldoInsoluto: string | null;
  isrRetencionPago: number;
  ivaTrasladoPago: number;
  ivaRetencionPago: number;
  iepsTrasladoPago: number;
  iepsRetencionPago: number;
  // Nómina
  fechaPago: string | null;
  fechaInicialPago: string | null;
  fechaFinalPago: string | null;
  numDiasPagados: number;
  numSeguroSocial: string | null;
  puesto: string | null;
  salarioBaseCotApor: number;
  salarioDiarioIntegrado: number;
  totalPercepciones: number;
  totalDeducciones: number;
  impRetenidosNomina: number;
  otrasDeduccionesNomina: number;
  subsidioCausado: number;

  regimenFiscalEmisor: string | null;
  regimenFiscalReceptor: string | null;
  codigoPostalReceptor: string | null;
  // CfdiRelacionados a nivel raíz del comprobante (CFDI 4.0).
  // `cfdiTipoRelacion` — clave SAT (01..07). NULL si no hay relación.
  // `cfdisRelacionados` — UUIDs pipe-separated.
  cfdiTipoRelacion: string | null;
  cfdisRelacionados: string | null;
  conceptos: ConceptoParsed[];
  xmlOriginal: string;

  // Factura global (InformacionGlobal)
  periodicidad: string | null;
  mesesGlobal: string | null;
  añoGlobal: string | null;
}

interface ConceptoParsed {
  claveProdServ: string | null;
  noIdentificacion: string | null;
  descripcion: string;
  cantidad: number;
  claveUnidad: string | null;
  unidad: string | null;
  valorUnitario: number;
  importe: number;
  descuento: number;
  // Impuestos por concepto
  isrRetencion: number;
  ivaTraslado: number;
  ivaRetencion: number;
  iepsTraslado: number;
  iepsRetencion: number;
}

interface ExtractedXml {
  filename: string;
  content: string;
}

const xmlParser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: '@_',
  removeNSPrefix: true,
});

/**
 * Extrae archivos XML de un paquete ZIP en base64
 */
export function extractXmlsFromZip(zipBase64: string): ExtractedXml[] {
  const zipBuffer = Buffer.from(zipBase64, 'base64');
  const zip = new AdmZip(zipBuffer);
  const entries = zip.getEntries();

  const xmlFiles: ExtractedXml[] = [];

  for (const entry of entries) {
    if (entry.entryName.toLowerCase().endsWith('.xml')) {
      let content = entry.getData().toString('utf-8');
      // Remover UTF-8 BOM si existe — fast-xml-parser no lo maneja y devuelve
      // result.Comprobante = undefined, dejando el CFDI sin parsear.
      if (content.charCodeAt(0) === 0xFEFF) {
        content = content.slice(1);
      }
      xmlFiles.push({
        filename: entry.entryName,
        content,
      });
    }
  }

  return xmlFiles;
}

/**
 * Parsea una fecha del XML/CSV del SAT preservando la hora **literal**.
 *
 * Problema: el CFDI 4.0 define `Fecha` y `FechaTimbrado` como ISO-8601 sin
 * zona horaria (hora local del contribuyente = México). Si se pasa tal cual
 * a `new Date(str)`, Node lo interpreta según la timezone de la máquina:
 * en CDMX (UTC-6), "2025-12-31T18:37:51" se convierte a UTC
 * "2026-01-01T00:37:51Z", cambiando la fecha efectiva y desalineando el
 * mes/año del CFDI. Postgres guarda ese valor UTC, y los filtros por rango
 * lo sacan del mes correcto.
 *
 * Solución: forzar 'Z' si el string no trae TZ indicator. Esto hace que
 * Node interprete el texto como UTC literal y preserve la hora tal cual.
 * El valor queda naive pero consistente: todo el sistema filtra con
 * fechas naive (sin TZ), así que el resultado es correcto.
 */
function parseCfdiDate(str: string | null | undefined): Date {
  if (!str) return new Date(0);
  let s = String(str).trim();
  if (!s) return new Date(0);
  // Defensa: el SAT a veces concatena múltiples fechas con '|' (ej. en
  // FechaTimbrado duplicado). Tomamos solo la primera fecha válida.
  if (s.includes('|')) {
    s = s.split('|')[0].trim();
  }
  const hasTz = /[Zz]|[+-]\d{2}:?\d{2}$/.test(s);
  return new Date(hasTz ? s : s + 'Z');
}

function toArray(val: any): any[] {
  if (!val) return [];
  return Array.isArray(val) ? val : [val];
}

function pf(val: any): number {
  return parseFloat(val || '0') || 0;
}

/**
 * Extrae el UUID del TimbreFiscalDigital
 */
function getFirstTimbre(comprobante: any): any {
  const timbre = comprobante.Complemento?.TimbreFiscalDigital;
  if (!timbre) return null;
  return Array.isArray(timbre) ? timbre[0] : timbre;
}

/**
 * Extrae el UUID del TimbreFiscalDigital
 */
function extractUuid(comprobante: any): string {
  const timbre = getFirstTimbre(comprobante);
  return timbre?.['@_UUID'] || '';
}

/**
 * Extrae datos del timbre: fecha cert SAT y PAC
 */
function extractTimbreData(comprobante: any): { fechaCertSat: Date | null; pac: string | null } {
  const timbre = getFirstTimbre(comprobante);
  if (!timbre) return { fechaCertSat: null, pac: null };

  return {
    fechaCertSat: timbre['@_FechaTimbrado'] ? parseCfdiDate(timbre['@_FechaTimbrado']) : null,
    pac: timbre['@_RfcProvCertif'] || null,
  };
}

/**
 * Extrae impuestos trasladados (IVA 002, IEPS 003)
 */
function extractTraslados(comprobante: any): { iva: number; ieps: number } {
  const traslados = toArray(comprobante.Impuestos?.Traslados?.Traslado);
  let iva = 0, ieps = 0;

  for (const t of traslados) {
    const importe = pf(t['@_Importe']);
    if (t['@_Impuesto'] === '002') iva += importe;
    else if (t['@_Impuesto'] === '003') ieps += importe;
  }

  return { iva, ieps };
}

/**
 * Extrae impuestos retenidos (ISR 001, IVA 002, IEPS 003)
 */
function extractRetenciones(comprobante: any): { isr: number; iva: number; ieps: number } {
  const retenciones = toArray(comprobante.Impuestos?.Retenciones?.Retencion);
  let isr = 0, iva = 0, ieps = 0;

  for (const r of retenciones) {
    const importe = pf(r['@_Importe']);
    if (r['@_Impuesto'] === '001') isr += importe;
    else if (r['@_Impuesto'] === '002') iva += importe;
    else if (r['@_Impuesto'] === '003') ieps += importe;
  }

  return { isr, iva, ieps };
}

/**
 * Extrae impuestos locales
 */
function extractImpuestosLocales(comprobante: any): { trasladado: number; retenido: number } {
  const complemento = comprobante.Complemento;
  if (!complemento) return { trasladado: 0, retenido: 0 };

  const impLocales = complemento.ImpuestosLocales;
  if (!impLocales) return { trasladado: 0, retenido: 0 };

  return {
    trasladado: pf(impLocales['@_TotaldeTraslados']),
    retenido: pf(impLocales['@_TotaldeRetenciones']),
  };
}

/**
 * Extrae CfdiRelacionados a nivel raíz del Comprobante. Puede haber 1+
 * nodos `cfdi:CfdiRelacionados` (cada uno con un `TipoRelacion`), y dentro
 * de cada uno 1+ `cfdi:CfdiRelacionado` con UUID. Retorna el primer
 * TipoRelacion encontrado (lo más común) y todos los UUIDs pipe-separated.
 */
function extractCfdiRelacionados(comprobante: any): {
  tipoRelacion: string | null;
  uuids: string | null;
} {
  const nodes = toArray(comprobante.CfdiRelacionados);
  if (nodes.length === 0) return { tipoRelacion: null, uuids: null };

  let tipoRelacion: string | null = null;
  const allUuids: string[] = [];

  for (const node of nodes) {
    if (!tipoRelacion && node['@_TipoRelacion']) {
      tipoRelacion = String(node['@_TipoRelacion']);
    }
    const rels = toArray(node.CfdiRelacionado);
    for (const r of rels) {
      if (r['@_UUID']) allUuids.push(String(r['@_UUID']));
    }
  }

  return {
    tipoRelacion,
    uuids: allUuids.length > 0 ? allUuids.join('|') : null,
  };
}

/**
 * Extrae datos del complemento de pagos (pago20)
 */
function extractPagos(comprobante: any): {
  montoPago: number;
  fechaPagoP: string | null;
  numParcialidad: string | null;
  uuidRelacionado: string | null;
  saldoInsoluto: string | null;
  isrRetencion: number;
  ivaTraslado: number;
  ivaRetencion: number;
  iepsTraslado: number;
  iepsRetencion: number;
} {
  const result = {
    montoPago: 0, fechaPagoP: null as string | null,
    numParcialidad: null as string | null,
    uuidRelacionado: null as string | null,
    saldoInsoluto: null as string | null,
    isrRetencion: 0, ivaTraslado: 0, ivaRetencion: 0,
    iepsTraslado: 0, iepsRetencion: 0,
  };

  const complemento = comprobante.Complemento;
  if (!complemento) return result;

  // Try pago20:Pagos or just Pagos
  const pagosNode = complemento.Pagos;
  if (!pagosNode) return result;

  const pagos = toArray(pagosNode.Pago);
  const fechas: string[] = [];
  const parcialidades: string[] = [];
  const uuids: string[] = [];
  const saldos: string[] = [];

  for (const pago of pagos) {
    result.montoPago += pf(pago['@_Monto']);
    if (pago['@_FechaPago']) fechas.push(pago['@_FechaPago']);

    // Impuestos del pago
    const retPago = toArray(pago.ImpuestosP?.RetencionesPP?.RetencionP || pago.ImpuestosP?.RetencionesPP);
    for (const r of retPago) {
      const importe = pf(r['@_ImporteP']);
      if (r['@_ImpuestoP'] === '001') result.isrRetencion += importe;
      else if (r['@_ImpuestoP'] === '002') result.ivaRetencion += importe;
      else if (r['@_ImpuestoP'] === '003') result.iepsRetencion += importe;
    }

    const trasPago = toArray(pago.ImpuestosP?.TrasladosP?.TrasladoP || pago.ImpuestosP?.TrasladosP);
    for (const t of trasPago) {
      const importe = pf(t['@_ImporteP']);
      if (t['@_ImpuestoP'] === '002') result.ivaTraslado += importe;
      else if (t['@_ImpuestoP'] === '003') result.iepsTraslado += importe;
    }

    // Documentos relacionados
    const doctos = toArray(pago.DoctoRelacionado);
    for (const d of doctos) {
      if (d['@_IdDocumento']) uuids.push(d['@_IdDocumento']);
      if (d['@_NumParcialidad']) parcialidades.push(d['@_NumParcialidad']);
      if (d['@_ImpSaldoInsoluto'] !== undefined) saldos.push(d['@_ImpSaldoInsoluto']);
    }
  }

  result.fechaPagoP = fechas.length > 0 ? parseCfdiDate(fechas[0]).toISOString() : null;
  result.numParcialidad = parcialidades.length > 0 ? parcialidades.join('|') : null;
  result.uuidRelacionado = uuids.length > 0 ? uuids.join('|') : null;
  result.saldoInsoluto = saldos.length > 0 ? saldos.join('|') : null;

  return result;
}

/**
 * Extrae datos del complemento de nómina (nomina12)
 */
function extractNomina(comprobante: any): {
  fechaPago: string | null;
  fechaInicialPago: string | null;
  fechaFinalPago: string | null;
  numDiasPagados: number;
  numSeguroSocial: string | null;
  puesto: string | null;
  salarioBaseCotApor: number;
  salarioDiarioIntegrado: number;
  totalPercepciones: number;
  totalDeducciones: number;
  impRetenidosNomina: number;
  otrasDeduccionesNomina: number;
  subsidioCausado: number;
} {
  const result = {
    fechaPago: null as string | null,
    fechaInicialPago: null as string | null,
    fechaFinalPago: null as string | null,
    numDiasPagados: 0,
    numSeguroSocial: null as string | null,
    puesto: null as string | null,
    salarioBaseCotApor: 0,
    salarioDiarioIntegrado: 0,
    totalPercepciones: 0,
    totalDeducciones: 0,
    impRetenidosNomina: 0,
    otrasDeduccionesNomina: 0,
    subsidioCausado: 0,
  };

  const complemento = comprobante.Complemento;
  if (!complemento) return result;

  const nomina = complemento.Nomina;
  if (!nomina) return result;

  result.fechaPago = nomina['@_FechaPago'] ? parseCfdiDate(nomina['@_FechaPago']).toISOString() : null;
  result.fechaInicialPago = nomina['@_FechaInicialPago'] ? parseCfdiDate(nomina['@_FechaInicialPago']).toISOString() : null;
  result.fechaFinalPago = nomina['@_FechaFinalPago'] ? parseCfdiDate(nomina['@_FechaFinalPago']).toISOString() : null;
  result.numDiasPagados = pf(nomina['@_NumDiasPagados']);
  result.totalPercepciones = pf(nomina['@_TotalPercepciones']);
  result.totalDeducciones = pf(nomina['@_TotalDeducciones']);

  // Receptor de nómina
  const receptor = nomina.Receptor;
  if (receptor) {
    result.numSeguroSocial = receptor['@_NumSeguridadSocial'] || null;
    result.puesto = receptor['@_Puesto'] || null;
    result.salarioBaseCotApor = pf(receptor['@_SalarioBaseCotApor']);
    result.salarioDiarioIntegrado = pf(receptor['@_SalarioDiarioIntegrado']);
  }

  // Deducciones
  const deducciones = nomina.Deducciones;
  if (deducciones) {
    result.impRetenidosNomina = pf(deducciones['@_TotalImpuestosRetenidos']);
    result.otrasDeduccionesNomina = pf(deducciones['@_TotalOtrasDeducciones']);
  }

  // Subsidio causado (OtrosPagos/OtroPago[@TipoOtroPago='002'])
  const otrosPagos = toArray(nomina.OtrosPagos?.OtroPago);
  for (const op of otrosPagos) {
    if (op['@_TipoOtroPago'] === '002') {
      result.subsidioCausado = pf(op.SubsidioAlEmpleo?.['@_SubsidioCausado']);
    }
  }

  return result;
}

/**
 * Extrae los conceptos del comprobante
 */
function extractConceptos(comprobante: any): ConceptoParsed[] {
  const conceptosNode = comprobante.Conceptos?.Concepto;
  if (!conceptosNode) return [];

  const conceptos = toArray(conceptosNode);
  return conceptos.map((c: any) => {
    // Impuestos por concepto
    const trasladosC = toArray(c.Impuestos?.Traslados?.Traslado);
    const retencionesC = toArray(c.Impuestos?.Retenciones?.Retencion);

    let ivaTraslado = 0, iepsTraslado = 0;
    for (const t of trasladosC) {
      const importe = pf(t['@_Importe']);
      if (t['@_Impuesto'] === '002') ivaTraslado += importe;
      else if (t['@_Impuesto'] === '003') iepsTraslado += importe;
    }

    let isrRetencion = 0, ivaRetencion = 0, iepsRetencion = 0;
    for (const r of retencionesC) {
      const importe = pf(r['@_Importe']);
      if (r['@_Impuesto'] === '001') isrRetencion += importe;
      else if (r['@_Impuesto'] === '002') ivaRetencion += importe;
      else if (r['@_Impuesto'] === '003') iepsRetencion += importe;
    }

    return {
      claveProdServ: c['@_ClaveProdServ'] || null,
      noIdentificacion: c['@_NoIdentificacion'] || null,
      descripcion: c['@_Descripcion'] || '',
      cantidad: pf(c['@_Cantidad']) || 1,
      claveUnidad: c['@_ClaveUnidad'] || null,
      unidad: c['@_Unidad'] || null,
      valorUnitario: pf(c['@_ValorUnitario']),
      importe: pf(c['@_Importe']),
      descuento: pf(c['@_Descuento']),
      isrRetencion,
      ivaTraslado,
      ivaRetencion,
      iepsTraslado,
      iepsRetencion,
    };
  });
}

/**
 * Parsea un XML de CFDI y extrae los datos relevantes
 * @param downloadType - 'emitidos' o 'recibidos' para determinar el type (EMITIDO/RECIBIDO)
 */
export function parseXml(xmlContent: string, downloadType: 'emitidos' | 'recibidos' = 'emitidos'): CfdiParsed | null {
  try {
    const result = xmlParser.parse(xmlContent);
    const comprobante = result.Comprobante;

    if (!comprobante) {
      console.error('[Parser] No se encontró el nodo Comprobante');
      return null;
    }

    const emisor = comprobante.Emisor || {};
    const receptor = comprobante.Receptor || {};
    const retenciones = extractRetenciones(comprobante);
    const traslados = extractTraslados(comprobante);
    const timbreData = extractTimbreData(comprobante);
    const impLocales = extractImpuestosLocales(comprobante);
    const tipoComprobante = comprobante['@_TipoDeComprobante'] || 'I';

    // Complemento de pagos (solo tipo P)
    const pagosData = tipoComprobante === 'P' ? extractPagos(comprobante) : {
      montoPago: 0, fechaPagoP: null, numParcialidad: null,
      uuidRelacionado: null, saldoInsoluto: null,
      isrRetencion: 0, ivaTraslado: 0, ivaRetencion: 0,
      iepsTraslado: 0, iepsRetencion: 0,
    };

    // CfdiRelacionados a nivel raíz. CFDI 4.0 permite 1+ nodos
    // `cfdi:CfdiRelacionados` cada uno con un TipoRelacion y múltiples UUIDs.
    // Aquí capturamos el PRIMER TipoRelacion (lo más común es que haya uno
    // solo, especialmente en NC tipo E). Los UUIDs de todos los bloques se
    // concatenan con `|`.
    const relacionesData = extractCfdiRelacionados(comprobante);

    // Complemento de nómina (solo tipo N)
    const nominaData = tipoComprobante === 'N' ? extractNomina(comprobante) : {
      fechaPago: null, fechaInicialPago: null, fechaFinalPago: null,
      numDiasPagados: 0, numSeguroSocial: null, puesto: null,
      salarioBaseCotApor: 0, salarioDiarioIntegrado: 0,
      totalPercepciones: 0, totalDeducciones: 0,
      impRetenidosNomina: 0, otrasDeduccionesNomina: 0, subsidioCausado: 0,
    };

    const cfdi: CfdiParsed = {
      uuid: extractUuid(comprobante),
      type: downloadType === 'emitidos' ? 'EMITIDO' : 'RECIBIDO',
      tipoComprobante,
      serie: comprobante['@_Serie'] || null,
      folio: comprobante['@_Folio'] || null,
      status: 'Vigente',
      fechaEmision: parseCfdiDate(comprobante['@_Fecha']),
      fechaCertSat: timbreData.fechaCertSat,
      rfcEmisor: emisor['@_Rfc'] || '',
      nombreEmisor: emisor['@_Nombre'] || '',
      rfcReceptor: receptor['@_Rfc'] || '',
      nombreReceptor: receptor['@_Nombre'] || '',
      subtotal: pf(comprobante['@_SubTotal']),
      descuento: pf(comprobante['@_Descuento']),
      total: pf(comprobante['@_Total']),
      moneda: comprobante['@_Moneda'] || 'MXN',
      tipoCambio: pf(comprobante['@_TipoCambio']) || 1,
      metodoPago: comprobante['@_MetodoPago'] || null,
      formaPago: comprobante['@_FormaPago'] || null,
      usoCfdi: receptor['@_UsoCFDI'] || null,
      pac: timbreData.pac,
      regimenFiscalEmisor: emisor['@_RegimenFiscal'] || null,
      regimenFiscalReceptor: receptor['@_RegimenFiscalReceptor'] || receptor['@_RegimenFiscal'] || null,
      codigoPostalReceptor: receptor['@_DomicilioFiscalReceptor'] || null,
      cfdiTipoRelacion: relacionesData.tipoRelacion,
      cfdisRelacionados: relacionesData.uuids,
      // Impuestos comprobante
      ivaTraslado: traslados.iva,
      isrRetencion: retenciones.isr,
      ivaRetencion: retenciones.iva,
      iepsTraslado: traslados.ieps,
      iepsRetencion: retenciones.ieps,
      // Impuestos locales
      impuestosLocalesTrasladado: impLocales.trasladado,
      impuestosLocalesRetenidos: impLocales.retenido,
      // Complemento de pagos
      montoPago: pagosData.montoPago,
      fechaPagoP: pagosData.fechaPagoP,
      numParcialidad: pagosData.numParcialidad,
      uuidRelacionado: pagosData.uuidRelacionado,
      saldoInsoluto: pagosData.saldoInsoluto,
      isrRetencionPago: pagosData.isrRetencion,
      ivaTrasladoPago: pagosData.ivaTraslado,
      ivaRetencionPago: pagosData.ivaRetencion,
      iepsTrasladoPago: pagosData.iepsTraslado,
      iepsRetencionPago: pagosData.iepsRetencion,
      // Nómina
      ...nominaData,
      conceptos: extractConceptos(comprobante),
      xmlOriginal: xmlContent,
      periodicidad: comprobante.InformacionGlobal?.['@_Periodicidad'] || null,
      mesesGlobal: comprobante.InformacionGlobal?.['@_Meses'] || null,
      añoGlobal: comprobante.InformacionGlobal?.['@_Año'] || null,
    };

    if (!cfdi.uuid) {
      console.error('[Parser] CFDI sin UUID');
      return null;
    }

    return cfdi;
  } catch (error) {
    console.error('[Parser Error]', error);
    return null;
  }
}

/**
 * Procesa un paquete ZIP completo y retorna los CFDIs parseados
 * @param downloadType - 'emitidos' o 'recibidos'
 */
export function processPackage(zipBase64: string, downloadType: 'emitidos' | 'recibidos' = 'emitidos'): CfdiParsed[] {
  const xmlFiles = extractXmlsFromZip(zipBase64);
  const cfdis: CfdiParsed[] = [];

  for (const { content } of xmlFiles) {
    const cfdi = parseXml(content, downloadType);
    if (cfdi) {
      cfdis.push(cfdi);
    }
  }

  return cfdis;
}

/**
 * Datos parseados de un registro de metadata del SAT
 */
interface CfdiMetadata {
  uuid: string;
  rfcEmisor: string;
  nombreEmisor: string;
  rfcReceptor: string;
  nombreReceptor: string;
  rfcPac: string | null;
  fechaEmision: Date;
  fechaCertSat: Date | null;
  fechaCancelacion: Date | null;
  monto: number;
  tipoComprobante: string;
  status: string;  // 'Vigente' | 'Cancelado'
  type: 'EMITIDO' | 'RECIBIDO';
}

/**
 * Extrae archivos CSV de un paquete ZIP de metadata en base64.
 * Usa AdmZip directamente para evitar problemas de archivos temporales en Windows.
 */
function extractCsvsFromZip(zipBase64: string): string[] {
  const zipBuffer = Buffer.from(zipBase64, 'base64');
  const zip = new AdmZip(zipBuffer);
  const entries = zip.getEntries();
  const csvContents: string[] = [];

  for (const entry of entries) {
    const name = entry.entryName.toLowerCase();
    if (name.endsWith('.csv') || name.endsWith('.txt')) {
      csvContents.push(entry.getData().toString('utf-8'));
    }
  }

  return csvContents;
}

/**
 * Parsea una línea CSV respetando campos entrecomillados
 */
function parseCsvLine(line: string): string[] {
  const fields: string[] = [];
  let current = '';
  let inQuotes = false;

  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (ch === '"') {
      inQuotes = !inQuotes;
    } else if (ch === '~' && !inQuotes) {
      fields.push(current.trim());
      current = '';
    } else {
      current += ch;
    }
  }
  fields.push(current.trim());
  return fields;
}

/**
 * Procesa un paquete de metadata del SAT (ZIP con CSV) y retorna los registros.
 * Usa AdmZip directo en vez de MetadataPackageReader para compatibilidad Windows.
 */
export function processMetadataPackage(
  zipBase64: string,
  downloadType: 'emitidos' | 'recibidos' = 'emitidos'
): CfdiMetadata[] {
  const csvContents = extractCsvsFromZip(zipBase64);
  const results: CfdiMetadata[] = [];

  const tipoMap: Record<string, string> = {
    'Ingreso': 'I', 'Egreso': 'E', 'Traslado': 'T', 'Nómina': 'N', 'Nomina': 'N', 'Pago': 'P',
  };

  for (const csv of csvContents) {
    const lines = csv.split(/\r?\n/).filter(l => l.trim());
    if (lines.length < 2) continue;

    // Header line — SAT uses ~ as delimiter
    const headers = parseCsvLine(lines[0]);

    // Find column indices (case-insensitive)
    const idx = (name: string) => headers.findIndex(h => h.toLowerCase() === name.toLowerCase());
    const iUuid = idx('Uuid');
    const iRfcEmisor = idx('RfcEmisor');
    const iNombreEmisor = idx('NombreEmisor');
    const iRfcReceptor = idx('RfcReceptor');
    const iNombreReceptor = idx('NombreReceptor');
    const iRfcPac = idx('RfcPac');
    const iFechaEmision = idx('FechaEmision');
    const iFechaCert = idx('FechaCertificacionSat');
    const iFechaCancel = idx('FechaCancelacion');
    const iMonto = idx('Monto');
    const iEfecto = idx('EfectoComprobante');
    const iEstatus = idx('Estatus');
    // Fallback column names
    const iEstado = iEstatus >= 0 ? iEstatus : idx('Estado');

    if (iUuid < 0) continue; // No UUID column = invalid CSV

    for (let i = 1; i < lines.length; i++) {
      const fields = parseCsvLine(lines[i]);
      const uuid = (fields[iUuid] || '').trim();
      if (!uuid) continue;

      const estatus = (fields[iEstado] || 'Vigente').trim();
      const fechaCancelStr = iFechaCancel >= 0 ? (fields[iFechaCancel] || '').trim() : '';
      const fechaEmisionStr = iFechaEmision >= 0 ? (fields[iFechaEmision] || '').trim() : '';
      const fechaCertStr = iFechaCert >= 0 ? (fields[iFechaCert] || '').trim() : '';
      const efecto = iEfecto >= 0 ? (fields[iEfecto] || 'Ingreso').trim() : 'Ingreso';

      results.push({
        uuid: uuid.toUpperCase(),
        rfcEmisor: iRfcEmisor >= 0 ? (fields[iRfcEmisor] || '').trim() : '',
        nombreEmisor: iNombreEmisor >= 0 ? (fields[iNombreEmisor] || '').trim() : '',
        rfcReceptor: iRfcReceptor >= 0 ? (fields[iRfcReceptor] || '').trim() : '',
        nombreReceptor: iNombreReceptor >= 0 ? (fields[iNombreReceptor] || '').trim() : '',
        rfcPac: iRfcPac >= 0 ? (fields[iRfcPac] || '').trim() || null : null,
        fechaEmision: fechaEmisionStr ? parseCfdiDate(fechaEmisionStr) : new Date(),
        fechaCertSat: fechaCertStr ? parseCfdiDate(fechaCertStr) : null,
        fechaCancelacion: fechaCancelStr ? parseCfdiDate(fechaCancelStr) : null,
        monto: parseFloat(iMonto >= 0 ? fields[iMonto] || '0' : '0') || 0,
        tipoComprobante: tipoMap[efecto] || efecto.charAt(0) || 'I',
        status: estatus === '0' || estatus.toLowerCase().includes('cancel') ? 'Cancelado' : 'Vigente',
        type: downloadType === 'emitidos' ? 'EMITIDO' : 'RECIBIDO',
      });
    }
  }

  return results;
}

/**
 * Valida que un XML sea un CFDI válido
 */
export function isValidCfdi(xmlContent: string): boolean {
  try {
    const result = xmlParser.parse(xmlContent);
    const comprobante = result.Comprobante;

    if (!comprobante) return false;
    if (!comprobante.Complemento?.TimbreFiscalDigital) return false;
    if (!extractUuid(comprobante)) return false;

    return true;
  } catch {
    return false;
  }
}

export type { CfdiParsed, CfdiMetadata, ConceptoParsed, ExtractedXml };
