// EL PDF DE LA FACTURA QUE VIAJA EN EL CORREO.
//
// Representacion digital del documento emitido: los datos que exige el
// articulo 7 de la Providencia SNAT/2024/000102 van SIEMPRE y los arma este
// codigo sin preguntar; la marca del negocio (logo, color, mensaje) solo
// agrega en su zona libre.
//
// El molde es el de una factura comercial de verdad: banda de cabecera con la
// identidad, bloques separados de emisor y comprador, tabla de renglones con
// encabezado y filas alternadas, totales destacados a la derecha y un pie
// anclado abajo con el sello de comprobacion. La hoja se usa completa.
//
// pdf-lib y el nucleo de qrcode a proposito: los dos son JavaScript puro y
// corren en Cloudflare Workers sin navegador ni binarios. Las fuentes
// estandar (Helvetica, WinAnsi) llevan los acentos del español.
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import qr from 'qrcode/lib/core/qrcode.js';

const ANCHO_HOJA = 595.28;
const ALTO_HOJA = 841.89;
const MARGEN = 42;
const ANCHO = ANCHO_HOJA - MARGEN * 2;

const BANDA = 108; // alto de la cabecera de color
const PIE = 162; // alto reservado abajo, donde vive el sello de comprobacion

/** Bolivares con puntos de miles y coma decimal, sin depender de Intl. */
export function formatoBs(n) {
  const [entero, decimal] = Math.abs(Number(n)).toFixed(2).split('.');
  const conPuntos = entero.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
  return `${Number(n) < 0 ? '-' : ''}${conPuntos},${decimal}`;
}

/** La forma impresa del numero de control (art. 30): "00-00001234". */
export function controlTexto(n) {
  const v = Number(n) || 0;
  return `${String(Math.floor(v / 100000000)).padStart(2, '0')}-${String(v % 100000000).padStart(8, '0')}`;
}

const ddmmaaaa = (iso) => {
  const f = new Date(iso);
  return `${String(f.getDate()).padStart(2, '0')}/${String(f.getMonth() + 1).padStart(2, '0')}/${f.getFullYear()}`;
};
const horaCompleta = (iso) => {
  const f = new Date(iso);
  return `${String(f.getHours()).padStart(2, '0')}:${String(f.getMinutes()).padStart(2, '0')}:${String(f.getSeconds()).padStart(2, '0')}`;
};

function colorDeHex(hex) {
  if (typeof hex !== 'string' || !/^#[0-9a-fA-F]{6}$/.test(hex)) return null;
  return rgb(parseInt(hex.slice(1, 3), 16) / 255, parseInt(hex.slice(3, 5), 16) / 255, parseInt(hex.slice(5, 7), 16) / 255);
}

/** ¿El color es claro? Sobre un acento claro el texto blanco no se lee. */
function esClaro(color) {
  return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue > 0.62;
}

/** Los metodos de pago como los dice la gente, no como los guarda la base. */
const PAGO = {
  efectivo: 'Efectivo',
  pago_movil: 'Pago movil',
  transferencia: 'Transferencia',
  zelle: 'Zelle',
  punto_venta: 'Punto de venta',
  tarjeta: 'Tarjeta',
  credito: 'Credito',
};

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

/**
 * Arma el PDF. Datos:
 *   tipo, serie, numero, control (numerico), fecha (ISO), fecha_asignacion,
 *   rango { desde, hasta } | null,
 *   documento (el DocumentoFiscal completo tal como quedo sellado),
 *   marca { mensaje_pie, color } | null,
 *   logo { bytes: Uint8Array, tipo: 'png'|'jpg' } | null (ya bajado por quien llama),
 *   imprenta { rif, razon_social, providencia, providencia_fecha } | null,
 *   enlace (la direccion publica del sello).
 * Devuelve Uint8Array con el PDF.
 */
export async function pdfFactura(datos) {
  const d = datos.documento;
  const tasa = Number(d.tasa_bs) || 0;
  const bs = (usd) => formatoBs((Number(usd) || 0) * tasa);

  const acento = colorDeHex(datos.marca?.color) ?? rgb(0.05, 0.06, 0.12);
  const sobreAcento = esClaro(acento) ? rgb(0.1, 0.1, 0.12) : rgb(1, 1, 1);
  const tinta = rgb(0.13, 0.13, 0.16);
  const gris = rgb(0.45, 0.45, 0.5);
  const grisSuave = rgb(0.96, 0.96, 0.97);
  const raya = rgb(0.88, 0.88, 0.9);

  const doc = await PDFDocument.create();
  const fuente = await doc.embedFont(StandardFonts.Helvetica);
  const negrita = await doc.embedFont(StandardFonts.HelveticaBold);

  // El logo se incrusta UNA vez y se reusa en cada pagina.
  let logo = null;
  if (datos.logo?.bytes?.length) {
    try {
      logo = datos.logo.tipo === 'png' ? await doc.embedPng(datos.logo.bytes) : await doc.embedJpg(datos.logo.bytes);
    } catch {
      logo = null; // un logo dañado no tumba la factura
    }
  }

  let pagina;
  let y = 0;
  let numeroPagina = 0;

  const texto = (p, t, x, yy, { f = fuente, tamano = 9.5, color = tinta } = {}) =>
    p.drawText(String(t), { x, y: yy, size: tamano, font: f, color });

  /** Texto pegado a la derecha de una x tope. */
  const textoDer = (p, t, xTope, yy, { f = fuente, tamano = 9.5, color = tinta } = {}) => {
    const s = String(t);
    p.drawText(s, { x: xTope - f.widthOfTextAtSize(s, tamano), y: yy, size: tamano, font: f, color });
  };

  /** Recorta un texto para que quepa en un ancho, con puntos suspensivos. */
  const recortar = (t, maxAncho, f, tamano) => {
    let s = String(t ?? '');
    if (f.widthOfTextAtSize(s, tamano) <= maxAncho) return s;
    while (s.length > 1 && f.widthOfTextAtSize(s + '...', tamano) > maxAncho) s = s.slice(0, -1);
    return s + '...';
  };

  /** Parte un texto en varias lineas que quepan en un ancho. */
  const partir = (t, maxAncho, f, tamano, maxLineas = 3) => {
    const palabras = String(t ?? '').trim().split(/\s+/).filter(Boolean);
    const lineas = [];
    let actual = '';
    for (const p of palabras) {
      const cand = actual ? `${actual} ${p}` : p;
      if (f.widthOfTextAtSize(cand, tamano) <= maxAncho) actual = cand;
      else {
        if (actual) lineas.push(actual);
        actual = p;
        if (lineas.length >= maxLineas) break;
      }
    }
    if (actual && lineas.length < maxLineas) lineas.push(actual);
    return lineas;
  };

  // ===== La cabecera: banda de color con la identidad y el documento =====
  const dibujarCabecera = () => {
    pagina = doc.addPage([ANCHO_HOJA, ALTO_HOJA]);
    numeroPagina += 1;
    pagina.drawRectangle({ x: 0, y: ALTO_HOJA - BANDA, width: ANCHO_HOJA, height: BANDA, color: acento });

    let yb = ALTO_HOJA - 38;
    let xIzq = MARGEN;

    if (logo) {
      const alto = 44;
      const escala = Math.min(alto / logo.height, 150 / logo.width);
      pagina.drawImage(logo, {
        x: MARGEN,
        y: ALTO_HOJA - BANDA + (BANDA - logo.height * escala) / 2,
        width: logo.width * escala,
        height: logo.height * escala,
      });
      xIzq = MARGEN + logo.width * escala + 16;
    }

    const anchoIzq = ANCHO_HOJA / 2 - xIzq + 30;
    texto(pagina, recortar(d.emisor.razon_social, anchoIzq, negrita, 17), xIzq, yb, { f: negrita, tamano: 17, color: sobreAcento });
    yb -= 16;
    texto(pagina, `RIF: ${d.emisor.rif}`, xIzq, yb, { tamano: 9.5, color: sobreAcento });
    yb -= 12;
    for (const l of partir(d.emisor.domicilio_fiscal, anchoIzq, fuente, 8.5, 2)) {
      texto(pagina, l, xIzq, yb, { tamano: 8.5, color: sobreAcento });
      yb -= 10;
    }

    // El documento, a la derecha de la banda.
    const xDer = ANCHO_HOJA - MARGEN;
    let yd = ALTO_HOJA - 36;
    textoDer(pagina, TITULO[datos.tipo] ?? 'DOCUMENTO', xDer, yd, { f: negrita, tamano: 21, color: sobreAcento });
    yd -= 20;
    textoDer(pagina, `N° ${datos.serie}-${String(datos.numero).padStart(8, '0')}`, xDer, yd, { f: negrita, tamano: 11, color: sobreAcento });
    yd -= 15;
    textoDer(pagina, `N° de Control: ${controlTexto(datos.control)}`, xDer, yd, { f: negrita, tamano: 10, color: sobreAcento });
    yd -= 13;
    textoDer(pagina, `Emitida el ${ddmmaaaa(datos.fecha)} a las ${horaCompleta(datos.fecha)}`, xDer, yd, { tamano: 8.5, color: sobreAcento });

    y = ALTO_HOJA - BANDA - 26;
  };

  /** El encabezado de la tabla, que se repite en cada pagina. */
  const columnas = () => {
    const xDesc = MARGEN + 10;
    const xTotal = ANCHO_HOJA - MARGEN - 10;
    const xAlic = xTotal - 82;
    const xPrecio = xAlic - 18;
    const xCant = xPrecio - 92;
    return { xDesc, xCant, xPrecio, xAlic, xTotal };
  };

  const dibujarEncabezadoTabla = () => {
    const c = columnas();
    pagina.drawRectangle({ x: MARGEN, y: y - 19, width: ANCHO, height: 19, color: acento });
    const yt = y - 13;
    texto(pagina, 'DESCRIPCION', c.xDesc, yt, { f: negrita, tamano: 8, color: sobreAcento });
    textoDer(pagina, 'CANT.', c.xCant, yt, { f: negrita, tamano: 8, color: sobreAcento });
    textoDer(pagina, 'PRECIO UNIT.', c.xPrecio, yt, { f: negrita, tamano: 8, color: sobreAcento });
    texto(pagina, 'IVA', c.xAlic + 4, yt, { f: negrita, tamano: 8, color: sobreAcento });
    textoDer(pagina, 'TOTAL Bs', c.xTotal, yt, { f: negrita, tamano: 8, color: sobreAcento });
    y -= 19;
  };

  dibujarCabecera();

  // ===== Emisor y comprador, en dos bloques =====
  const ALTO_BLOQUE = 68;
  const anchoBloque = (ANCHO - 14) / 2;
  const yBloque = y - ALTO_BLOQUE;
  const idCliente = d.cliente?.rif ?? d.cliente?.cedula ?? null;

  const bloque = (x, titulo, lineas) => {
    pagina.drawRectangle({ x, y: yBloque, width: anchoBloque, height: ALTO_BLOQUE, color: grisSuave });
    pagina.drawRectangle({ x, y: yBloque, width: 3, height: ALTO_BLOQUE, color: acento });
    let yy = yBloque + ALTO_BLOQUE - 15;
    texto(pagina, titulo, x + 12, yy, { f: negrita, tamano: 7.5, color: gris });
    yy -= 15;
    for (const [t, opciones] of lineas) {
      if (!t) continue;
      texto(pagina, recortar(t, anchoBloque - 24, opciones?.f ?? fuente, opciones?.tamano ?? 9), x + 12, yy, opciones);
      yy -= (opciones?.tamano ?? 9) + 3.5;
    }
  };

  bloque(MARGEN, 'DATOS DEL EMISOR', [
    [d.emisor.razon_social, { f: negrita, tamano: 10 }],
    [`RIF: ${d.emisor.rif}`, { tamano: 9 }],
    [d.emisor.domicilio_fiscal, { tamano: 8.5, color: gris }],
  ]);
  bloque(MARGEN + anchoBloque + 14, 'DATOS DEL COMPRADOR', [
    [d.cliente?.nombre || 'Consumidor final', { f: negrita, tamano: 10 }],
    [idCliente ? `C.I./RIF: ${idCliente}` : 'Sin identificacion', { tamano: 9 }],
    [d.cliente?.direccion || '', { tamano: 8.5, color: gris }],
  ]);
  y = yBloque - 16;

  // ===== Fecha de asignacion y rango de controles (art. 7, nums. 5 y 15) =====
  const notas = [];
  if (datos.fecha_asignacion) notas.push(`Fecha de asignacion del N° de Control: ${ddmmaaaa(datos.fecha_asignacion)}`);
  if (datos.rango?.desde != null && datos.rango?.hasta != null) {
    notas.push(`Controles asignados: del N° ${datos.rango.desde} al N° ${datos.rango.hasta}`);
  }
  if (notas.length) {
    texto(pagina, notas.join('     '), MARGEN, y, { tamano: 7.5, color: gris });
    y -= 16;
  }

  // ===== La tabla de renglones =====
  dibujarEncabezadoTabla();
  const c = columnas();
  const ALTO_FILA = 20;
  const renglones = d.renglones ?? [];
  let alterna = false;
  let yTablaArriba = y; // donde arranca el cuerpo, para el marco lateral

  for (const r of renglones) {
    if (y - ALTO_FILA < PIE + 96) {
      // Se cierra el marco de esta pagina antes de pasar a la siguiente.
      pagina.drawLine({ start: { x: MARGEN, y: yTablaArriba }, end: { x: MARGEN, y }, thickness: 0.5, color: raya });
      pagina.drawLine({ start: { x: MARGEN + ANCHO, y: yTablaArriba }, end: { x: MARGEN + ANCHO, y }, thickness: 0.5, color: raya });
      dibujarCabecera();
      dibujarEncabezadoTabla();
      yTablaArriba = y;
      alterna = false;
    }
    if (alterna) {
      pagina.drawRectangle({ x: MARGEN, y: y - ALTO_FILA, width: ANCHO, height: ALTO_FILA, color: grisSuave });
    }
    alterna = !alterna;
    const yt = y - 13.5;
    const anchoDesc = c.xCant - c.xDesc - 30;
    texto(pagina, recortar(r.nombre, anchoDesc, fuente, 9.5), c.xDesc, yt, { tamano: 9.5 });
    textoDer(pagina, r.cantidad, c.xCant, yt, { tamano: 9.5 });
    textoDer(pagina, bs(r.precio_usd), c.xPrecio, yt, { tamano: 9.5 });
    texto(pagina, Number(r.alicuota) === 0 ? 'Exento' : `${r.alicuota}%`, c.xAlic + 4, yt, { tamano: 8.5, color: gris });
    textoDer(pagina, bs(r.total_usd), c.xTotal, yt, { f: negrita, tamano: 9.5 });
    y -= ALTO_FILA;
    pagina.drawLine({ start: { x: MARGEN, y }, end: { x: MARGEN + ANCHO, y }, thickness: 0.5, color: raya });
  }

  // El cuadro de la tabla se estira hasta donde empiezan los totales, como en
  // las facturas comerciales de toda la vida: el espacio que sobra queda DENTRO
  // del cuadro y no como un hueco en mitad de la hoja.
  const filasTotales =
    (d.por_alicuota ?? []).reduce((n, a) => n + (Number(a.alicuota) === 0 ? 1 : 2), 0) + (d.igtf_usd ? 1 : 0);
  const altoTotales = filasTotales * 15 + 34 + (tasa > 0 ? 12 : 0) + (d.pago?.metodo ? 12 : 0);
  const yCierreTabla = Math.min(y, PIE + altoTotales + 16);
  pagina.drawLine({ start: { x: MARGEN, y: yTablaArriba }, end: { x: MARGEN, y: yCierreTabla }, thickness: 0.5, color: raya });
  pagina.drawLine({ start: { x: MARGEN + ANCHO, y: yTablaArriba }, end: { x: MARGEN + ANCHO, y: yCierreTabla }, thickness: 0.5, color: raya });
  pagina.drawLine({ start: { x: MARGEN, y: yCierreTabla }, end: { x: MARGEN + ANCHO, y: yCierreTabla }, thickness: 0.5, color: raya });
  y = yCierreTabla;

  // ===== Totales, a la derecha =====
  y -= 18;
  const anchoTot = 232;
  const xTot = ANCHO_HOJA - MARGEN - anchoTot;
  const filaTotal = (etiqueta, valor, opciones = {}) => {
    texto(pagina, etiqueta, xTot + 12, y, { tamano: opciones.tamano ?? 9, color: opciones.color ?? gris });
    textoDer(pagina, valor, ANCHO_HOJA - MARGEN - 12, y, { f: opciones.f ?? fuente, tamano: opciones.tamano ?? 9, color: opciones.valorColor ?? tinta });
    y -= (opciones.tamano ?? 9) + 6;
  };

  for (const a of d.por_alicuota ?? []) {
    if (Number(a.alicuota) === 0) {
      filaTotal('Operaciones exentas', `Bs ${bs(a.base_usd)}`);
    } else {
      filaTotal(`Base imponible ${a.alicuota}%`, `Bs ${bs(a.base_usd)}`);
      filaTotal(`IVA ${a.alicuota}%`, `Bs ${bs(a.iva_usd)}`);
    }
  }
  if (d.igtf_usd) filaTotal('IGTF 3%', `Bs ${bs(d.igtf_usd)}`);

  // La banda del total: lo primero que el ojo busca.
  const totalFinal = Number(d.total_usd) + (Number(d.igtf_usd) || 0);
  y -= 4;
  pagina.drawRectangle({ x: xTot, y: y - 12, width: anchoTot, height: 30, color: acento });
  texto(pagina, d.igtf_usd ? 'TOTAL A PAGAR' : 'TOTAL', xTot + 12, y - 2, { f: negrita, tamano: 10.5, color: sobreAcento });
  textoDer(pagina, `Bs ${bs(totalFinal)}`, ANCHO_HOJA - MARGEN - 12, y - 3, { f: negrita, tamano: 13, color: sobreAcento });
  y -= 24;

  // Referencia en dolares y forma de pago, debajo del total.
  if (tasa > 0) {
    textoDer(pagina, `Tasa del dia Bs ${formatoBs(tasa)}   ·   Equivalente USD ${Number(totalFinal).toFixed(2)}`, ANCHO_HOJA - MARGEN - 12, y, { tamano: 8, color: gris });
    y -= 12;
  }
  if (d.pago?.metodo) {
    const metodo = PAGO[d.pago.metodo] ?? String(d.pago.metodo).replace(/_/g, ' ');
    textoDer(pagina, `Forma de pago: ${metodo}${d.pago.referencia ? `   ·   Referencia ${d.pago.referencia}` : ''}`, ANCHO_HOJA - MARGEN - 12, y, { tamano: 8, color: gris });
  }

  // ===== El pie: sello de comprobacion, mensaje del negocio e imprenta =====
  // Tres pisos que no se pisan: la raya arriba, el QR con su texto al lado, y
  // lo legal al ras del borde ocupando todo el ancho.
  const yRaya = 150;
  const yQrAbajo = 74;
  const LADO_QR = 64;
  pagina.drawLine({ start: { x: MARGEN, y: yRaya }, end: { x: MARGEN + ANCHO, y: yRaya }, thickness: 0.7, color: raya });

  // El QR, dibujado cuadrito a cuadrito (mismo sello que lleva el papel).
  let xTextoPie = MARGEN;
  try {
    const matriz = qr.create(datos.enlace, { errorCorrectionLevel: 'M' });
    const n = matriz.modules.size;
    const punto = LADO_QR / n;
    for (let fila = 0; fila < n; fila++) {
      for (let col = 0; col < n; col++) {
        if (matriz.modules.data[fila * n + col]) {
          pagina.drawRectangle({
            x: MARGEN + col * punto,
            y: yQrAbajo + (n - 1 - fila) * punto,
            width: punto + 0.3,
            height: punto + 0.3,
            color: tinta,
          });
        }
      }
    }
    xTextoPie = MARGEN + LADO_QR + 16;
  } catch {
    // Sin QR el documento igual se verifica por el enlace en texto.
  }

  let yp = yRaya - 14;
  texto(pagina, 'COMPRUEBA ESTE DOCUMENTO', xTextoPie, yp, { f: negrita, tamano: 8, color: gris });
  yp -= 12;
  texto(pagina, 'Escanea el codigo o entra a la direccion:', xTextoPie, yp, { tamano: 8, color: gris });
  yp -= 11;
  texto(pagina, recortar(datos.enlace.replace(/^https?:\/\//, ''), ANCHO - (xTextoPie - MARGEN), negrita, 8.5), xTextoPie, yp, { f: negrita, tamano: 8.5, color: acento });
  yp -= 16;

  if (datos.marca?.mensaje_pie) {
    for (const l of partir(datos.marca.mensaje_pie.slice(0, 240), ANCHO - (xTextoPie - MARGEN), fuente, 9, 2)) {
      texto(pagina, l, xTextoPie, yp, { tamano: 9, color: tinta });
      yp -= 11;
    }
  }

  // Lo de la imprenta y la leyenda, debajo del QR y a todo lo ancho.
  let yb2 = 50;
  if (datos.imprenta?.razon_social) {
    const prov = datos.imprenta.providencia
      ? `   ·   Providencia ${datos.imprenta.providencia}${datos.imprenta.providencia_fecha ? ` del ${ddmmaaaa(datos.imprenta.providencia_fecha + 'T12:00:00')}` : ''}`
      : '';
    texto(pagina, recortar(`Imprenta digital autorizada: ${datos.imprenta.razon_social}   ·   RIF: ${datos.imprenta.rif}${prov}`, ANCHO, fuente, 7.5), MARGEN, yb2, { tamano: 7.5, color: gris });
    yb2 -= 10;
  }
  texto(pagina, 'Representacion digital del documento emitido por medios electronicos.', MARGEN, yb2, { tamano: 7.5, color: gris });

  // Numeracion de paginas, solo si hay mas de una.
  if (numeroPagina > 1) {
    const paginas = doc.getPages();
    paginas.forEach((p, i) => {
      textoDer(p, `Pagina ${i + 1} de ${paginas.length}`, ANCHO_HOJA - MARGEN, 26, { f: fuente, tamano: 7.5, color: gris });
    });
  }

  return doc.save();
}
