// LA FACTURA QUE VE EL CLIENTE.
//
// Dos formas del mismo documento:
//   - EN PAPEL: para la termica comun de 80mm que el negocio ya tiene (no hace
//     falta maquina fiscal: la representacion grafica se imprime en cualquier
//     tiquetera).
//   - EN EL TELEFONO: los mismos datos, para mandarlos por enlace a quien pidio
//     por el QR de la mesa. Si el cliente ya estaba en su telefono, lo natural
//     es que la factura le llegue ahi y no en un papel que va a botar.
//
// Las dos llevan el SELLO DE COMPROBACION: el codigo QR que abre la pagina
// donde cualquiera comprueba que el documento es real.
//
// ⚠ TODO EL TEXTO VA EN ASCII, sin acentos ni simbolos raros. Las POS-80
// genericas chinas arrancan en modo chino y se comen la letra siguiente a
// cualquier byte alto (lecccion cara del Cerebrito: "Guayanés" salia
// "Guayan俿"). Aca no puede pasar: ni un byte por encima de 127.
import type { Asiento } from './tipos.js';

const ANCHO = 48;

// Mismo mapa que usa el Cerebrito para dejar todo en ASCII.
const SIN_ACENTO: Record<string, string> = {
  á: 'a', é: 'e', í: 'i', ó: 'o', ú: 'u', ü: 'u',
  Á: 'A', É: 'E', Í: 'I', Ó: 'O', Ú: 'U', Ü: 'U',
  ñ: 'n', Ñ: 'N', '·': '-', '°': 'o', '¿': '?', '¡': '!',
  '“': '"', '”': '"', '‘': "'", '’': "'", '–': '-', '—': '-', '€': 'EUR',
};

export function aAscii(texto: string): string {
  return [...(texto ?? '')]
    .map((c) => SIN_ACENTO[c] ?? (c.charCodeAt(0) > 126 ? '?' : c))
    .join('');
}

/**
 * Deja la linea CORTA para que la centre la impresora.
 *
 * Ojo con esto, que salio mal impreso una vez: el documento se manda con el
 * centrado de la impresora activado, asi que si ademas se le metian espacios a
 * la izquierda el texto se centraba DOS veces y salia corrido hacia la derecha,
 * como escalonado. Centrar es no hacer nada: solo recortar al ancho.
 */
const centrar = (t: string) => aAscii(t).slice(0, ANCHO);

const dosColumnas = (izq: string, der: string) => {
  const a = aAscii(izq);
  const b = aAscii(der);
  const espacio = Math.max(1, ANCHO - a.length - b.length);
  return a + ' '.repeat(espacio) + b;
};

const bs = (n: number) =>
  Number(n).toLocaleString('es-VE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

/** Datos que necesita la factura y que no vienen del libro. */
export interface DatosSello {
  /** Numero de control que devolvio la imprenta. */
  numeroControl: string;
  /** Direccion completa del sello de comprobacion. */
  urlSello: string;
  /**
   * El serial de la estacion (lo asigna la imprenta). Va impreso en el pie,
   * como el registro de una maquina fiscal: cualquiera lo verifica en la
   * pagina publica del sello.
   */
  serial?: string | null;
  /**
   * Cuando la imprenta asigno el numero de control (art. 7 numeral 15 de la
   * 000102: la factura debe decir la fecha de asignacion, no solo el numero).
   */
  fechaAsignacion?: string | null;
  /**
   * Rango de numeros de control asignado al emisor (art. 7 numeral 5:
   * "desde el N... hasta el N..."). Vacio en numeracion libre.
   */
  rango?: { desde: string; hasta: string } | null;
  /**
   * Razon social, RIF y providencia de la imprenta digital autorizada
   * (art. 7 numeral 14). Vacio solo mientras no haya imprenta configurada.
   */
  imprentaFiscal?: {
    rif: string;
    razon_social: string;
    providencia?: string | null;
    providencia_fecha?: string | null;
  } | null;
}

/**
 * La zona LIBRE del documento: lo que el negocio decide de su factura. Nada de
 * aqui toca ni desplaza los datos del art. 7 de la 000102, que van siempre y
 * los arma el codigo sin preguntar.
 */
export interface ModeloFactura {
  /** Mensaje del pie ("Gracias por su compra"). */
  mensaje_pie?: string | null;
  /** Logo para la version digital. La termica no lo imprime todavia. */
  logo_url?: string | null;
  /** Color de acento de la version digital (#RRGGBB). */
  color?: string | null;
}

/** Parte un mensaje en lineas de hasta ANCHO, respetando palabras. */
function partirMensaje(texto: string, maxLineas: number): string[] {
  const palabras = texto.trim().split(/\s+/);
  const lineas: string[] = [];
  let actual = '';
  for (const p of palabras) {
    const candidata = actual ? `${actual} ${p}` : p;
    if (candidata.length <= ANCHO) {
      actual = candidata;
    } else {
      if (actual) lineas.push(actual);
      actual = p.slice(0, ANCHO);
    }
    if (lineas.length >= maxLineas) break;
  }
  if (actual && lineas.length < maxLineas) lineas.push(actual);
  return lineas;
}

/**
 * Las lineas de texto del documento, sin los comandos de impresora.
 * TODAS salen ya en ASCII: la conversion se hace aca, en un solo lugar, y no
 * linea por linea. Antes se colaba el nombre del cliente sin convertir y, como
 * al imprimir se recortaban los bytes altos, "Maria" habria salido "Mara" en el
 * papel sin que nadie se enterara.
 */
export function lineasFactura(asiento: Asiento, sello: DatosSello, modelo?: ModeloFactura | null): string[] {
  return construirLineas(asiento, sello, modelo).map(aAscii);
}

/**
 * Pega la linea al margen IZQUIERDO.
 *
 * La impresora imprime todo el documento centrado, asi que una linea corta se
 * va sola al medio del papel y los datos que exige la ley quedaban bailando.
 * Rellenandola hasta el ancho completo, el centrado no la mueve y queda donde
 * tiene que quedar. Lo centrado de verdad (el nombre del negocio, la imprenta,
 * la estacion) se deja corto a proposito.
 */
function izq(texto: string): string {
  return texto.length >= ANCHO ? texto.slice(0, ANCHO) : texto.padEnd(ANCHO);
}

const ddmmaaaa = (f: Date) =>
  `${String(f.getDate()).padStart(2, '0')}/${String(f.getMonth() + 1).padStart(2, '0')}/${f.getFullYear()}`;

function construirLineas(asiento: Asiento, sello: DatosSello, modelo?: ModeloFactura | null): string[] {
  const d = asiento.documento;
  const f = new Date(asiento.sellado_en);
  const fecha = ddmmaaaa(f);
  // Con segundos y en 24 horas: el art. 7 numeral 6 de la 000102 exige la hora
  // de emision completa (HH.MM.SS), no una hora recortada a minutos.
  const hora = `${String(f.getHours()).padStart(2, '0')}:${String(f.getMinutes()).padStart(2, '0')}:${String(f.getSeconds()).padStart(2, '0')}`;
  const tasa = d.tasa_bs ?? 0;
  const enBs = (usd: number) => bs(usd * tasa);

  const TITULO: Record<string, string> = {
    factura: 'FACTURA',
    nota_credito: 'NOTA DE CREDITO',
    nota_debito: 'NOTA DE DEBITO',
  };

  const l: string[] = [];
  // Cabecera CENTRADA: quien vende, y debajo de su direccion, la fecha y hora.
  l.push(centrar(d.emisor.razon_social));
  l.push(centrar(`RIF ${d.emisor.rif}`));
  l.push(centrar(d.emisor.domicilio_fiscal));
  l.push(centrar(`${fecha}  ${hora}`));
  l.push('-'.repeat(ANCHO));
  // De aqui para abajo, TODO lo que exige la ley va pegado al margen izquierdo:
  // centrado se leia como un volante, no como una factura.
  l.push(izq(`${TITULO[asiento.tipo] ?? 'DOCUMENTO'}  ${asiento.serie}-${asiento.numero}`));
  // "N. de Control": la frase que pide el art. 30 de la 000102, adaptada a
  // ASCII puro (la tiquetera se come el signo de grados).
  l.push(izq(`N. de Control ${sello.numeroControl}`));
  if (sello.fechaAsignacion) {
    // Art. 7 numeral 15: cuando la imprenta asigno ese numero.
    l.push(izq(`Control asignado el ${ddmmaaaa(new Date(sello.fechaAsignacion))}`));
  }
  if (sello.rango) {
    // Art. 7 numeral 5: el rango autorizado del que salio el numero.
    l.push(izq(`Controles asignados: del N. ${sello.rango.desde} al N. ${sello.rango.hasta}`));
  }
  if (asiento.ref) l.push(izq(`Corrige: ${asiento.ref.serie}-${asiento.ref.numero}`));
  l.push(izq(`Cliente: ${aAscii(d.cliente.nombre)}`));
  const idCliente = d.cliente.rif ?? d.cliente.cedula;
  if (idCliente) l.push(izq(`C.I./RIF: ${idCliente}`));
  l.push('-'.repeat(ANCHO));

  // Cada renglon: lo que se llevo arriba, y debajo el precio unitario con el
  // monto pegado al margen derecho, que es donde el ojo busca las cifras.
  for (const r of d.renglones) {
    l.push(izq(aAscii(`${r.cantidad}  ${r.nombre}`)));
    const alic = r.alicuota === 0 ? 'exento' : `${r.alicuota}%`;
    l.push(dosColumnas(`   a ${bs(r.precio_usd * tasa)} c/u  (${alic})`, enBs(r.total_usd)));
  }

  l.push('-'.repeat(ANCHO));
  for (const a of d.por_alicuota) {
    if (a.alicuota === 0) {
      l.push(dosColumnas('Exento', enBs(a.base_usd)));
    } else {
      l.push(dosColumnas(`Base imponible ${a.alicuota}%`, enBs(a.base_usd)));
      l.push(dosColumnas(`IVA ${a.alicuota}%`, enBs(a.iva_usd)));
    }
  }
  // El TOTAL encajonado entre lineas: es la cifra que la gente busca, tiene que
  // saltar sin que uno la persiga por el papel.
  l.push('-'.repeat(ANCHO));
  l.push(dosColumnas('TOTAL Bs', enBs(d.total_usd)));
  // IGTF: solo aparece si el documento lo trae (emisor designado + pago en
  // divisas). Va aparte del total de la venta, como lo percibe la norma.
  if (d.igtf_usd) {
    l.push(dosColumnas('IGTF 3%', enBs(d.igtf_usd)));
    l.push(dosColumnas('TOTAL A PAGAR Bs', enBs(d.total_usd + d.igtf_usd)));
  }
  l.push('-'.repeat(ANCHO));
  // El cierre en UNA linea: como pago a la izquierda, cuanto es en dolares a la
  // derecha. Antes el metodo de pago se llevaba una linea entera para el.
  const comoPago = d.pago?.metodo ? nombrePago(d.pago.metodo) : '';
  if (tasa > 0 || comoPago) {
    // Con coma decimal, como se escriben los numeros aqui.
    l.push(dosColumnas(comoPago, tasa > 0 ? `USD ${d.total_usd.toFixed(2).replace('.', ',')}` : ''));
  }
  if (tasa > 0) l.push(dosColumnas(`Tasa del dia ${bs(tasa)}`, d.pago?.referencia ? `Ref ${aAscii(d.pago.referencia)}` : ''));
  else if (d.pago?.referencia) l.push(dosColumnas('', `Ref ${aAscii(d.pago.referencia)}`));
  l.push('-'.repeat(ANCHO));
  // La voz del negocio, en su zona libre: se SUMA al documento, jamas
  // reemplaza ni desplaza los datos del art. 7. Cuatro lineas maximo para que
  // un mensaje largo no se coma el papel.
  if (modelo?.mensaje_pie?.trim()) {
    for (const linea of partirMensaje(modelo.mensaje_pie, 4)) l.push(centrar(linea));
    l.push('-'.repeat(ANCHO));
  }
  // Art. 7 numeral 14: la imprenta digital autorizada que numero el documento,
  // con la providencia que la autoriza. Sin esto el papel no dice quien
  // respondio por el numero de control.
  if (sello.imprentaFiscal) {
    const imp = sello.imprentaFiscal;
    // La razon social va en su propia linea: junta con la etiqueta no cabe en
    // los 48 caracteres del papel y el nombre saldria mochado.
    l.push(centrar('Imprenta digital autorizada:'));
    l.push(centrar(imp.razon_social));
    l.push(centrar(`RIF: ${imp.rif}`));
    if (imp.providencia) {
      const fp = imp.providencia_fecha ? ` del ${ddmmaaaa(new Date(imp.providencia_fecha + 'T12:00:00'))}` : '';
      l.push(centrar(`Providencia ${imp.providencia}${fp}`));
    }
    l.push('-'.repeat(ANCHO));
  }
  // El serial de la estacion, como el registro impreso de una maquina fiscal.
  // Al margen, igual que el resto de los datos.
  if (sello.serial) l.push(izq(`Estacion ${sello.serial}`));
  // Sin la direccion escrita: el QR va justo debajo y con eso basta. La
  // direccion en texto ocupaba dos lineas de codigo ilegible que nadie iba a
  // teclear.
  l.push(izq('Comprueba tu factura:'));
  return l;
}

/** El metodo de pago como lo dice la gente, no como lo guarda la base. */
function nombrePago(metodo: string): string {
  const m: Record<string, string> = {
    efectivo: 'Efectivo',
    pago_movil: 'Pago Movil',
    transferencia: 'Transferencia',
    zelle: 'Zelle',
    punto_venta: 'Punto de venta',
    perdida: 'Perdida',
  };
  return m[metodo] ?? aAscii(metodo).replace(/_/g, ' ');
}

/** Parte la direccion para que quepa en el papel sin perder ni un caracter. */
function partirUrl(url: string): string[] {
  if (url.length <= ANCHO) return [url];
  // Se corta por la ultima raya antes del limite: asi el dominio queda entero en
  // la primera linea y el codigo en la segunda, facil de copiar.
  const corte = url.lastIndexOf('/', ANCHO - 1);
  const i = corte > 10 ? corte + 1 : ANCHO;
  const resto = url.slice(i);
  return [url.slice(0, i), ...(resto.length > ANCHO ? partirUrl(resto) : [resto])];
}

// ===== Comandos de impresora (ESC/POS) =====
const ESC = 0x1b;
const GS = 0x1d;

/** Codigo QR nativo de la impresora: sale nitido y no depende de imagenes. */
function bytesQr(texto: string, tamano = 6): number[] {
  const datos = [...texto].map((c) => c.charCodeAt(0));
  const largo = datos.length + 3;
  return [
    // Modelo 2
    GS, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00,
    // Tamaño del punto
    GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, tamano,
    // Correccion de errores nivel M (aguanta que el papel se manche)
    GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, 0x31,
    // Guardar el contenido
    GS, 0x28, 0x6b, largo % 256, Math.floor(largo / 256), 0x31, 0x50, 0x30, ...datos,
    // Imprimirlo
    GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30,
  ];
}

/**
 * El documento listo para mandar a la termica. Devuelve bytes crudos: el
 * Cerebrito ya sabe empujarlos por USB o por red.
 */
export function facturaTermica(asiento: Asiento, sello: DatosSello, modelo?: ModeloFactura | null): Uint8Array {
  const b: number[] = [];
  b.push(ESC, 0x40); // reiniciar
  b.push(0x1c, 0x2e); // cancelar modo chino (lo que salvo los acentos en el Cerebrito)
  // Fuente A EXPLICITA (12x24 puntos de impresion). A 203 dpi son 3,0 mm de
  // alto por caracter, unos 8,5 puntos tipograficos: por encima del minimo del
  // art. 31 de la 000102 (8 puntos para emisor y numero de control, 6 para la
  // imprenta). Se manda el comando en vez de confiar en el default porque hay
  // tiqueteras que arrancan configuradas en fuente B (9x17, ~6 puntos), y ahi
  // el emisor y el control quedarian por debajo del minimo.
  b.push(ESC, 0x4d, 0x00);
  b.push(ESC, 0x61, 0x01); // centrado

  for (const linea of lineasFactura(asiento, sello, modelo)) {
    for (const c of linea) {
      const cod = c.charCodeAt(0);
      // Las lineas YA vienen en ASCII. Si algo se colara, se marca con '?' en
      // vez de recortarlo: recortar bytes altos cambia la letra en silencio y
      // el papel sale con palabras raras sin que nadie lo note.
      b.push(cod > 126 ? 0x3f : cod);
    }
    b.push(0x0a);
  }

  // El QR pegado a "Comprueba tu factura:", al MARGEN IZQUIERDO como el resto
  // del pie, con un renglon de aire para que no quede montado en el texto.
  b.push(ESC, 0x61, 0x00); // alinear a la izquierda
  b.push(0x0a);
  b.push(...bytesQr(sello.urlSello, 3));
  b.push(0x0a);
  b.push(ESC, 0x61, 0x01); // volver al centrado para la leyenda del pie
  // La leyenda va SIEMPRE: este papel es la representacion grafica, el
  // documento con valor es el digital que quedo sellado y numerado.
  for (const c of centrar('Representacion grafica del documento digital')) b.push(c.charCodeAt(0) & 0x7f);
  b.push(0x0a);
  b.push(0x0a, 0x0a);
  b.push(GS, 0x56, 0x00); // cortar

  return Uint8Array.from(b);
}

/** Los mismos datos, para mandarlos al telefono de quien pidio por el QR. */
export function facturaParaTelefono(
  asiento: Asiento,
  sello: DatosSello,
  modelo?: ModeloFactura | null,
): {
  emisor: { razon_social: string; rif: string; domicilio: string };
  documento: {
    tipo: string;
    numero: string;
    numero_control: string;
    fecha: string;
    fecha_asignacion: string | null;
    rango: { desde: string; hasta: string } | null;
  };
  imprenta: { razon_social: string; rif: string; providencia: string | null; providencia_fecha: string | null } | null;
  cliente: { nombre: string; identificacion: string | null };
  renglones: { nombre: string; cantidad: number; alicuota: number; total_bs: number }[];
  totales: { base_bs: number; iva_bs: number; total_bs: number; total_usd: number; tasa_bs: number | null; igtf_bs?: number };
  marca: { mensaje_pie: string | null; logo_url: string | null; color: string | null } | null;
  sello: string;
} {
  const d = asiento.documento;
  const tasa = d.tasa_bs ?? 0;
  const dos = (n: number) => Number((n * tasa).toFixed(2));
  return {
    emisor: { razon_social: d.emisor.razon_social, rif: d.emisor.rif, domicilio: d.emisor.domicilio_fiscal },
    documento: {
      tipo: asiento.tipo,
      numero: `${asiento.serie}-${asiento.numero}`,
      numero_control: sello.numeroControl,
      fecha: asiento.sellado_en,
      // Art. 7 numerales 15 y 5: mismos datos que lleva el papel.
      fecha_asignacion: sello.fechaAsignacion ?? null,
      rango: sello.rango ?? null,
    },
    // Art. 7 numeral 14: quien numero este documento.
    imprenta: sello.imprentaFiscal
      ? {
          razon_social: sello.imprentaFiscal.razon_social,
          rif: sello.imprentaFiscal.rif,
          providencia: sello.imprentaFiscal.providencia ?? null,
          providencia_fecha: sello.imprentaFiscal.providencia_fecha ?? null,
        }
      : null,
    cliente: { nombre: d.cliente.nombre, identificacion: d.cliente.rif ?? d.cliente.cedula ?? null },
    renglones: d.renglones.map((r) => ({
      nombre: r.nombre,
      cantidad: r.cantidad,
      alicuota: r.alicuota,
      total_bs: dos(r.total_usd),
    })),
    totales: {
      base_bs: dos(d.base_usd),
      iva_bs: dos(d.iva_usd),
      total_bs: dos(d.total_usd),
      total_usd: d.total_usd,
      tasa_bs: d.tasa_bs,
      ...(d.igtf_usd != null ? { igtf_bs: dos(d.igtf_usd) } : {}),
    },
    // La zona libre del negocio, para que la pantalla que pinte la factura
    // ponga su logo, su color y su mensaje sin tocar lo obligatorio.
    marca: modelo && (modelo.mensaje_pie || modelo.logo_url || modelo.color)
      ? {
          mensaje_pie: modelo.mensaje_pie?.trim() || null,
          logo_url: modelo.logo_url || null,
          color: modelo.color || null,
        }
      : null,
    sello: sello.urlSello,
  };
}
