import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const require = createRequire(import.meta.url);
const sharp = require("sharp");

const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(SCRIPT_DIR, "..");
const SLUG = "empresas-ecommerce-facturacion-espana-2026";
const FOLDER = "empresas-ecommerce-eurostat-2026";
const RESEARCH_DIR = path.join(ROOT, "docs", "investigaciones", FOLDER);
const DATA_DIR = path.join(RESEARCH_DIR, "datos");
const SOURCE_DIR = path.join(RESEARCH_DIR, "fuentes");
const CHART_DIR = path.join(RESEARCH_DIR, "graficos");
const PUBLIC_DIR = path.join(ROOT, "public", "assets", "investigaciones", FOLDER);
const LOGO_PATH = path.join(ROOT, "public", "assets", "tienrank-brand-v3.webp");
const EUROSTAT_API = "https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data";
const RETRIEVED_AT = "2026-08-03";
const OBSERVATION_TIME = "2025";

const EU27 = [
  "BE",
  "BG",
  "CZ",
  "DK",
  "DE",
  "EE",
  "IE",
  "EL",
  "ES",
  "FR",
  "HR",
  "IT",
  "CY",
  "LV",
  "LT",
  "LU",
  "HU",
  "MT",
  "NL",
  "AT",
  "PL",
  "PT",
  "RO",
  "SI",
  "SK",
  "FI",
  "SE"
];

const COUNTRY_NAMES = {
  EU27_2020: "UE-27",
  AT: "Austria",
  BE: "Belgica",
  BG: "Bulgaria",
  CY: "Chipre",
  CZ: "Chequia",
  DE: "Alemania",
  DK: "Dinamarca",
  EE: "Estonia",
  EL: "Grecia",
  ES: "Espana",
  FI: "Finlandia",
  FR: "Francia",
  HR: "Croacia",
  HU: "Hungria",
  IE: "Irlanda",
  IT: "Italia",
  LT: "Lituania",
  LU: "Luxemburgo",
  LV: "Letonia",
  MT: "Malta",
  NL: "Paises Bajos",
  PL: "Polonia",
  PT: "Portugal",
  RO: "Rumania",
  SE: "Suecia",
  SI: "Eslovenia",
  SK: "Eslovaquia"
};

const SELECTED_GEOS = ["EU27_2020", "ES", "DE", "FR", "IT", "PT", "NL", "AT", "BE", "DK", "SE", "IE", "PL"];
const SIZE_GROUPS = ["10-49", "50-249", "GE250", "GE10"];
const SIZE_LABELS = {
  "10-49": "10-49 empleados",
  "50-249": "50-249 empleados",
  GE250: "250 o mas empleados",
  GE10: "10 o mas empleados"
};

const ENTERPRISE_INDICATORS = {
  ecommerce_enterprises_pct: "E_AESELL",
  ecommerce_at_least_1pct_turnover_pct: "E_ESELL",
  web_sales_enterprises_pct: "E_AWSELL",
  edi_sales_enterprises_pct: "E_AXSELL",
  own_site_enterprises_pct: "E_AWS_COWN",
  marketplace_enterprises_pct: "E_AWS_CMP",
  marketplace_20pct_enterprises_pct: "E_AWS_CMP_GE20",
  web_sales_foreign_enterprises_pct: "E_AWSFOR",
  web_sales_other_eu_enterprises_pct: "E_AWSEU",
  web_sales_rest_world_enterprises_pct: "E_AWSWW",
  physical_goods_web_enterprises_pct: "E_AWSPG",
  digital_services_web_enterprises_pct: "E_AWSDS",
  other_services_web_enterprises_pct: "E_AWSSV"
};

const TURNOVER_INDICATORS = {
  ecommerce_turnover_pct: "E_ETURN",
  web_sales_turnover_pct: "E_AWSVAL",
  edi_sales_turnover_pct: "E_AXSVAL",
  own_site_turnover_pct: "E_AWSVAL_COWN",
  marketplace_turnover_pct: "E_AWSVAL_CMP",
  domestic_web_turnover_pct: "E_AWSVAL_HM",
  other_eu_web_turnover_pct: "E_AWSVAL_EU",
  rest_world_web_turnover_pct: "E_AWSVAL_WW"
};

const OBSTACLE_INDICATORS = {
  any_difficulty_pct_of_eu_web_sellers: "E_AWSEU_DANY",
  delivery_returns_cost_pct_of_eu_web_sellers: "E_AWSEU_DHCD",
  complaints_disputes_pct_of_eu_web_sellers: "E_AWSEU_DRCD",
  product_labelling_pct_of_eu_web_sellers: "E_AWSEU_DAPL",
  languages_pct_of_eu_web_sellers: "E_AWSEU_DFL",
  partner_restrictions_pct_of_eu_web_sellers: "E_AWSEU_DBP",
  vat_pct_of_eu_web_sellers: "E_AWSEU_DVAT",
  no_difficulty_pct_of_eu_web_sellers: "E_AWSEU_DNONE"
};

const SECTORS = [
  ["C10-S951_X_K", "Total empresas >=10"],
  ["G", "Comercio mayorista y minorista"],
  ["G46", "Comercio mayorista"],
  ["G47", "Comercio minorista"],
  ["C", "Manufacturas"],
  ["C10-C12", "Alimentacion, bebidas y tabaco"],
  ["I", "Alojamiento y restauracion"],
  ["I55", "Alojamiento"],
  ["N79", "Agencias de viaje y reservas"],
  ["H", "Transporte y almacenamiento"],
  ["J", "Informacion y comunicacion"],
  ["J62_J63", "Programacion y servicios informaticos"],
  ["ICT", "Sector TIC total"]
];

const COLORS = {
  ink: "#121826",
  slate: "#344054",
  muted: "#667085",
  grid: "#E4E7EC",
  panel: "#F6F8FA",
  white: "#FFFFFF",
  green: "#12B76A",
  greenDark: "#067647",
  mint: "#EAFBF3",
  blue: "#2563EB",
  blueDark: "#1D4ED8",
  bluePale: "#EAF1FF",
  coral: "#EF5F48",
  coralDark: "#B42318",
  coralPale: "#FFF0EC",
  amber: "#D98A00",
  amberPale: "#FFF7E6",
  violet: "#7C3AED",
  gray: "#98A2B3",
  darkPanel: "#111827"
};

const WIDTH = 1600;
const HEIGHT = 1000;

function eurostatUrl(dataset, filters) {
  const params = new URLSearchParams({ lang: "en", ...filters });
  return `${EUROSTAT_API}/${dataset}?${params}`;
}

async function fetchEurostat(dataset, filters) {
  const url = eurostatUrl(dataset, filters);
  const response = await fetch(url);
  if (!response.ok) throw new Error(`Eurostat ${response.status} for ${url}`);
  return { url, data: await response.json() };
}

function dimensionCodes(data, dimensionId) {
  return Object.entries(data.dimension[dimensionId].category.index)
    .sort((left, right) => left[1] - right[1])
    .map(([code]) => code);
}

function jsonStatValue(data, selectors) {
  let flatIndex = 0;
  let stride = 1;

  for (let index = data.id.length - 1; index >= 0; index -= 1) {
    const dimensionId = data.id[index];
    const selectedCode = selectors[dimensionId] ?? dimensionCodes(data, dimensionId)[0];
    const selectedIndex = data.dimension[dimensionId].category.index[selectedCode];

    if (selectedIndex === undefined) return null;
    flatIndex += selectedIndex * stride;
    stride *= data.size[index];
  }

  return data.value?.[flatIndex] ?? null;
}

function round(value, decimals = 2) {
  return value === null || value === undefined || Number.isNaN(Number(value)) ? null : Number(Number(value).toFixed(decimals));
}

function diff(left, right) {
  return left === null || right === null ? null : round(left - right, 2);
}

function ratio(left, right) {
  return left === null || right === null || right === 0 ? null : round(left / right, 2);
}

function pct(value, decimals = 1) {
  if (value === null || value === undefined) return "n/d";
  return `${Number(value).toFixed(decimals).replace(".", ",")}%`;
}

function pp(value, decimals = 1, forceSign = true) {
  if (value === null || value === undefined) return "n/d";
  const fixed = Number(value).toFixed(decimals).replace(".", ",");
  if (!forceSign) return `${fixed} pp`;
  return `${value > 0 ? "+" : ""}${fixed} pp`;
}

function numberEs(value, decimals = 1) {
  if (value === null || value === undefined) return "n/d";
  return Number(value).toFixed(decimals).replace(".", ",");
}

function csvCell(value) {
  if (value === null || value === undefined) return "";
  const text = String(value);
  return /[",\n]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}

function toCsv(rows, columns) {
  return `${[columns, ...rows.map((row) => columns.map((column) => row[column] ?? ""))]
    .map((row) => row.map(csvCell).join(","))
    .join("\n")}\n`;
}

function rankRows(rows, field, direction = "desc") {
  return rows
    .filter((row) => row[field] !== null && row[field] !== undefined)
    .toSorted((left, right) => (direction === "asc" ? left[field] - right[field] : right[field] - left[field]))
    .map((row, index) => ({ ...row, rank: index + 1 }));
}

function rankOf(rows, field, geo = "ES", direction = "desc") {
  const ranked = rankRows(rows, field, direction);
  const match = ranked.find((row) => row.geo === geo);
  return { rank: match?.rank ?? null, total: ranked.length };
}

function escapeXml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&apos;");
}

function escapeHtml(value) {
  return escapeXml(value);
}

function text({ x, y, value, size = 18, color = COLORS.ink, weight = 400, anchor = "start" }) {
  return `<text x="${x}" y="${y}" font-family="Arial, Helvetica, sans-serif" font-size="${size}" font-weight="${weight}" fill="${color}" text-anchor="${anchor}">${escapeXml(value)}</text>`;
}

function multiline({ x, y, lines, size = 18, color = COLORS.ink, weight = 400, lineHeight = 1.28, anchor = "start" }) {
  return `<text x="${x}" y="${y}" font-family="Arial, Helvetica, sans-serif" font-size="${size}" font-weight="${weight}" fill="${color}" text-anchor="${anchor}">${lines
    .map((line, index) => `<tspan x="${x}" dy="${index === 0 ? 0 : size * lineHeight}">${escapeXml(line)}</tspan>`)
    .join("")}</text>`;
}

function rect({ x, y, width, height, fill, stroke = "none", strokeWidth = 0, radius = 0 }) {
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
}

function line({ x1, y1, x2, y2, color = COLORS.grid, width = 1 }) {
  return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${color}" stroke-width="${width}" stroke-linecap="round" />`;
}

function circle({ x, y, radius, fill, stroke = "none", strokeWidth = 0 }) {
  return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
}

function pill({ x, y, width, height, fill, label, labelColor = COLORS.ink, size = 14 }) {
  return [
    rect({ x, y, width, height, fill, radius: height / 2 }),
    text({
      x: x + width / 2,
      y: y + height / 2 + size * 0.34,
      value: label,
      size,
      color: labelColor,
      weight: 700,
      anchor: "middle"
    })
  ].join("");
}

function svgDocument(body) {
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
    <rect width="${WIDTH}" height="${HEIGHT}" fill="${COLORS.white}" />
    ${body}
  </svg>`;
}

async function brandHeader(label) {
  const logo = await readFile(LOGO_PATH);
  const dataUri = `data:image/webp;base64,${logo.toString("base64")}`;
  return [
    `<image href="${dataUri}" x="78" y="46" width="48" height="48" />`,
    text({ x: 140, y: 78, value: "TienRank", size: 23, weight: 700 }),
    pill({ x: 1175, y: 50, width: 345, height: 36, fill: COLORS.panel, label, labelColor: COLORS.muted, size: 12 })
  ].join("");
}

function sourceFooter(value) {
  return [line({ x1: 80, y1: 930, x2: 1520, y2: 930 }), text({ x: 80, y: 963, value, size: 13, color: COLORS.muted })].join("");
}

async function renderPng(svg, filename) {
  await mkdir(CHART_DIR, { recursive: true });
  await mkdir(PUBLIC_DIR, { recursive: true });
  const buffer = await sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer();
  await writeFile(path.join(CHART_DIR, filename), buffer);
  await writeFile(path.join(PUBLIC_DIR, filename), buffer);
}

async function heroChart(summary) {
  const parts = [
    await brandHeader("ANALISIS TIENRANK | AGOSTO 2026"),
    pill({ x: 80, y: 132, width: 240, height: 34, fill: COLORS.bluePale, label: "ECOMMERCE EMPRESARIAL", labelColor: COLORS.blueDark, size: 12 }),
    text({ x: 80, y: 218, value: "España vende online más que la UE", size: 45, weight: 700 }),
    text({ x: 80, y: 272, value: "pero convierte menos facturación", size: 45, weight: 700 }),
    text({ x: 80, y: 318, value: "Empresas de 10 o más empleados. Eurostat 2025; ventas ecommerce referidas al último año natural.", size: 18, color: COLORS.muted }),
    rect({ x: 80, y: 392, width: 438, height: 265, fill: COLORS.mint, radius: 8 }),
    text({ x: 116, y: 438, value: "EMPRESAS CON VENTAS ECOMMERCE", size: 13, color: COLORS.greenDark, weight: 700 }),
    text({ x: 116, y: 520, value: pct(summary.spain.ecommerce_enterprises_pct), size: 70, color: COLORS.greenDark, weight: 700 }),
    text({ x: 116, y: 566, value: `UE-27: ${pct(summary.eu.ecommerce_enterprises_pct)} (${pp(summary.gaps.ecommerce_enterprises_pct)})`, size: 19, color: COLORS.slate, weight: 700 }),
    text({ x: 116, y: 604, value: `Puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de ${summary.ranks.ecommerce_enterprises_pct.total} en la UE-27`, size: 16, color: COLORS.muted }),
    rect({ x: 582, y: 392, width: 438, height: 265, fill: COLORS.coralPale, radius: 8 }),
    text({ x: 618, y: 438, value: "FACTURACION POR ECOMMERCE", size: 13, color: COLORS.coralDark, weight: 700 }),
    text({ x: 618, y: 520, value: pct(summary.spain.ecommerce_turnover_pct), size: 70, color: COLORS.coralDark, weight: 700 }),
    text({ x: 618, y: 566, value: `UE-27: ${pct(summary.eu.ecommerce_turnover_pct)} (${pp(summary.gaps.ecommerce_turnover_pct)})`, size: 19, color: COLORS.slate, weight: 700 }),
    text({ x: 618, y: 604, value: `Puesto ${summary.ranks.ecommerce_turnover_pct.rank} de ${summary.ranks.ecommerce_turnover_pct.total}`, size: 16, color: COLORS.muted }),
    rect({ x: 1084, y: 392, width: 436, height: 265, fill: COLORS.bluePale, radius: 8 }),
    text({ x: 1120, y: 438, value: "VENTA WEB AL EXTRANJERO", size: 13, color: COLORS.blueDark, weight: 700 }),
    text({ x: 1120, y: 520, value: pct(summary.spain.web_sales_foreign_share_of_web_sellers_pct), size: 70, color: COLORS.blueDark, weight: 700 }),
    text({ x: 1120, y: 566, value: `UE-27: ${pct(summary.eu.web_sales_foreign_share_of_web_sellers_pct)}`, size: 19, color: COLORS.slate, weight: 700 }),
    text({ x: 1120, y: 604, value: pp(summary.gaps.web_sales_foreign_share_of_web_sellers_pct), size: 16, color: COLORS.muted }),
    rect({ x: 80, y: 725, width: 1440, height: 96, fill: COLORS.darkPanel, radius: 8 }),
    multiline({
      x: 116,
      y: 763,
      lines: [
        "Lectura: la madurez no se mide solo por cuántas empresas aceptan pedidos online.",
        "El cuello de botella español está en transformar esa presencia en volumen, escala y venta internacional."
      ],
      size: 20,
      color: COLORS.white,
      weight: 700
    }),
    sourceFooter("Fuentes: Eurostat isoc_ec_esels/isoc_ec_evals (2025), INE ETICCE 2024-1T2025 y OCDE. Calculos de TienRank.")
  ];
  await renderPng(svgDocument(parts.join("")), "03-resumen-empresas-ecommerce.png");
}

async function adoptionTurnoverChart(countryRows) {
  const countries = ["IE", "DK", "SE", "BE", "NL", "AT", "DE", "ES", "PT", "IT", "FR", "PL"];
  const plot = { x: 300, y: 350, w: 930, h: 480 };
  const xMin = 15;
  const xMax = 40;
  const yMin = 10;
  const yMax = 40;
  const scaleX = (value) => plot.x + ((value - xMin) / (xMax - xMin)) * plot.w;
  const scaleY = (value) => plot.y + plot.h - ((value - yMin) / (yMax - yMin)) * plot.h;
  const eu = countryRows.find((row) => row.geo === "EU27_2020");

  const parts = [
    await brandHeader("EUROSTAT 2025 | EMPRESAS >=10"),
    pill({ x: 80, y: 132, width: 222, height: 34, fill: COLORS.coralPale, label: "ADOPCION VS VOLUMEN", labelColor: COLORS.coralDark, size: 12 }),
    text({ x: 80, y: 218, value: "Más adopción no siempre implica", size: 43, weight: 700 }),
    text({ x: 80, y: 270, value: "más peso en facturación", size: 43, weight: 700 }),
    text({ x: 80, y: 314, value: "Eje X: empresas con ventas ecommerce. Eje Y: facturación total que procede del ecommerce.", size: 17, color: COLORS.muted })
  ];

  for (let tick = xMin; tick <= xMax; tick += 5) {
    const x = scaleX(tick);
    parts.push(line({ x1: x, y1: plot.y, x2: x, y2: plot.y + plot.h, color: "#EEF0F3" }));
    parts.push(text({ x, y: plot.y + plot.h + 34, value: `${tick}%`, size: 13, color: COLORS.muted, anchor: "middle" }));
  }
  for (let tick = yMin; tick <= yMax; tick += 5) {
    const y = scaleY(tick);
    parts.push(line({ x1: plot.x, y1: y, x2: plot.x + plot.w, y2: y, color: "#EEF0F3" }));
    parts.push(text({ x: plot.x - 18, y: y + 5, value: `${tick}%`, size: 13, color: COLORS.muted, anchor: "end" }));
  }

  parts.push(line({ x1: scaleX(eu.ecommerce_enterprises_pct), y1: plot.y, x2: scaleX(eu.ecommerce_enterprises_pct), y2: plot.y + plot.h, color: COLORS.green, width: 2 }));
  parts.push(line({ x1: plot.x, y1: scaleY(eu.ecommerce_turnover_pct), x2: plot.x + plot.w, y2: scaleY(eu.ecommerce_turnover_pct), color: COLORS.coral, width: 2 }));
  parts.push(text({ x: scaleX(eu.ecommerce_enterprises_pct) + 12, y: plot.y + 22, value: "UE empresas", size: 13, color: COLORS.greenDark, weight: 700 }));
  parts.push(text({ x: plot.x + plot.w - 92, y: scaleY(eu.ecommerce_turnover_pct) - 10, value: "UE facturacion", size: 13, color: COLORS.coralDark, weight: 700 }));

  for (const geo of countries) {
    const row = countryRows.find((item) => item.geo === geo);
    if (!row || row.ecommerce_enterprises_pct === null || row.ecommerce_turnover_pct === null) continue;
    const isSpain = geo === "ES";
    const labelOffset = geo === "ES" ? { x: 18, y: 18 } : geo === "AT" ? { x: 18, y: -26 } : { x: 16, y: -12 };
    const x = scaleX(row.ecommerce_enterprises_pct);
    const y = scaleY(row.ecommerce_turnover_pct);
    parts.push(circle({ x, y, radius: isSpain ? 16 : 10, fill: isSpain ? COLORS.blue : COLORS.gray, stroke: COLORS.white, strokeWidth: 3 }));
    parts.push(text({ x: x + labelOffset.x, y: y + labelOffset.y, value: COUNTRY_NAMES[geo], size: isSpain ? 17 : 13, color: isSpain ? COLORS.blueDark : COLORS.slate, weight: isSpain ? 700 : 600 }));
  }

  parts.push(text({ x: plot.x + plot.w / 2, y: 900, value: "Empresas con ventas ecommerce (% de empresas)", size: 15, color: COLORS.slate, weight: 700, anchor: "middle" }));
  parts.push(text({ x: 92, y: 550, value: "Facturacion ecommerce", size: 15, color: COLORS.slate, weight: 700 }));
  parts.push(sourceFooter("Fuente: Eurostat isoc_ec_esels e isoc_ec_evals, time=2025. Ranking descriptivo de paises con dato disponible. Calculos de TienRank."));

  await renderPng(svgDocument(parts.join("")), "01-adopcion-vs-facturacion-ue.png");
}

async function sizeGapChart(countryRowsBySize) {
  const groups = ["10-49", "50-249", "GE250", "GE10"];
  const parts = [
    await brandHeader("EUROSTAT 2025 | TAMANO DE EMPRESA"),
    pill({ x: 80, y: 132, width: 180, height: 34, fill: COLORS.mint, label: "BRECHA PYME", labelColor: COLORS.greenDark, size: 12 }),
    text({ x: 80, y: 218, value: "La brecha cambia de signo", size: 43, weight: 700 }),
    text({ x: 80, y: 270, value: "según el tamaño de empresa", size: 43, weight: 700 }),
    text({ x: 80, y: 314, value: "España supera a la UE en adopción en todos los tamaños, pero la pequeña empresa factura menos por ecommerce.", size: 17, color: COLORS.muted })
  ];

  const chartX = 435;
  const chartW = 850;
  const rowY = 402;
  const rowGap = 112;
  const max = 60;
  for (let tick = 0; tick <= max; tick += 10) {
    const x = chartX + (tick / max) * chartW;
    parts.push(line({ x1: x, y1: 360, x2: x, y2: 800, color: "#EEF0F3" }));
    parts.push(text({ x, y: 833, value: `${tick}%`, size: 13, color: COLORS.muted, anchor: "middle" }));
  }

  for (const [index, size] of groups.entries()) {
    const row = countryRowsBySize[size];
    const spain = row.find((item) => item.geo === "ES");
    const eu = row.find((item) => item.geo === "EU27_2020");
    const y = rowY + index * rowGap;
    const barH = 20;
    parts.push(text({ x: 80, y: y + 7, value: SIZE_LABELS[size], size: 18, weight: 700 }));
    parts.push(text({ x: 80, y: y + 34, value: `Adopcion: ${pp(diff(spain.ecommerce_enterprises_pct, eu.ecommerce_enterprises_pct))}`, size: 14, color: COLORS.greenDark, weight: 700 }));
    parts.push(text({ x: 80, y: y + 58, value: `Facturacion: ${pp(diff(spain.ecommerce_turnover_pct, eu.ecommerce_turnover_pct))}`, size: 14, color: diff(spain.ecommerce_turnover_pct, eu.ecommerce_turnover_pct) >= 0 ? COLORS.greenDark : COLORS.coralDark, weight: 700 }));
    const esAdoptionW = (spain.ecommerce_enterprises_pct / max) * chartW;
    const euAdoptionW = (eu.ecommerce_enterprises_pct / max) * chartW;
    const esTurnoverW = (spain.ecommerce_turnover_pct / max) * chartW;
    const euTurnoverW = (eu.ecommerce_turnover_pct / max) * chartW;
    parts.push(rect({ x: chartX, y: y - 22, width: euAdoptionW, height: barH, fill: COLORS.green, radius: 4 }));
    parts.push(rect({ x: chartX, y: y + 4, width: esAdoptionW, height: barH, fill: COLORS.greenDark, radius: 4 }));
    parts.push(rect({ x: chartX, y: y + 38, width: euTurnoverW, height: barH, fill: COLORS.coral, radius: 4 }));
    parts.push(rect({ x: chartX, y: y + 64, width: esTurnoverW, height: barH, fill: COLORS.coralDark, radius: 4 }));
    parts.push(text({ x: chartX + euAdoptionW + 10, y: y - 6, value: `UE ${pct(eu.ecommerce_enterprises_pct)}`, size: 13, color: COLORS.greenDark, weight: 700 }));
    parts.push(text({ x: chartX + esAdoptionW + 10, y: y + 20, value: `ES ${pct(spain.ecommerce_enterprises_pct)}`, size: 13, color: COLORS.greenDark, weight: 700 }));
    parts.push(text({ x: chartX + euTurnoverW + 10, y: y + 54, value: `UE ${pct(eu.ecommerce_turnover_pct)}`, size: 13, color: COLORS.coralDark, weight: 700 }));
    parts.push(text({ x: chartX + esTurnoverW + 10, y: y + 80, value: `ES ${pct(spain.ecommerce_turnover_pct)}`, size: 13, color: COLORS.coralDark, weight: 700 }));
  }

  parts.push(sourceFooter("Fuente: Eurostat, isoc_ec_esels e isoc_ec_evals por clase de tamano, time=2025. Calculos de TienRank."));
  await renderPng(svgDocument(parts.join("")), "02-brecha-tamano-empresa.png");
}

async function channelChart(summary) {
  const groups = [
    ["Web propia entre vendedores web", summary.spain.own_site_share_of_web_sellers_pct, summary.eu.own_site_share_of_web_sellers_pct, COLORS.greenDark],
    ["Marketplace entre vendedores web", summary.spain.marketplace_share_of_web_sellers_pct, summary.eu.marketplace_share_of_web_sellers_pct, COLORS.blueDark],
    ["Venta web al extranjero", summary.spain.web_sales_foreign_share_of_web_sellers_pct, summary.eu.web_sales_foreign_share_of_web_sellers_pct, COLORS.coralDark]
  ];
  const parts = [
    await brandHeader("EUROSTAT 2025 | CANALES"),
    pill({ x: 80, y: 132, width: 232, height: 34, fill: COLORS.bluePale, label: "WEB, MARKETPLACE Y UE", labelColor: COLORS.blueDark, size: 12 }),
    text({ x: 80, y: 218, value: "España se apoya más en web propia", size: 43, weight: 700 }),
    text({ x: 80, y: 270, value: "y menos en venta exterior", size: 43, weight: 700 }),
    text({ x: 80, y: 314, value: "Porcentaje sobre empresas con ventas web. Las categorías pueden solaparse.", size: 17, color: COLORS.muted })
  ];

  const chartX = 470;
  const chartW = 780;
  for (let tick = 0; tick <= 100; tick += 20) {
    const x = chartX + (tick / 100) * chartW;
    parts.push(line({ x1: x, y1: 360, x2: x, y2: 760, color: "#EEF0F3" }));
    parts.push(text({ x, y: 795, value: `${tick}%`, size: 13, color: COLORS.muted, anchor: "middle" }));
  }

  groups.forEach(([label, es, eu, color], index) => {
    const y = 420 + index * 134;
    parts.push(text({ x: 80, y: y + 12, value: label, size: 19, weight: 700 }));
    parts.push(rect({ x: chartX, y: y - 22, width: (eu / 100) * chartW, height: 30, fill: COLORS.gray, radius: 5 }));
    parts.push(rect({ x: chartX, y: y + 20, width: (es / 100) * chartW, height: 30, fill: color, radius: 5 }));
    parts.push(text({ x: chartX + (eu / 100) * chartW + 12, y: y + 1, value: `UE ${pct(eu)}`, size: 15, color: COLORS.slate, weight: 700 }));
    parts.push(text({ x: chartX + (es / 100) * chartW + 12, y: y + 43, value: `ES ${pct(es)}`, size: 15, color, weight: 700 }));
    parts.push(pill({ x: 1320, y: y - 6, width: 150, height: 36, fill: COLORS.panel, label: pp(es - eu), labelColor: color, size: 14 }));
  });

  parts.push(rect({ x: 80, y: 822, width: 1440, height: 58, fill: COLORS.panel, radius: 8 }));
  parts.push(text({ x: 112, y: 858, value: `Facturacion: web ${pct(summary.spain.web_sales_turnover_pct)} del total en España frente a ${pct(summary.eu.web_sales_turnover_pct)} en la UE; EDI ${pct(summary.spain.edi_sales_turnover_pct)} frente a ${pct(summary.eu.edi_sales_turnover_pct)}.`, size: 17, color: COLORS.slate, weight: 700 }));
  parts.push(sourceFooter("Fuente: Eurostat, isoc_ec_esels e isoc_ec_evals, time=2025. Base indicada en cada variable. Calculos de TienRank."));
  await renderPng(svgDocument(parts.join("")), "04-canales-web-marketplace-exterior.png");
}

async function sectorChart(sectorRows) {
  const selected = ["I55", "N79", "I", "C10-C12", "C", "H", "J", "G", "G47"];
  const rows = selected.map((nace) => sectorRows.find((row) => row.nace === nace)).filter(Boolean);
  const max = 55;
  const chartX = 510;
  const chartW = 830;
  const parts = [
    await brandHeader("EUROSTAT 2025 | SECTORES EN ESPAÑA"),
    pill({ x: 80, y: 132, width: 214, height: 34, fill: COLORS.amberPale, label: "SECTORES", labelColor: COLORS.amber, size: 12 }),
    text({ x: 80, y: 218, value: "El ecommerce empresarial español", size: 43, weight: 700 }),
    text({ x: 80, y: 270, value: "no es solo retail", size: 43, weight: 700 }),
    text({ x: 80, y: 314, value: "Peso de ventas ecommerce en la facturación sectorial; barra dividida entre web y EDI.", size: 17, color: COLORS.muted })
  ];

  for (let tick = 0; tick <= max; tick += 10) {
    const x = chartX + (tick / max) * chartW;
    parts.push(line({ x1: x, y1: 360, x2: x, y2: 822, color: "#EEF0F3" }));
    parts.push(text({ x, y: 854, value: `${tick}%`, size: 13, color: COLORS.muted, anchor: "middle" }));
  }

  rows.forEach((row, index) => {
    const y = 390 + index * 52;
    const webW = (row.web_sales_turnover_pct / max) * chartW;
    const ediW = (row.edi_sales_turnover_pct / max) * chartW;
    parts.push(text({ x: 80, y: y + 22, value: row.sector_short, size: 15, color: COLORS.ink, weight: 700 }));
    parts.push(rect({ x: chartX, y, width: webW, height: 30, fill: COLORS.greenDark, radius: 4 }));
    parts.push(rect({ x: chartX + webW, y, width: ediW, height: 30, fill: COLORS.blue, radius: 0 }));
    parts.push(text({ x: chartX + webW + ediW + 12, y: y + 21, value: pct(row.ecommerce_turnover_pct), size: 14, color: COLORS.slate, weight: 700 }));
  });

  parts.push(rect({ x: 1180, y: 174, width: 24, height: 14, fill: COLORS.greenDark, radius: 2 }));
  parts.push(text({ x: 1214, y: 187, value: "Web/app/marketplace", size: 14, color: COLORS.slate }));
  parts.push(rect({ x: 1180, y: 204, width: 24, height: 14, fill: COLORS.blue, radius: 2 }));
  parts.push(text({ x: 1214, y: 217, value: "EDI", size: 14, color: COLORS.slate }));
  parts.push(sourceFooter("Fuente: Eurostat isoc_ec_evaln2, España, time=2025. Sectores NACE Rev. 2 seleccionados. Calculos de TienRank."));
  await renderPng(svgDocument(parts.join("")), "05-sectores-ecommerce-espana.png");
}

function makeRows({ esels, evals }) {
  const countryRowsBySize = {};
  for (const size of SIZE_GROUPS) {
    countryRowsBySize[size] = SELECTED_GEOS.map((geo) => {
      const row = { geo, country: COUNTRY_NAMES[geo], size_emp: size, size_label: SIZE_LABELS[size] };
      for (const [field, indicator] of Object.entries(ENTERPRISE_INDICATORS)) {
        row[field] = round(jsonStatValue(esels.data, { geo, size_emp: size, indic_is: indicator, unit: "PC_ENT" }));
      }
      for (const [field, indicator] of Object.entries(TURNOVER_INDICATORS)) {
        row[field] = round(jsonStatValue(evals.data, { geo, size_emp: size, indic_is: indicator, unit: "PC_TURN" }));
      }
      row.own_site_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: size, indic_is: "E_AWS_COWN", unit: "PC_ENT_AWSELL" }));
      row.marketplace_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: size, indic_is: "E_AWS_CMP", unit: "PC_ENT_AWSELL" }));
      row.web_sales_foreign_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: size, indic_is: "E_AWSFOR", unit: "PC_ENT_AWSELL" }));
      row.ecommerce_turnover_per_seller_index = ratio(row.ecommerce_turnover_pct, row.ecommerce_enterprises_pct);
      return row;
    });
  }
  return countryRowsBySize;
}

function makeEuRankings({ esels, evals }) {
  const rows = EU27.map((geo) => {
    const row = { geo, country: COUNTRY_NAMES[geo], size_emp: "GE10" };
    for (const [field, indicator] of Object.entries(ENTERPRISE_INDICATORS)) {
      row[field] = round(jsonStatValue(esels.data, { geo, size_emp: "GE10", indic_is: indicator, unit: "PC_ENT" }));
    }
    for (const [field, indicator] of Object.entries(TURNOVER_INDICATORS)) {
      row[field] = round(jsonStatValue(evals.data, { geo, size_emp: "GE10", indic_is: indicator, unit: "PC_TURN" }));
    }
    row.own_site_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: "GE10", indic_is: "E_AWS_COWN", unit: "PC_ENT_AWSELL" }));
    row.marketplace_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: "GE10", indic_is: "E_AWS_CMP", unit: "PC_ENT_AWSELL" }));
    row.web_sales_foreign_share_of_web_sellers_pct = round(jsonStatValue(esels.data, { geo, size_emp: "GE10", indic_is: "E_AWSFOR", unit: "PC_ENT_AWSELL" }));
    row.ecommerce_turnover_per_seller_index = ratio(row.ecommerce_turnover_pct, row.ecommerce_enterprises_pct);
    return row;
  });

  const rankFields = [
    "ecommerce_enterprises_pct",
    "ecommerce_turnover_pct",
    "web_sales_enterprises_pct",
    "web_sales_foreign_enterprises_pct",
    "web_sales_foreign_share_of_web_sellers_pct",
    "own_site_enterprises_pct",
    "marketplace_enterprises_pct"
  ];

  return rows.map((row) => {
    const ranked = { ...row };
    for (const field of rankFields) {
      ranked[`${field}_rank`] = rankOf(rows, field).rank === null ? null : rankRows(rows, field).find((candidate) => candidate.geo === row.geo)?.rank ?? null;
      ranked[`${field}_rank_total`] = rankRows(rows, field).length;
    }
    return ranked;
  });
}

function makeSectorRows({ eseln2, evaln2 }) {
  return SECTORS.map(([nace, sector_short]) => {
    const row = { nace, sector_short };
    for (const [field, indicator] of Object.entries(ENTERPRISE_INDICATORS)) {
      row[field] = round(jsonStatValue(eseln2.data, { geo: "ES", nace_r2: nace, indic_is: indicator, unit: "PC_ENT" }));
    }
    for (const [field, indicator] of Object.entries(TURNOVER_INDICATORS)) {
      row[field] = round(jsonStatValue(evaln2.data, { geo: "ES", nace_r2: nace, indic_is: indicator, unit: "PC_TURN" }));
    }
    row.web_share_of_ecommerce_turnover_pct = round(jsonStatValue(evaln2.data, { geo: "ES", nace_r2: nace, indic_is: "E_AWSVAL", unit: "PC_ETURN" }));
    row.edi_share_of_ecommerce_turnover_pct = round(jsonStatValue(evaln2.data, { geo: "ES", nace_r2: nace, indic_is: "E_AXSVAL", unit: "PC_ETURN" }));
    return row;
  });
}

function makeObstacleRows({ wsobs }) {
  const geos = ["ES", "DE", "PT", "NL", "PL", "AT"];
  return geos.map((geo) => {
    const row = { geo, country: COUNTRY_NAMES[geo], size_emp: "GE10" };
    for (const [field, indicator] of Object.entries(OBSTACLE_INDICATORS)) {
      row[field] = round(jsonStatValue(wsobs.data, { geo, size_emp: "GE10", indic_is: indicator, unit: "PC_ENT_AWSEU" }));
    }
    return row;
  });
}

function buildSummary(countryRowsBySize, rankingRows, obstacleRows, sectorRows) {
  const ge10Rows = countryRowsBySize.GE10;
  const spain = ge10Rows.find((row) => row.geo === "ES");
  const eu = ge10Rows.find((row) => row.geo === "EU27_2020");
  const spainSmall = countryRowsBySize["10-49"].find((row) => row.geo === "ES");
  const euSmall = countryRowsBySize["10-49"].find((row) => row.geo === "EU27_2020");
  const spainLarge = countryRowsBySize.GE250.find((row) => row.geo === "ES");
  const euLarge = countryRowsBySize.GE250.find((row) => row.geo === "EU27_2020");
  const travel = sectorRows.find((row) => row.nace === "N79");
  const accommodation = sectorRows.find((row) => row.nace === "I55");
  const manufacturing = sectorRows.find((row) => row.nace === "C");
  const retail = sectorRows.find((row) => row.nace === "G47");
  const spainObstacles = obstacleRows.find((row) => row.geo === "ES");
  const germanyObstacles = obstacleRows.find((row) => row.geo === "DE");

  return {
    title: "España tiene más empresas vendiendo online que la UE, pero el ecommerce pesa menos en su facturación",
    slug: SLUG,
    folder: FOLDER,
    retrievedAt: RETRIEVED_AT,
    observationTime: OBSERVATION_TIME,
    spain,
    eu,
    spainSmall,
    euSmall,
    spainLarge,
    euLarge,
    travel,
    accommodation,
    manufacturing,
    retail,
    spainObstacles,
    germanyObstacles,
    gaps: {
      ecommerce_enterprises_pct: diff(spain.ecommerce_enterprises_pct, eu.ecommerce_enterprises_pct),
      ecommerce_turnover_pct: diff(spain.ecommerce_turnover_pct, eu.ecommerce_turnover_pct),
      web_sales_enterprises_pct: diff(spain.web_sales_enterprises_pct, eu.web_sales_enterprises_pct),
      edi_sales_enterprises_pct: diff(spain.edi_sales_enterprises_pct, eu.edi_sales_enterprises_pct),
      own_site_share_of_web_sellers_pct: diff(spain.own_site_share_of_web_sellers_pct, eu.own_site_share_of_web_sellers_pct),
      marketplace_share_of_web_sellers_pct: diff(spain.marketplace_share_of_web_sellers_pct, eu.marketplace_share_of_web_sellers_pct),
      web_sales_foreign_share_of_web_sellers_pct: diff(spain.web_sales_foreign_share_of_web_sellers_pct, eu.web_sales_foreign_share_of_web_sellers_pct),
      small_turnover_pct: diff(spainSmall.ecommerce_turnover_pct, euSmall.ecommerce_turnover_pct),
      large_turnover_pct: diff(spainLarge.ecommerce_turnover_pct, euLarge.ecommerce_turnover_pct)
    },
    ranks: {
      ecommerce_enterprises_pct: rankOf(rankingRows, "ecommerce_enterprises_pct"),
      ecommerce_turnover_pct: rankOf(rankingRows, "ecommerce_turnover_pct"),
      web_sales_enterprises_pct: rankOf(rankingRows, "web_sales_enterprises_pct"),
      web_sales_foreign_enterprises_pct: rankOf(rankingRows, "web_sales_foreign_enterprises_pct"),
      web_sales_foreign_share_of_web_sellers_pct: rankOf(rankingRows, "web_sales_foreign_share_of_web_sellers_pct"),
      own_site_enterprises_pct: rankOf(rankingRows, "own_site_enterprises_pct"),
      marketplace_enterprises_pct: rankOf(rankingRows, "marketplace_enterprises_pct")
    }
  };
}

function paragraph(value) {
  return `<p>${escapeHtml(value)}</p>`;
}

function list(items) {
  return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
}

function table(headers, rows) {
  return `<table><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${rows
    .map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
    .join("")}</tbody></table>`;
}

function htmlDoc({ title, subtitle, body, label }) {
  return `<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8" />
  <title>${escapeHtml(title)}</title>
  <style>
    body { font-family: Arial, Helvetica, sans-serif; color: #182230; margin: 42px; line-height: 1.42; }
    h1 { font-size: 31px; line-height: 1.08; margin: 0 0 12px; }
    h2 { font-size: 20px; margin: 28px 0 10px; color: #067647; }
    h3 { font-size: 16px; margin: 22px 0 8px; color: #344054; }
    p { font-size: 11.5pt; margin: 0 0 9px; }
    ul { margin-top: 6px; }
    li { margin: 0 0 7px; }
    table { border-collapse: collapse; width: 100%; margin: 12px 0 18px; font-size: 9.3pt; }
    th { background: #067647; color: white; text-align: left; padding: 7px; }
    td { border: 1px solid #D0D5DD; padding: 7px; vertical-align: top; }
    .kicker { color: #067647; font-size: 9.5pt; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
    .subtitle { color: #667085; font-size: 13.5pt; margin-bottom: 14px; }
    .meta { color: #067647; font-size: 10pt; font-weight: 700; margin-bottom: 20px; }
    .chart { width: 100%; max-width: 720px; display: block; margin: 18px auto 6px; }
    .caption { text-align: center; color: #667085; font-style: italic; font-size: 9pt; margin-bottom: 14px; }
    .quote { border-left: 4px solid #12B76A; background: #EAFBF3; padding: 12px 16px; font-style: italic; }
    .small { color: #667085; font-size: 9pt; }
  </style>
</head>
<body>
  <p class="kicker">${escapeHtml(label)}</p>
  <h1>${escapeHtml(title)}</h1>
  <p class="subtitle">${escapeHtml(subtitle)}</p>
  <p class="meta">David Trotonda, TienRank | Valencia | Cierre: 3 de agosto de 2026</p>
  ${body}
</body>
</html>`;
}

function markdownTable(headers, rows) {
  return [
    `| ${headers.join(" | ")} |`,
    `| ${headers.map(() => "---").join(" | ")} |`,
    ...rows.map((row) => `| ${row.join(" | ")} |`)
  ].join("\n");
}

function buildDocuments(summary, countryRowsBySize) {
  const title = summary.title;
  const subtitle = `Eurostat sitúa a España en el puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de la UE-27 por empresas con ventas ecommerce (${pct(summary.spain.ecommerce_enterprises_pct)}), pero en el ${summary.ranks.ecommerce_turnover_pct.rank} por peso del ecommerce en la facturación (${pct(summary.spain.ecommerce_turnover_pct)}).`;
  const findings = [
    `El ${pct(summary.spain.ecommerce_enterprises_pct)} de las empresas españolas de 10 o más empleados tuvo ventas ecommerce, ${pp(summary.gaps.ecommerce_enterprises_pct)} por encima de la UE-27.`,
    `El ecommerce aportó el ${pct(summary.spain.ecommerce_turnover_pct)} de la facturación empresarial española, ${pp(summary.gaps.ecommerce_turnover_pct)} por debajo de la UE-27.`,
    `España ocupa el puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de ${summary.ranks.ecommerce_enterprises_pct.total} en adopción empresarial ecommerce, pero el ${summary.ranks.ecommerce_turnover_pct.rank} de ${summary.ranks.ecommerce_turnover_pct.total} en peso sobre facturación.`,
    `La pequeña empresa española de 10 a 49 empleados supera a la UE en adopción (${pct(summary.spainSmall.ecommerce_enterprises_pct)} frente a ${pct(summary.euSmall.ecommerce_enterprises_pct)}), pero factura menos por ecommerce (${pct(summary.spainSmall.ecommerce_turnover_pct)} frente a ${pct(summary.euSmall.ecommerce_turnover_pct)}).`,
    `Entre las empresas españolas con ventas web, el ${pct(summary.spain.own_site_share_of_web_sellers_pct)} vende por web o app propia, frente al ${pct(summary.eu.own_site_share_of_web_sellers_pct)} europeo; en marketplaces, España queda en ${pct(summary.spain.marketplace_share_of_web_sellers_pct)} frente al ${pct(summary.eu.marketplace_share_of_web_sellers_pct)}.`,
    `Solo el ${pct(summary.spain.web_sales_foreign_share_of_web_sellers_pct)} de las empresas españolas con ventas web vende al extranjero, ${pp(summary.gaps.web_sales_foreign_share_of_web_sellers_pct)} por debajo de la UE-27.`,
    `En España, alojamiento (${pct(summary.accommodation.ecommerce_turnover_pct)}) y agencias de viaje y reservas (${pct(summary.travel.ecommerce_turnover_pct)}) muestran intensidades de facturación ecommerce mucho mayores que el comercio minorista (${pct(summary.retail.ecommerce_turnover_pct)}).`
  ];
  const executiveSummary = [
    `España no aparece rezagada en adopción empresarial del ecommerce. Según los datos Eurostat 2025, el ${pct(summary.spain.ecommerce_enterprises_pct)} de las empresas españolas de 10 o más empleados realizó ventas por comercio electrónico, frente al ${pct(summary.eu.ecommerce_enterprises_pct)} de la UE-27. En términos de presencia comercial digital, España está por encima de la media europea y se sitúa en la novena posición descriptiva entre los 27 Estados miembros.`,
    `La fotografía cambia cuando se mira el peso económico. Las ventas por comercio electrónico representan el ${pct(summary.spain.ecommerce_turnover_pct)} de la facturación empresarial española, por debajo del ${pct(summary.eu.ecommerce_turnover_pct)} de la UE-27. España baja al puesto ${summary.ranks.ecommerce_turnover_pct.rank} de ${summary.ranks.ecommerce_turnover_pct.total} en la clasificación europea de intensidad de facturación ecommerce. La brecha no es de acceso al canal, sino de conversión de ese canal en volumen.`,
    `El análisis de TienRank identifica tres focos: tamaño de empresa, canal y alcance internacional. La empresa pequeña española vende online más que la media europea, pero obtiene menos peso de facturación; las empresas con ventas web dependen más de web propia y menos de marketplace; y la venta web al extranjero está por debajo de la media de la UE. El resultado no mide ventas minoristas al consumidor ni operaciones con tarjeta: mide empresas, facturación y canales declarados en la encuesta armonizada de TIC y comercio electrónico.`
  ];
  const sections = [
    {
      title: "La adopción empresarial supera a la media, pero el volumen no acompaña",
      paragraphs: [
        `El contraste principal es claro: España supera a la UE-27 en porcentaje de empresas con ventas ecommerce, con ${pct(summary.spain.ecommerce_enterprises_pct)} frente a ${pct(summary.eu.ecommerce_enterprises_pct)}, pero queda por debajo en peso de facturación, con ${pct(summary.spain.ecommerce_turnover_pct)} frente a ${pct(summary.eu.ecommerce_turnover_pct)}. La distancia convierte un dato positivo de adopción en una pregunta económica: cuántas empresas venden algo online y cuántas han convertido ese canal en una parte material de su negocio.`,
        `La lectura de ranking refuerza el matiz. España ocupa el puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de la UE-27 en adopción empresarial ecommerce, pero el puesto ${summary.ranks.ecommerce_turnover_pct.rank} en facturación ecommerce sobre el total. El estudio no afirma que una posición cause la otra. Señala una asimetría descriptiva: hay más empresas dentro del canal que volumen relativo capturado por ese canal.`
      ],
      chart: ["01-adopcion-vs-facturacion-ue.png", "Empresas con ventas ecommerce y peso de la facturación ecommerce en países seleccionados de la UE-27."]
    },
    {
      title: "La pequeña empresa española es el punto donde más se ve la tensión",
      paragraphs: [
        `En las empresas de 10 a 49 empleados, España registra ${pct(summary.spainSmall.ecommerce_enterprises_pct)} de empresas con ventas ecommerce, frente al ${pct(summary.euSmall.ecommerce_enterprises_pct)} europeo. Sin embargo, el peso de esas ventas en la facturación queda en ${pct(summary.spainSmall.ecommerce_turnover_pct)}, frente al ${pct(summary.euSmall.ecommerce_turnover_pct)} de la UE-27. La brecha de facturación es de ${pp(summary.gaps.small_turnover_pct)}.`,
        `En las empresas de 250 o más empleados ocurre lo contrario: España supera a la UE tanto en adopción (${pct(summary.spainLarge.ecommerce_enterprises_pct)} frente a ${pct(summary.euLarge.ecommerce_enterprises_pct)}) como en peso de facturación (${pct(summary.spainLarge.ecommerce_turnover_pct)} frente a ${pct(summary.euLarge.ecommerce_turnover_pct)}). La conclusión práctica es que no basta con hablar de empresas españolas en agregado: la intensidad del ecommerce cambia mucho según escala.`
      ],
      chart: ["02-brecha-tamano-empresa.png", "Diferencia entre España y UE-27 por tamaño de empresa, adopción y facturación ecommerce."]
    },
    {
      title: "Web propia fuerte, marketplace menos extendido y menos venta web exterior",
      paragraphs: [
        `Entre las empresas españolas con ventas web, el ${pct(summary.spain.own_site_share_of_web_sellers_pct)} vende mediante web o app propia, por encima del ${pct(summary.eu.own_site_share_of_web_sellers_pct)} de la UE-27. El uso de marketplaces queda por debajo: ${pct(summary.spain.marketplace_share_of_web_sellers_pct)} en España frente a ${pct(summary.eu.marketplace_share_of_web_sellers_pct)} en la UE-27.`,
        `El dato internacional es más exigente. Solo el ${pct(summary.spain.web_sales_foreign_share_of_web_sellers_pct)} de las empresas españolas con ventas web vende a clientes de otros países, frente al ${pct(summary.eu.web_sales_foreign_share_of_web_sellers_pct)} de la UE-27. Austria alcanza el ${pct(60.9)} en esta métrica y Países Bajos, el ${pct(47.71)}. España no está muy lejos de la media en empresas que venden por web al extranjero sobre el total de empresas, pero sí queda más baja cuando el denominador son solo las empresas que ya venden por web.`
      ],
      chart: ["04-canales-web-marketplace-exterior.png", "Uso de web propia, marketplaces y venta exterior entre empresas con ventas web."]
    },
    {
      title: "El ecommerce empresarial español no es solo retail",
      paragraphs: [
        `Cuando se analiza por actividad NACE, el comercio minorista español registra un peso ecommerce del ${pct(summary.retail.ecommerce_turnover_pct)} de su facturación. Es relevante para consumo, pero no es el máximo sectorial. Alojamiento alcanza el ${pct(summary.accommodation.ecommerce_turnover_pct)} y agencias de viaje y reservas, el ${pct(summary.travel.ecommerce_turnover_pct)}.`,
        `La composición por canal también cambia. En manufacturas, el ecommerce representa el ${pct(summary.manufacturing.ecommerce_turnover_pct)} de la facturación, pero el ${pct(summary.manufacturing.edi_share_of_ecommerce_turnover_pct)} de esa facturación ecommerce procede de EDI. En comercio minorista, la web pesa más: el ${pct(summary.retail.web_share_of_ecommerce_turnover_pct)} de la facturación ecommerce del sector procede de web, app o marketplace. Por eso conviene separar ecommerce estadístico, venta web y experiencia B2C visible.`
      ],
      chart: ["05-sectores-ecommerce-espana.png", "Peso de la facturación ecommerce por sectores españoles y división entre web y EDI."]
    },
    {
      title: "Las barreras transfronterizas no se pueden leer como causa única",
      paragraphs: [
        `Eurostat también pregunta por dificultades de venta web a otros países de la UE. En España, el ${pct(summary.spainObstacles.any_difficulty_pct_of_eu_web_sellers)} de las empresas que venden por web a otros países de la UE declara alguna dificultad entre costes de entrega o devolución, reclamaciones, etiquetado, idiomas, restricciones de socios o IVA. El coste de entrega y devolución aparece en el ${pct(summary.spainObstacles.delivery_returns_cost_pct_of_eu_web_sellers)}.`,
        `La cifra española es inferior a la alemana en dificultades totales (${pct(summary.germanyObstacles.any_difficulty_pct_of_eu_web_sellers)}), lo que impide usar las barreras declaradas como explicación causal simple de la menor venta exterior. El dato útil es operativo: vender fuera exige logística, fiscalidad, atención postventa e información de producto más robustas, pero el peso final depende también de escala, catálogo, marca, demanda y estructura sectorial.`
      ]
    },
    {
      title: "Conclusión: el siguiente salto no es abrir tienda online, es hacerla pesar",
      paragraphs: [
        `El ecommerce empresarial español ya tiene una base amplia. La cuestión periodísticamente relevante no es si las empresas han entrado en el canal, sino por qué esa presencia pesa menos en la facturación que en otros mercados europeos y por qué la venta web internacional queda por debajo cuando se mira solo a quienes ya venden por web.`,
        `La lectura de TienRank es que la madurez ecommerce debe medirse con tres capas: adopción, intensidad económica y capacidad transfronteriza. Para empresas y asociaciones, el trabajo está en convertir escaparates digitales en ingresos recurrentes, reducir fricción logística y fiscal, profesionalizar datos de catálogo y medir mejor la diferencia entre venta web visible, marketplace, EDI y comercio internacional.`
      ]
    }
  ];
  const methodology = [
    "Fecha de cierre del análisis: 3 de agosto de 2026.",
    "Fuente principal de cálculo: Eurostat Digital Economy and Society, datasets isoc_ec_esels, isoc_ec_evals, isoc_ec_eseln2, isoc_ec_evaln2 e isoc_ec_wsobs, descargados por API el 3 de agosto de 2026.",
    "Universo Eurostat: empresas con 10 o más personas empleadas, actividades C10-S951_X_K, es decir, todas las actividades salvo agricultura, silvicultura y pesca, minería y canteras, y sector financiero, salvo cuando se indica un sector NACE concreto.",
    "El campo time=2025 de Eurostat corresponde a la encuesta TIC de empresas 2025; las variables de comercio electrónico se refieren al último año natural. En la nota nacional del INE, esa referencia se publica como año 2024 y primer trimestre de 2025.",
    "Las posiciones de España se calculan ordenando los 27 Estados miembros con dato disponible en cada indicador. Si un país no tiene dato en una variable, el denominador del ranking se reduce y se informa.",
    "Las cuotas sobre empresas con ventas web usan la unidad Eurostat PC_ENT_AWSELL; no son porcentajes sobre todas las empresas.",
    "Las cuotas de facturación por web, EDI, web propia, marketplace y destino se expresan como porcentaje de la facturación total empresarial, salvo cuando la tabla indica porcentaje sobre facturación ecommerce o sobre ventas web.",
    "La definición OCDE 2025 se usa para separar el pedido realizado por redes digitales del pago y la entrega, y para no confundir ecommerce estadístico con pago online, retail B2C o tráfico web."
  ];
  const limitations = [
    "La investigación es descriptiva y no establece causalidad entre tamaño de empresa, canal, venta exterior y peso de facturación.",
    "Eurostat e INE miden empresas y facturación declarada; no miden pedidos individuales, visitas web, conversión de tiendas, ventas con tarjeta ni cuota de marketplaces concretos.",
    "Las ventas EDI forman parte del ecommerce estadístico aunque no equivalen a una experiencia web de compra para consumidores.",
    "Algunos indicadores transfronterizos u obstáculos no están disponibles para todos los países; los rankings y comparaciones se calculan solo con países con dato.",
    "Los sectores NACE tienen tamaños económicos distintos; un porcentaje sectorial alto no implica mayor volumen absoluto que otro sector.",
    "La comparabilidad entre países depende de la encuesta armonizada, pero puede verse afectada por estructura sectorial, tamaño empresarial y composición de ventas.",
    "La nota del INE aporta contraste nacional y metodología, pero las comparaciones europeas de esta investigación se calculan sobre la API de Eurostat para conservar un marco común."
  ];
  const mediaAngles = [
    "Pymes: por qué la pequeña empresa española vende online más que la media europea, pero convierte menos facturación.",
    "Internacionalización: la distancia de 10,5 puntos entre España y la UE en venta web exterior entre empresas que ya venden por web.",
    "Canales: web propia fuerte, marketplace menos extendido y EDI todavía decisivo en manufacturas.",
    "Turismo y reservas: alojamiento y agencias de viaje muestran intensidades ecommerce muy superiores al retail.",
    "Política digital: medir madurez ecommerce exige separar adopción, facturación y alcance internacional."
  ];
  const tables = [
    {
      caption: "Adopción y facturación ecommerce en países seleccionados",
      headers: ["País", "Empresas ecommerce", "Facturación ecommerce", "Puesto adopción", "Puesto facturación"],
      rows: [
        ["UE-27", pct(summary.eu.ecommerce_enterprises_pct), pct(summary.eu.ecommerce_turnover_pct), "Referencia", "Referencia"],
        ["España", pct(summary.spain.ecommerce_enterprises_pct), pct(summary.spain.ecommerce_turnover_pct), `${summary.ranks.ecommerce_enterprises_pct.rank}/${summary.ranks.ecommerce_enterprises_pct.total}`, `${summary.ranks.ecommerce_turnover_pct.rank}/${summary.ranks.ecommerce_turnover_pct.total}`],
        ["Alemania", "23,1%", "19,1%", "17/27", "14/27"],
        ["Francia", "19,4%", "14,3%", "23/27", "24/27"],
        ["Italia", "20,1%", "15,7%", "22/27", "22/27"],
        ["Portugal", "21,8%", "20,6%", "18/27", "11/27"],
        ["Países Bajos", "28,6%", "21,7%", "10/27", "10/27"],
        ["Irlanda", "36,6%", "38,3%", "4/27", "1/27"]
      ],
      note: "Empresas de 10 o más personas empleadas. Puestos descriptivos calculados por TienRank sobre países con dato disponible."
    },
    {
      caption: "España frente a la UE-27 por tamaño de empresa",
      headers: ["Tamaño", "Adopción España", "Adopción UE-27", "Facturación España", "Facturación UE-27"],
      rows: [
        ["10-49", pct(summary.spainSmall.ecommerce_enterprises_pct), pct(summary.euSmall.ecommerce_enterprises_pct), pct(summary.spainSmall.ecommerce_turnover_pct), pct(summary.euSmall.ecommerce_turnover_pct)],
        ["50-249", pct(countryRowsBySize["50-249"].find((row) => row.geo === "ES").ecommerce_enterprises_pct), pct(countryRowsBySize["50-249"].find((row) => row.geo === "EU27_2020").ecommerce_enterprises_pct), pct(countryRowsBySize["50-249"].find((row) => row.geo === "ES").ecommerce_turnover_pct), pct(countryRowsBySize["50-249"].find((row) => row.geo === "EU27_2020").ecommerce_turnover_pct)],
        ["250 o más", pct(summary.spainLarge.ecommerce_enterprises_pct), pct(summary.euLarge.ecommerce_enterprises_pct), pct(summary.spainLarge.ecommerce_turnover_pct), pct(summary.euLarge.ecommerce_turnover_pct)],
        ["Total >=10", pct(summary.spain.ecommerce_enterprises_pct), pct(summary.eu.ecommerce_enterprises_pct), pct(summary.spain.ecommerce_turnover_pct), pct(summary.eu.ecommerce_turnover_pct)]
      ],
      note: "La facturación ecommerce se expresa como porcentaje de la facturación total de cada grupo de empresas."
    },
    {
      caption: "Canales web entre empresas que ya venden por web",
      headers: ["Indicador", "España", "UE-27", "Diferencia"],
      rows: [
        ["Venta por web/app propia", pct(summary.spain.own_site_share_of_web_sellers_pct), pct(summary.eu.own_site_share_of_web_sellers_pct), pp(summary.gaps.own_site_share_of_web_sellers_pct)],
        ["Venta por marketplace", pct(summary.spain.marketplace_share_of_web_sellers_pct), pct(summary.eu.marketplace_share_of_web_sellers_pct), pp(summary.gaps.marketplace_share_of_web_sellers_pct)],
        ["Venta web al extranjero", pct(summary.spain.web_sales_foreign_share_of_web_sellers_pct), pct(summary.eu.web_sales_foreign_share_of_web_sellers_pct), pp(summary.gaps.web_sales_foreign_share_of_web_sellers_pct)]
      ],
      note: "Porcentaje sobre empresas con ventas web. Las opciones pueden solaparse."
    },
    {
      caption: "Sectores españoles seleccionados",
      headers: ["Sector", "Facturación ecommerce", "Web sobre ecommerce", "EDI sobre ecommerce"],
      rows: [
        ["Alojamiento", pct(summary.accommodation.ecommerce_turnover_pct), pct(summary.accommodation.web_share_of_ecommerce_turnover_pct), pct(summary.accommodation.edi_share_of_ecommerce_turnover_pct)],
        ["Agencias de viaje y reservas", pct(summary.travel.ecommerce_turnover_pct), pct(summary.travel.web_share_of_ecommerce_turnover_pct), pct(summary.travel.edi_share_of_ecommerce_turnover_pct)],
        ["Manufacturas", pct(summary.manufacturing.ecommerce_turnover_pct), pct(summary.manufacturing.web_share_of_ecommerce_turnover_pct), pct(summary.manufacturing.edi_share_of_ecommerce_turnover_pct)],
        ["Comercio minorista", pct(summary.retail.ecommerce_turnover_pct), pct(summary.retail.web_share_of_ecommerce_turnover_pct), pct(summary.retail.edi_share_of_ecommerce_turnover_pct)]
      ],
      note: "NACE Rev. 2. Porcentaje de facturación sectorial y reparto de esa facturación ecommerce."
    }
  ];

  const sourceList = [
    "Eurostat Digital Economy and Society, isoc_ec_esels: empresas con ventas ecommerce por tamaño.",
    "Eurostat Digital Economy and Society, isoc_ec_evals: valor de ventas ecommerce por tamaño.",
    "Eurostat Digital Economy and Society, isoc_ec_eseln2/isoc_ec_evaln2: empresas y facturación ecommerce por actividad NACE.",
    "Eurostat Digital Economy and Society, isoc_ec_wsobs: obstáculos para venta web a otros países de la UE.",
    "INE, Encuesta sobre el uso de TIC y del comercio electrónico en las empresas. Año 2024 - Primer trimestre 2025.",
    "OCDE, The 2025 OECD Definition of E-Commerce and Guidelines for Interpretation."
  ];

  const citation = "TienRank (2026). España tiene más empresas vendiendo online que la UE, pero el ecommerce pesa menos en su facturación. Análisis de TienRank a partir de Eurostat, INE y OCDE.";
  const quote = "España no tiene un problema de entrada al ecommerce empresarial: tiene un reto de intensidad. Muchas empresas ya aceptan pedidos online, pero el salto relevante es convertir ese canal en facturación recurrente, internacional y medible sin confundir web, marketplace y EDI.";
  const quoteNote = "Propuesta editorial pendiente de aprobación. No debe enviarse ni reproducirse como declaración literal hasta que David Trotonda apruebe exactamente este texto.";

  const md = [
    `# ${title}`,
    "",
    `> ${subtitle}`,
    "",
    "**Autor:** David Trotonda, CEO de TienRank  ",
    "**Fecha:** 3 de agosto de 2026  ",
    "**Periodo observado:** Eurostat time=2025; ventas ecommerce referidas al último año natural, contrastadas con INE año 2024 - primer trimestre 2025.",
    "",
    "## Resumen ejecutivo",
    ...executiveSummary.map((item) => `\n${item}`),
    "",
    "## Hallazgos principales",
    ...findings.map((item) => `- ${item}`),
    "",
    ...sections.flatMap((section) => [
      `## ${section.title}`,
      ...section.paragraphs.map((item) => `\n${item}`),
      ...(section.chart ? [`\n![${section.chart[1]}](graficos/${section.chart[0]})`, `_${section.chart[1]}_`] : []),
      ""
    ]),
    "## Tablas",
    ...tables.flatMap((tableData) => [`### ${tableData.caption}`, markdownTable(tableData.headers, tableData.rows), tableData.note, ""]),
    "## Material para medios",
    `> ${quote}`,
    "",
    `David Trotonda, CEO de TienRank. ${quoteNote}`,
    "",
    "### Ángulos editoriales",
    ...mediaAngles.map((item) => `- ${item}`),
    "",
    "### Sobre TienRank",
    "TienRank es una plataforma de investigación y descubrimiento de tiendas online desarrollada en Valencia. El equipo combina experiencia en desarrollo de software, estrategia y crecimiento de ecommerce. David Trotonda, CEO de TienRank, está disponible para entrevistas sobre los resultados, su impacto para las tiendas y las mejoras de experiencia que sugieren los datos.",
    "",
    "### Contacto de prensa",
    "info@tienrank.com",
    "",
    "## Metodología",
    ...methodology.map((item) => `- ${item}`),
    "",
    "## Limitaciones",
    ...limitations.map((item) => `- ${item}`),
    "",
    "## Fuentes principales",
    ...sourceList.map((item) => `- ${item}`),
    "",
    "## Cómo citar",
    citation,
    ""
  ].join("\n");

  const htmlBody = [
    `<img class="chart" src="${path.resolve(CHART_DIR, "03-resumen-empresas-ecommerce.png")}" alt="Resumen visual del estudio" />`,
    `<p class="caption">Resumen del contraste entre adopción, facturación e internacionalización web.</p>`,
    "<h2>Resumen ejecutivo</h2>",
    executiveSummary.map(paragraph).join(""),
    "<h2>Hallazgos principales</h2>",
    list(findings),
    ...sections.flatMap((section) => [
      `<h2>${escapeHtml(section.title)}</h2>`,
      section.paragraphs.map(paragraph).join(""),
      section.chart ? `<img class="chart" src="${path.resolve(CHART_DIR, section.chart[0])}" alt="${escapeHtml(section.chart[1])}" /><p class="caption">${escapeHtml(section.chart[1])}</p>` : ""
    ]),
    "<h2>Tablas</h2>",
    ...tables.flatMap((tableData) => [
      `<h3>${escapeHtml(tableData.caption)}</h3>`,
      table(tableData.headers, tableData.rows),
      `<p class="small">${escapeHtml(tableData.note)}</p>`
    ]),
    "<h2>Material para medios</h2>",
    `<p class="quote">${escapeHtml(quote)}<br /><br />David Trotonda, CEO de TienRank. ${escapeHtml(quoteNote)}</p>`,
    "<h3>Ángulos editoriales</h3>",
    list(mediaAngles),
    "<h3>Sobre TienRank</h3>",
    paragraph("TienRank es una plataforma de investigación y descubrimiento de tiendas online desarrollada en Valencia. El equipo combina experiencia en desarrollo de software, estrategia y crecimiento de ecommerce. David Trotonda, CEO de TienRank, está disponible para entrevistas sobre los resultados, su impacto para las tiendas y las mejoras de experiencia que sugieren los datos."),
    "<h3>Contacto de prensa</h3>",
    paragraph("info@tienrank.com"),
    "<h2>Metodología</h2>",
    list(methodology),
    "<h2>Limitaciones</h2>",
    list(limitations),
    "<h2>Fuentes principales</h2>",
    list(sourceList),
    "<h2>Cómo citar</h2>",
    paragraph(citation)
  ].join("\n");

  const pressTitle = "España tiene más empresas vendiendo online que la UE, pero el ecommerce pesa menos en su facturación";
  const pressMd = [
    `# ${pressTitle}`,
    "",
    "Valencia, 3 de agosto de 2026. TienRank publica una investigación sobre la intensidad real del ecommerce en las empresas españolas a partir de datos de Eurostat, INE y OCDE.",
    "",
    `El análisis muestra que el ${pct(summary.spain.ecommerce_enterprises_pct)} de las empresas españolas de 10 o más empleados realizó ventas ecommerce, por encima del ${pct(summary.eu.ecommerce_enterprises_pct)} de la UE-27. Sin embargo, esas ventas representan el ${pct(summary.spain.ecommerce_turnover_pct)} de la facturación, por debajo del ${pct(summary.eu.ecommerce_turnover_pct)} europeo.`,
    "",
    `España ocupa el puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de la UE-27 en adopción, pero cae al ${summary.ranks.ecommerce_turnover_pct.rank} en peso de facturación. La pequeña empresa concentra la señal más útil: vende online más que la media europea, pero el ecommerce pesa ${pp(Math.abs(summary.gaps.small_turnover_pct), 1, false)} menos en su facturación.`,
    "",
    "La investigación completa incluye metodología, limitaciones, gráficos, CSV y JSON descargables. La cita propuesta de David Trotonda queda pendiente de aprobación y no se distribuye como declaración literal.",
    "",
    "URL canónica: https://tienrank.com/investigaciones/empresas-ecommerce-facturacion-espana-2026",
    "Contacto: info@tienrank.com"
  ].join("\n");

  const pressHtmlBody = [
    paragraph("Valencia, 3 de agosto de 2026. TienRank publica una investigación sobre la intensidad real del ecommerce en las empresas españolas a partir de datos de Eurostat, INE y OCDE."),
    paragraph(`El análisis muestra que el ${pct(summary.spain.ecommerce_enterprises_pct)} de las empresas españolas de 10 o más empleados realizó ventas ecommerce, por encima del ${pct(summary.eu.ecommerce_enterprises_pct)} de la UE-27. Sin embargo, esas ventas representan el ${pct(summary.spain.ecommerce_turnover_pct)} de la facturación, por debajo del ${pct(summary.eu.ecommerce_turnover_pct)} europeo.`),
    paragraph(`España ocupa el puesto ${summary.ranks.ecommerce_enterprises_pct.rank} de la UE-27 en adopción, pero cae al ${summary.ranks.ecommerce_turnover_pct.rank} en peso de facturación. La pequeña empresa concentra la señal más útil: vende online más que la media europea, pero el ecommerce pesa ${pp(Math.abs(summary.gaps.small_turnover_pct), 1, false)} menos en su facturación.`),
    paragraph("La investigación completa incluye metodología, limitaciones, gráficos, CSV y JSON descargables. La cita propuesta de David Trotonda queda pendiente de aprobación y no se distribuye como declaración literal."),
    paragraph("URL canónica: https://tienrank.com/investigaciones/empresas-ecommerce-facturacion-espana-2026"),
    paragraph("Contacto: info@tienrank.com")
  ].join("");

  return {
    title,
    subtitle,
    findings,
    executiveSummary,
    sections,
    methodology,
    limitations,
    mediaAngles,
    tables,
    quote,
    quoteNote,
    citation,
    dossierMarkdown: md,
    dossierHtml: htmlDoc({ title, subtitle, body: htmlBody, label: "Investigación de ecommerce" }),
    pressMarkdown: pressMd,
    pressHtml: htmlDoc({ title: pressTitle, subtitle: "Nota de prensa", body: pressHtmlBody, label: "Nota de prensa" })
  };
}

async function convertHtmlToPdf(htmlPath) {
  const result = spawnSync("soffice", ["--headless", "--convert-to", "pdf", "--outdir", path.dirname(htmlPath), htmlPath], {
    encoding: "utf8"
  });
  if (result.status !== 0) {
    throw new Error(`LibreOffice PDF conversion failed for ${htmlPath}: ${result.stderr || result.stdout}`);
  }
  return htmlPath.replace(/\.html$/u, ".pdf");
}

async function buildZip(files) {
  const zipPath = path.join(RESEARCH_DIR, "kit-prensa-empresas-ecommerce-eurostat-2026.zip");
  const result = spawnSync("zip", ["-j", "-q", zipPath, ...files], { encoding: "utf8" });
  if (result.status !== 0) throw new Error(`zip failed: ${result.stderr || result.stdout}`);
  await copyFile(zipPath, path.join(PUBLIC_DIR, path.basename(zipPath)));
}

async function main() {
  await mkdir(DATA_DIR, { recursive: true });
  await mkdir(SOURCE_DIR, { recursive: true });
  await mkdir(CHART_DIR, { recursive: true });
  await mkdir(PUBLIC_DIR, { recursive: true });

  const [esels, evals, eseln2, evaln2, wsobs] = await Promise.all([
    fetchEurostat("isoc_ec_esels", { time: OBSERVATION_TIME }),
    fetchEurostat("isoc_ec_evals", { time: OBSERVATION_TIME }),
    fetchEurostat("isoc_ec_eseln2", { time: OBSERVATION_TIME, geo: "ES" }),
    fetchEurostat("isoc_ec_evaln2", { time: OBSERVATION_TIME, geo: "ES" }),
    fetchEurostat("isoc_ec_wsobs", { time: OBSERVATION_TIME })
  ]);

  await Promise.all([
    writeFile(path.join(SOURCE_DIR, "eurostat-isoc-ec-esels-2025.raw.json"), JSON.stringify({ url: esels.url, retrievedAt: RETRIEVED_AT, data: esels.data }, null, 2)),
    writeFile(path.join(SOURCE_DIR, "eurostat-isoc-ec-evals-2025.raw.json"), JSON.stringify({ url: evals.url, retrievedAt: RETRIEVED_AT, data: evals.data }, null, 2)),
    writeFile(path.join(SOURCE_DIR, "eurostat-isoc-ec-eseln2-2025-es.raw.json"), JSON.stringify({ url: eseln2.url, retrievedAt: RETRIEVED_AT, data: eseln2.data }, null, 2)),
    writeFile(path.join(SOURCE_DIR, "eurostat-isoc-ec-evaln2-2025-es.raw.json"), JSON.stringify({ url: evaln2.url, retrievedAt: RETRIEVED_AT, data: evaln2.data }, null, 2)),
    writeFile(path.join(SOURCE_DIR, "eurostat-isoc-ec-wsobs-2025.raw.json"), JSON.stringify({ url: wsobs.url, retrievedAt: RETRIEVED_AT, data: wsobs.data }, null, 2))
  ]);

  const countryRowsBySize = makeRows({ esels, evals });
  const countryRows = Object.values(countryRowsBySize).flat();
  const rankingRows = makeEuRankings({ esels, evals });
  const sectorRows = makeSectorRows({ eseln2, evaln2 });
  const obstacleRows = makeObstacleRows({ wsobs });
  const summary = buildSummary(countryRowsBySize, rankingRows, obstacleRows, sectorRows);

  const countryColumns = [
    "geo",
    "country",
    "size_emp",
    "size_label",
    ...Object.keys(ENTERPRISE_INDICATORS),
    ...Object.keys(TURNOVER_INDICATORS),
    "own_site_share_of_web_sellers_pct",
    "marketplace_share_of_web_sellers_pct",
    "web_sales_foreign_share_of_web_sellers_pct",
    "ecommerce_turnover_per_seller_index"
  ];
  const rankingColumns = [
    "geo",
    "country",
    "size_emp",
    "ecommerce_enterprises_pct",
    "ecommerce_enterprises_pct_rank",
    "ecommerce_enterprises_pct_rank_total",
    "ecommerce_turnover_pct",
    "ecommerce_turnover_pct_rank",
    "ecommerce_turnover_pct_rank_total",
    "web_sales_enterprises_pct",
    "web_sales_enterprises_pct_rank",
    "web_sales_enterprises_pct_rank_total",
    "web_sales_foreign_share_of_web_sellers_pct",
    "web_sales_foreign_share_of_web_sellers_pct_rank",
    "web_sales_foreign_share_of_web_sellers_pct_rank_total",
    "own_site_enterprises_pct",
    "own_site_enterprises_pct_rank",
    "own_site_enterprises_pct_rank_total",
    "marketplace_enterprises_pct",
    "marketplace_enterprises_pct_rank",
    "marketplace_enterprises_pct_rank_total"
  ];
  const sectorColumns = [
    "nace",
    "sector_short",
    ...Object.keys(ENTERPRISE_INDICATORS),
    ...Object.keys(TURNOVER_INDICATORS),
    "web_share_of_ecommerce_turnover_pct",
    "edi_share_of_ecommerce_turnover_pct"
  ];
  const obstacleColumns = ["geo", "country", "size_emp", ...Object.keys(OBSTACLE_INDICATORS)];

  const csvFiles = [
    ["empresas-ecommerce-eurostat-2025.csv", toCsv(countryRows, countryColumns)],
    ["ranking-ue27-empresas-ecommerce-2025.csv", toCsv(rankingRows, rankingColumns)],
    ["sectores-espana-ecommerce-2025.csv", toCsv(sectorRows, sectorColumns)],
    ["obstaculos-venta-web-ue-2025.csv", toCsv(obstacleRows, obstacleColumns)]
  ];
  for (const [filename, content] of csvFiles) {
    await writeFile(path.join(DATA_DIR, filename), content);
    await writeFile(path.join(PUBLIC_DIR, filename), content);
  }

  await heroChart(summary);
  await adoptionTurnoverChart(countryRowsBySize.GE10);
  await sizeGapChart(countryRowsBySize);
  await channelChart(summary);
  await sectorChart(sectorRows);

  const docs = buildDocuments(summary, countryRowsBySize);
  await writeFile(path.join(RESEARCH_DIR, "dossier-medios.md"), docs.dossierMarkdown);
  await writeFile(path.join(RESEARCH_DIR, "nota-prensa.md"), docs.pressMarkdown);
  await writeFile(path.join(RESEARCH_DIR, "dossier-medios.html"), docs.dossierHtml);
  await writeFile(path.join(RESEARCH_DIR, "nota-prensa.html"), docs.pressHtml);

  const findingsJson = {
    title: docs.title,
    slug: SLUG,
    retrievedAt: RETRIEVED_AT,
    observationTime: OBSERVATION_TIME,
    eurostatUpdated: {
      isoc_ec_esels: esels.data.updated,
      isoc_ec_evals: evals.data.updated,
      isoc_ec_eseln2: eseln2.data.updated,
      isoc_ec_evaln2: evaln2.data.updated,
      isoc_ec_wsobs: wsobs.data.updated
    },
    canonicalUrl: `https://tienrank.com/investigaciones/${SLUG}`,
    summary,
    findings: docs.findings,
    methodology: docs.methodology,
    limitations: docs.limitations,
    sources: [
      { organization: "Eurostat", label: "E-commerce sales of enterprises by size class of enterprise", url: "https://doi.org/10.2908/ISOC_EC_ESELS" },
      { organization: "Eurostat", label: "Value of e-commerce sales by size class of enterprise", url: "https://doi.org/10.2908/ISOC_EC_EVALS" },
      { organization: "Eurostat", label: "E-commerce sales of enterprises by NACE Rev. 2 activity", url: "https://doi.org/10.2908/ISOC_EC_ESELN2" },
      { organization: "Eurostat", label: "Value of e-commerce sales by NACE Rev. 2 activity", url: "https://doi.org/10.2908/ISOC_EC_EVALN2" },
      { organization: "Eurostat", label: "Obstacles for web sales by size class of enterprise", url: "https://doi.org/10.2908/ISOC_EC_WSOBS" },
      { organization: "INE", label: "Encuesta sobre el uso de TIC y del comercio electrónico en las empresas. Año 2024 - Primer trimestre 2025", url: "https://www.ine.es/dyngs/Prensa/ETICCE20241T2025.htm" },
      { organization: "OCDE", label: "The 2025 OECD Definition of E-Commerce and Guidelines for Interpretation", url: "https://www.oecd.org/en/publications/the-2025-oecd-definition-of-e-commerce-and-guidelines-for-interpretation_2254f1de-en.html" }
    ]
  };
  await writeFile(path.join(DATA_DIR, "hallazgos-y-metodologia.json"), JSON.stringify(findingsJson, null, 2));
  await writeFile(path.join(PUBLIC_DIR, "hallazgos-y-metodologia.json"), JSON.stringify(findingsJson, null, 2));

  const parity = [
    "# Matriz de paridad dossier-web",
    "",
    "- Título, subtítulo, autor, fecha y periodo observado: WEB.",
    "- Resumen ejecutivo completo de tres párrafos: WEB.",
    "- Siete hallazgos principales: WEB.",
    "- Secciones narrativas completas: WEB.",
    "- Cuatro tablas de datos: WEB.",
    "- Cuatro gráficos PNG originales: WEB y DOWNLOAD.",
    "- Metodología, limitaciones, fuentes, cita bibliográfica y contacto: WEB.",
    "- Dossier PDF, nota de prensa PDF, CSV, JSON y ZIP: DOWNLOAD con resumen suficiente en HTML.",
    "- Cita propuesta de David Trotonda: WEB con approvalStatus pending; WITHHELD para distribución literal por falta de aprobación.",
    "",
    "Resultado: paridad dossier-web completa. No hay contenido público del dossier final omitido del HTML por acortamiento."
  ].join("\n");
  await writeFile(path.join(RESEARCH_DIR, "web-parity-checklist.md"), parity);

  const readme = [
    "# Investigación empresas ecommerce Eurostat 2026",
    "",
    `URL canónica prevista: https://tienrank.com/investigaciones/${SLUG}`,
    "Fuentes principales: Eurostat, INE y OCDE.",
    "Script reproducible: scripts/research-empresas-ecommerce-eurostat-2026.mjs",
    "Cierre del análisis: 3 de agosto de 2026.",
    "",
    "La cita propuesta de David Trotonda está pendiente de aprobación y no se usa en outreach como declaración literal."
  ].join("\n");
  await writeFile(path.join(RESEARCH_DIR, "LEEME.md"), readme);

  const emailDraft = [
    "# Criterio de outreach",
    "",
    "No incluir la cita pendiente como declaración literal.",
    "Enlazar la URL canónica y ofrecer PDF/CSV/metodología bajo petición.",
    "Rotar destinatarios: economía digital, pymes, internacionalización, comercio minorista y transformación digital empresarial.",
    "",
    "Asunto base sugerido: Datos Eurostat: España vende online más que la UE, pero factura menos por ecommerce"
  ].join("\n");
  await writeFile(path.join(RESEARCH_DIR, "correo-presentacion-medios.md"), emailDraft);

  const dossierPdf = await convertHtmlToPdf(path.join(RESEARCH_DIR, "dossier-medios.html"));
  const pressPdf = await convertHtmlToPdf(path.join(RESEARCH_DIR, "nota-prensa.html"));
  await copyFile(dossierPdf, path.join(PUBLIC_DIR, "dossier-medios.pdf"));
  await copyFile(pressPdf, path.join(PUBLIC_DIR, "nota-prensa.pdf"));

  await buildZip([
    path.join(RESEARCH_DIR, "dossier-medios.md"),
    path.join(RESEARCH_DIR, "dossier-medios.html"),
    path.join(RESEARCH_DIR, "dossier-medios.pdf"),
    path.join(RESEARCH_DIR, "nota-prensa.md"),
    path.join(RESEARCH_DIR, "nota-prensa.html"),
    path.join(RESEARCH_DIR, "nota-prensa.pdf"),
    path.join(RESEARCH_DIR, "web-parity-checklist.md"),
    path.join(RESEARCH_DIR, "LEEME.md"),
    path.join(SCRIPT_DIR, "research-empresas-ecommerce-eurostat-2026.mjs"),
    ...csvFiles.map(([filename]) => path.join(DATA_DIR, filename)),
    path.join(DATA_DIR, "hallazgos-y-metodologia.json"),
    "03-resumen-empresas-ecommerce.png",
    "01-adopcion-vs-facturacion-ue.png",
    "02-brecha-tamano-empresa.png",
    "04-canales-web-marketplace-exterior.png",
    "05-sectores-ecommerce-espana.png"
  ].map((file) => (file.endsWith(".png") ? path.join(CHART_DIR, file) : file)));

  console.log(JSON.stringify({ slug: SLUG, folder: FOLDER, summary: findingsJson.summary }, null, 2));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
