/**
 * Normalizadores puros para el Libro de Compras del SET (Paraguay).
 * Sin dependencias de Nest/Prisma — 100% testeables con Jest puro.
 */

export type NumeroComprobanteResult =
  | { ok: true; establecimiento: string; punto_expedicion: string; numero_factura: string }
  | { ok: false; error: string };

/** Tipo real de una celda tal como la devuelve XLSX.utils.sheet_to_json */
export type CeldaExcel = string | number | boolean | Date | null | undefined;

const KEYWORDS_NO_GASTO: Array<{ match: (texto: string) => boolean; motivo: string }> = [
  { match: (t) => t.includes('IMPORTACION EN CURSO'), motivo: 'IMPORTACION_EN_CURSO' },
  { match: (t) => t.includes('MERCADERIA'), motivo: 'MERCADERIA_NO_GASTO' },
  { match: (t) => t.includes('REMUNERACION') && t.includes('PERSONAL'), motivo: 'REMUNERACION_PERSONAL' },
];

/** upper + trim + sin tildes + colapsa espacios + limpia caracteres sueltos (ej. "}") */
export function normalizarTextoAlias(valor: CeldaExcel): string {
  const str = String(valor ?? '');
  return str
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^\w\s]/g, ' ')
    .trim()
    .toUpperCase()
    .replace(/\s+/g, ' ');
}

/** dd/mm/yyyy (string) o Date real (si XLSX ya parseó con cellDates:true) → Date | null */
export function parseFechaSET(valor: CeldaExcel): Date | null {
  if (valor instanceof Date) {
    return Number.isNaN(valor.getTime()) ? null : valor;
  }
  const str = String(valor ?? '').trim();
  const match = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
  if (!match) return null;
  const dia = Number(match[1]);
  const mes = Number(match[2]);
  const anio = Number(match[3]);
  if (mes < 1 || mes > 12 || dia < 1 || dia > 31) return null;
  const fecha = new Date(Date.UTC(anio, mes - 1, dia));
  if (fecha.getUTCMonth() !== mes - 1 || fecha.getUTCDate() !== dia) return null;
  return fecha;
}

/** "001-001-0006806" → { establecimiento, punto_expedicion, numero_factura } (con padStart) */
export function splitNumeroComprobante(valor: CeldaExcel): NumeroComprobanteResult {
  const str = String(valor ?? '').trim();
  const match = str.match(/^(\d{1,3})-(\d{1,3})-(\d{1,7})$/);
  if (match) {
    return {
      ok: true,
      establecimiento: match[1].padStart(3, '0'),
      punto_expedicion: match[2].padStart(3, '0'),
      numero_factura: match[3].padStart(7, '0'),
    };
  }
  const soloDigitos = str.replace(/\D/g, '');
  if (soloDigitos.length === 13) {
    return {
      ok: true,
      establecimiento: soloDigitos.slice(0, 3),
      punto_expedicion: soloDigitos.slice(3, 6),
      numero_factura: soloDigitos.slice(6, 13),
    };
  }
  return { ok: false, error: `Número de comprobante con formato inválido: "${str}"` };
}

/** "CONTADO" / "CREDITO" / "CRÉDITO" → 'CONTADO' | 'CREDITO' */
export function normalizarCondicionOperacion(valor: CeldaExcel): 'CONTADO' | 'CREDITO' {
  return normalizarTextoAlias(valor).includes('CREDITO') ? 'CREDITO' : 'CONTADO';
}

/** RUC crudo del Libro de Compras → hasta 8 caracteres (personas.ruc es VarChar(8)) */
export function normalizarRuc(valor: CeldaExcel): { ruc: string; truncado: boolean } {
  const limpio = String(valor ?? '').trim().replace(/[^\dA-Za-z]/g, '');
  return { ruc: limpio.slice(0, 8), truncado: limpio.length > 8 };
}

/** Detecta filas que no son gastos (van a otro módulo o se excluyen por decisión de negocio) */
export function detectarFilaNoGasto(tipoGastoTexto: CeldaExcel): { esNoGasto: boolean; motivo?: string } {
  const normalizado = normalizarTextoAlias(tipoGastoTexto);
  const encontrado = KEYWORDS_NO_GASTO.find((k) => k.match(normalizado));
  return encontrado ? { esNoGasto: true, motivo: encontrado.motivo } : { esNoGasto: false };
}

/**
 * "ANTICIPO DE VIATICO" en Contrapartida Pasiva — decisión de negocio (2026-07-23):
 * se importa como gasto normal (CONTADO/CREDITO según Condición) en vez de forma_pago=ANTICIPO,
 * porque no existe una rendición de viáticos vigente para el período histórico importado.
 * Se marca con una observación para trazabilidad contable futura.
 */
export function esAnticipoViaticoHistorico(contrapartidaPasiva: CeldaExcel): boolean {
  const normalizado = normalizarTextoAlias(contrapartidaPasiva);
  return normalizado.includes('ANTICIPO') && normalizado.includes('VIATIC');
}

export function buildObservacionAnticipoHistorico(): string {
  return (
    'Origen: Anticipo de viático (histórico — período anterior a la implementación en el sistema, ' +
    'sin rendición de viáticos vinculada). Importado desde Libro de Compras SET.'
  );
}

/** Claves de campo → alias de headers posibles del Libro de Compras del SET. */
export const CAMPOS_LIBRO_COMPRAS: Record<string, string[]> = {
  ruc: ['RUC N DE IDENTIFICACION DEL INFORMADO', 'RUC', 'NUMERO DE IDENTIFICACION'],
  razon_social: ['NOMBRE O RAZON SOCIAL DEL INFORMADO', 'NOMBRE O RAZON SOCIAL', 'RAZON SOCIAL'],
  fecha_emision: ['FECHA DE EMISION'],
  condicion_operacion: ['CONDICION DE LA OPERACION'],
  timbrado: ['TIMBRADO DEL COMPROBANTE', 'TIMBRADO'],
  numero_comprobante: ['NUMERO DE COMPROBANTE'],
  monto_gravado_10: ['MONTO GRAVADO 10'],
  iva_10: ['IVA 10'],
  monto_gravado_5: ['MONTO GRAVADO 5'],
  iva_5: ['IVA 5'],
  monto_no_gravado: ['MONTO NO GRAVADO EXENTO', 'MONTO NO GRAVADO', 'EXENTO'],
  total_comprobante: ['TOTAL COMPROBANTE'],
  tipo_gasto_texto: ['TIPO CUENTA CONTABLE GASTO', 'TIPO CUENTA CONTABLE- GASTO'],
  contrapartida_pasiva: ['TIPO CUENTA CONTABLE CONTRAPARTIDA PASIVA', 'CONTRAPARTIDA PASIVA'],
};

function normalizarHeader(valor: string): string {
  return valor
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toUpperCase()
    .replace(/[^A-Z0-9]/g, ' ')
    .trim()
    .replace(/\s+/g, ' ');
}

/** Resuelve, para cada campo destino, qué header real del Excel le corresponde (o null si no se encontró). */
export function resolverMapeoColumnas(headers: string[]): Record<string, string | null> {
  const headersNormalizados = headers.map((h) => ({ original: h, normalizado: normalizarHeader(h) }));
  const resultado: Record<string, string | null> = {};
  for (const [campo, alias] of Object.entries(CAMPOS_LIBRO_COMPRAS)) {
    const match = alias
      .map((a) => normalizarHeader(a))
      .reduce<string | null>((acc, aliasNorm) => {
        if (acc) return acc;
        const encontrado = headersNormalizados.find(
          (h) => h.normalizado === aliasNorm || h.normalizado.includes(aliasNorm),
        );
        return encontrado ? encontrado.original : null;
      }, null);
    resultado[campo] = match;
  }
  return resultado;
}

export interface FilaEstructurada {
  ruc: string;
  razonSocial: string;
  fechaEmision: Date | null;
  condicionOperacion: 'CONTADO' | 'CREDITO';
  timbrado: string;
  comprobante: NumeroComprobanteResult;
  montoGravado10: number;
  iva10: number;
  montoGravado5: number;
  iva5: number;
  montoNoGravado: number;
  totalComprobante: number;
  tipoGastoTexto: string;
  contrapartidaPasiva: string;
  noGasto: { esNoGasto: boolean; motivo?: string };
  esAnticipoViatico: boolean;
  errores: string[];
}

/** Toma una fila cruda del Excel + el mapeo de columnas resuelto y arma la fila estructurada. */
export function construirFilaEstructurada(
  row: Record<string, unknown>,
  mapeo: Record<string, string | null>,
): FilaEstructurada {
  const get = (campo: string): CeldaExcel => {
    const header = mapeo[campo];
    return header ? (row[header] as CeldaExcel) : undefined;
  };
  const errores: string[] = [];

  const { ruc } = normalizarRuc(get('ruc'));
  const razonSocial = String(get('razon_social') ?? '').trim();
  const fechaEmision = parseFechaSET(get('fecha_emision'));
  if (!fechaEmision) errores.push('Fecha de emisión inválida o faltante');

  const condicionOperacion = normalizarCondicionOperacion(get('condicion_operacion'));
  const timbrado = String(get('timbrado') ?? '').trim();
  const comprobante = splitNumeroComprobante(get('numero_comprobante'));
  if (comprobante.ok === false) errores.push(comprobante.error);

  const num = (campo: string): number => {
    const raw = get(campo);
    const n = Number(String(raw ?? '0').replace(/,/g, '.'));
    return Number.isFinite(n) ? n : 0;
  };

  const tipoGastoTexto = String(get('tipo_gasto_texto') ?? '').trim();
  const contrapartidaPasiva = String(get('contrapartida_pasiva') ?? '').trim();

  if (!razonSocial) errores.push('Falta razón social');
  if (!tipoGastoTexto) errores.push('Falta tipo de gasto (cuenta contable)');

  return {
    ruc,
    razonSocial,
    fechaEmision,
    condicionOperacion,
    timbrado,
    comprobante,
    montoGravado10: num('monto_gravado_10'),
    iva10: num('iva_10'),
    montoGravado5: num('monto_gravado_5'),
    iva5: num('iva_5'),
    montoNoGravado: num('monto_no_gravado'),
    totalComprobante: num('total_comprobante'),
    tipoGastoTexto,
    contrapartidaPasiva,
    noGasto: detectarFilaNoGasto(tipoGastoTexto),
    esAnticipoViatico: esAnticipoViaticoHistorico(contrapartidaPasiva),
    errores,
  };
}
