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

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

const ROOT = process.cwd();
const SLUG = "logistica-devoluciones-ecommerce-espana-2026";
const FOLDER = SLUG;
const CLOSED_AT = "2026-08-10";
const PUBLISHED_AT = "2026-08-10T08:00:00.000Z";
const RESEARCH_DIR = path.join(ROOT, "docs", "investigaciones", FOLDER);
const DATA_DIR = path.join(RESEARCH_DIR, "datos");
const CHART_DIR = path.join(RESEARCH_DIR, "graficos");
const PUBLIC_DIR = path.join(ROOT, "public", "assets", "investigaciones", FOLDER);
const ASSET_PATH = `/assets/investigaciones/${FOLDER}`;

const SOURCES = [
  {
    label: "Análisis del sector postal y del sector de mensajería y paquetería 2025",
    organization: "CNMC",
    url: "https://www.cnmc.es/sites/default/files/6749773.pdf"
  },
  {
    label: "Por primera vez, en 2025 se enviaron más paquetes que cartas en España",
    organization: "CNMC",
    url: "https://www.cnmc.es/prensa/informe-anual-postal-20260805"
  },
  {
    label: "Panel de Hogares: sector postal y paquetería, IV trimestre de 2025",
    organization: "CNMCData",
    url: "https://data.cnmc.es/panel-de-hogares/conjuntos-de-datos/estadisticas-panel-de-hogares"
  },
  {
    label: "Panel de hogares CNMC: comprar online sin pagar envío, la nueva normalidad",
    organization: "Blog CNMC",
    url: "https://blog.cnmc.es/2026/06/05/panel-de-hogares-cnmc-paquetes/"
  },
  {
    label: "Compras online en España 2025",
    organization: "ONTSI / Red.es",
    url: "https://www.ontsi.es/es/publicaciones/compras-online-en-espana-2025"
  },
  {
    label: "The 2025 OECD Definition of E-Commerce and Guidelines for Interpretation",
    organization: "OCDE",
    url: "https://www.oecd.org/en/publications/the-2025-oecd-definition-of-e-commerce-and-guidelines-for-interpretation_2254f1de-en.html"
  }
];

const cnmcAnnual = {
  period: "2025",
  published: "2026-08-05",
  traditionalPostal: {
    label: "Sector postal tradicional",
    shipmentsMillion: 1163.5,
    shipmentChangeYoYPct: -7.7,
    shipmentChange2019Pct: -52.1,
    revenueMillionEur: 1146.2,
    revenueChangeYoYPct: 2.8,
    revenueChange2019Pct: -17.1
  },
  parcelCourier: {
    label: "Mensajería y paquetería",
    shipmentsMillion: 1334.8,
    shipmentChangeYoYPct: 9.7,
    shipmentChange2019Pct: 147.8,
    revenueMillionEur: 7324.6,
    revenueChangeYoYPct: 9.8,
    revenueChange2019Pct: 86.3,
    b2cShipmentSharePct: 70.7,
    b2bShipmentSharePct: 24.9,
    c2xShipmentSharePct: 4.5,
    under2kgShipmentSharePct: 66.4,
    under2kgRevenueSharePct: 47.2,
    nationalShipmentSharePct: 68.0,
    importShipmentSharePct: 20.5,
    exportShipmentSharePct: 11.4,
    homeDeliveryOperatorSharePct: 85.8,
    conveniencePointOperatorSharePct: 9.4,
    lockerOperatorSharePct: 3.4,
    operatorOfficeSharePct: 1.5
  }
};

const panel = {
  period: "IV trimestre de 2025",
  publication: "2026-06-05",
  methodology:
    "Panel representativo de hogares e individuos residentes en España; en el segundo semestre de 2025 la muestra neta fue de 5.846 hogares y 8.745 individuos de 10 o más años.",
  ecommercePackageReceivedPct: 61.23850851800907,
  ecommercePackageReceivedChange2015Pp: 31.9,
  nonEcommercePackageReceivedPct: 10.310484176580173,
  nonEcommercePackageReceivedChange2015Pp: -6.6,
  returnedPackagePct: 34.99014489626929,
  homeOrWorkReceivedPct: 89.09539256538004,
  pickupSellerStorePct: 10.847341170825527,
  pickupConvenienceOfficePct: 27.71310293369675,
  pickupLockerPct: 18.199008995029872,
  lastPurchaseAmazonPct: 47.77334753407683,
  lastPurchaseChinesePlatformsPct: 24.712761221429225,
  lastPurchaseOnlineWithPhysicalStorePct: 13.04924983317389,
  lastPurchaseOnlineOnlyPct: 4.991245491125541,
  lastPurchaseSmallMerchantsPct: 2.0501991656930497,
  paidShippingPct: 15.237763975214375,
  didNotPayShippingPct: 83.19988608965171,
  freeShippingAnyAmountPct: 35.729716923322824,
  freeShippingMinimumOrderPct: 37.672932305936885,
  flatRateShippingPct: 22.625512162460745,
  onTimeLastPackagePct: 94.82461466141174,
  lateLastPackagePct: 4.3570648738336105,
  acceptPickupDiscountPct: 78.07845202716455,
  acceptLaterDeliveryDiscountPct: 57.64907056800366,
  anyDeliveryProblemPct: 16.006157397228435,
  falseAbsenceAmongProblemsPct: 45.1515290350793,
  lostGoodsAmongProblemsPct: 26.92749427842694,
  noticePickupElsewhereAmongProblemsPct: 17.114141934351054,
  damagedGoodsAmongProblemsPct: 15.255737104842057,
  deliveredToOtherWithoutConsentAmongProblemsPct: 13.170976026842757
};

const ontsi = {
  period: "2024",
  publication: "2025-11",
  fieldwork: "16/09/2025-03/10/2025",
  methodology:
    "Encuesta online CAWI a 3.000 personas usuarias de Internet de 16 a 74 años en España, con margen de error de ±1,79 puntos y confianza del 95%.",
  internetUsersMillion: 35.306865,
  onlineBuyersMillion: 29.4,
  onlineBuyerShareInternetUsersPct: 83.3,
  returnedProductOrServicePct: 35.0,
  previousReturnedProductOrServicePct: 40.3,
  deliveryFreeImportantPct: 61.8,
  deliveryTimeImportantPct: 53.4,
  trackingImportantPct: 50.8,
  deliveryFlexImportantPct: 46.3,
  deliveryCarrierImportantPct: 37.3,
  reasonableSameDayPct: 3.2,
  reasonableNextDayPct: 16.6,
  reasonableThreeDaysPct: 44.2,
  reasonableFiveDaysPct: 20.4,
  reasonableWeekPct: 13.1,
  reasonableMoreThanWeekPct: 2.6,
  buyersWithProblemPct: 24.5,
  claimedAmongProblemPct: 75.3,
  problemTypes: [
    ["Retraso en la entrega", 26.6],
    ["Producto estropeado o con desperfectos", 25.5],
    ["Llegó otro producto o con características diferentes", 23.4],
    ["No llegó el producto", 20.9],
    ["Problemas con la devolución", 18.6],
    ["Fraude económico", 18.2],
    ["Problemas con el proceso de reclamación o reparación", 17.9],
    ["Páginas de comercio electrónico sospechosas", 17.4],
    ["Coste final más elevado que el indicado", 15.5],
    ["Problemas con el medio de pago", 11.6],
    ["Conflictos aduaneros", 9.9]
  ],
  returnsByCategory: [
    ["Ropa, zapatos o accesorios", 47.4],
    ["Equipamiento electrónico o electrodomésticos", 16.1],
    ["Ordenadores, tabletas, teléfonos móviles o accesorios", 12.3],
    ["Muebles, hogar o jardinería", 11.3],
    ["Artículos deportivos", 10.9],
    ["Juguetes o cuidado de niños", 9.6],
    ["Cosméticos, belleza o bienestar", 8.4],
    ["Libros, cómics, revistas o periódicos físicos", 7.9],
    ["Restaurantes, comida rápida o catering", 6.8],
    ["Bicicletas, automóviles o repuestos", 6.0],
    ["Mascotas", 5.7],
    ["Limpieza o higiene personal", 5.5],
    ["Alimentos o bebidas", 5.4],
    ["Coleccionismo y antigüedades", 5.0],
    ["Películas o series en formato físico", 4.9],
    ["Música en formato físico", 4.8],
    ["Medicamentos o suplementos dietéticos", 4.4]
  ],
  returnsByAge: [
    ["16-24 años", 39.4],
    ["25-34 años", 40.1],
    ["35-44 años", 38.1],
    ["45-54 años", 38.6],
    ["55-64 años", 27.2],
    ["65-74 años", 22.7]
  ]
};

const calculations = {
  parcelVsTraditionalShipmentDifferenceMillion: round(cnmcAnnual.parcelCourier.shipmentsMillion - cnmcAnnual.traditionalPostal.shipmentsMillion),
  parcelVsTraditionalShipmentRatio: round(cnmcAnnual.parcelCourier.shipmentsMillion / cnmcAnnual.traditionalPostal.shipmentsMillion, 2),
  parcelVsTraditionalRevenueDifferenceMillionEur: round(cnmcAnnual.parcelCourier.revenueMillionEur - cnmcAnnual.traditionalPostal.revenueMillionEur),
  parcelVsTraditionalRevenueRatio: round(cnmcAnnual.parcelCourier.revenueMillionEur / cnmcAnnual.traditionalPostal.revenueMillionEur, 2),
  estimatedB2CShipmentsMillion: round(cnmcAnnual.parcelCourier.shipmentsMillion * cnmcAnnual.parcelCourier.b2cShipmentSharePct / 100),
  estimatedUnder2kgShipmentsMillion: round(cnmcAnnual.parcelCourier.shipmentsMillion * cnmcAnnual.parcelCourier.under2kgShipmentSharePct / 100),
  ecommerceVsNonEcommercePackageReachPp: round(panel.ecommercePackageReceivedPct - panel.nonEcommercePackageReceivedPct),
  ecommerceVsNonEcommercePackageReachRatio: round(panel.ecommercePackageReceivedPct / panel.nonEcommercePackageReceivedPct, 1),
  panelApproxInternetUsersWithEcommercePackageAndReturnPct: round(panel.ecommercePackageReceivedPct * panel.returnedPackagePct / 100),
  estimatedReturnersMillionOntsi: round(ontsi.onlineBuyersMillion * ontsi.returnedProductOrServicePct / 100, 1),
  returnsRateChangePpOntsi: round(ontsi.returnedProductOrServicePct - ontsi.previousReturnedProductOrServicePct),
  topReturnCategoryGapPp: round(ontsi.returnsByCategory[0][1] - ontsi.returnsByCategory[1][1]),
  shippingImprovementSharePctOntsi: round(47.7 + 25.2),
  deliveryQualityImprovementSharePctOntsi: round(45.5 + 16.3)
};

const COLORS = {
  ink: "#172033",
  slate: "#344054",
  muted: "#667085",
  border: "#D0D5DD",
  green: "#067647",
  green2: "#12B76A",
  blue: "#2563EB",
  blue2: "#3B82F6",
  amber: "#B54708",
  amber2: "#F59E0B",
  red: "#B42318",
  surface: "#F8FAFC",
  paleGreen: "#EAFBF3",
  paleBlue: "#EFF6FF",
  paleAmber: "#FFF7E6"
};

function round(value, digits = 1) {
  return Number(value.toFixed(digits));
}

function fmtNumber(value, digits = 1) {
  return new Intl.NumberFormat("es-ES", {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits
  }).format(value);
}

function pct(value, digits = 1) {
  return `${fmtNumber(value, digits)}%`;
}

function pp(value, digits = 1) {
  const sign = value > 0 ? "+" : "";
  return `${sign}${fmtNumber(value, digits)} pp`;
}

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

function toCsv(headers, rows) {
  return `${headers.join(",")}\n${rows.map((row) => row.map(csvEscape).join(",")).join("\n")}\n`;
}

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

function escapeXml(value) {
  return escapeHtml(value).replaceAll("'", "&apos;");
}

function attrs(attributes) {
  return Object.entries(attributes)
    .filter(([, value]) => value !== undefined && value !== null)
    .map(([key, value]) => `${key}="${escapeXml(value)}"`)
    .join(" ");
}

function rect(props) {
  return `<rect ${attrs(props)} />`;
}

function line(props) {
  return `<line ${attrs({ stroke: COLORS.border, "stroke-width": 1, ...props })} />`;
}

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

function tspanLine({ x, dy, value }) {
  return `<tspan x="${x}" dy="${dy}">${escapeXml(value)}</tspan>`;
}

function wrappedText({ x, y, value, maxChars = 52, size = 20, color = COLORS.ink, weight = 400, lineHeight = 1.24 }) {
  const words = String(value).split(/\s+/u);
  const lines = [];
  let current = "";
  for (const word of words) {
    const next = current ? `${current} ${word}` : word;
    if (next.length > maxChars && current) {
      lines.push(current);
      current = word;
    } else {
      current = next;
    }
  }
  if (current) lines.push(current);
  const tspans = lines
    .map((lineValue, index) => tspanLine({ x, dy: index === 0 ? 0 : `${lineHeight}em`, value: lineValue }))
    .join("");
  return `<text x="${x}" y="${y}" fill="${color}" font-size="${size}" font-weight="${weight}" font-family="Arial, Helvetica, sans-serif">${tspans}</text>`;
}

function pill({ x, y, width, label, color = COLORS.green, fill = COLORS.paleGreen }) {
  return `${rect({ x, y, width, height: 34, rx: 17, fill })}${text({ x: x + width / 2, y: y + 23, value: label, size: 13, weight: 700, color, anchor: "middle" })}`;
}

function svgDocument(content) {
  return `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
    <rect width="1600" height="1000" fill="#FFFFFF" />
    ${content}
  </svg>`;
}

function sourceFooter(value) {
  return [
    line({ x1: 70, y1: 914, x2: 1530, y2: 914, stroke: "#E4E7EC" }),
    text({ x: 70, y: 948, value: "Fuente:", size: 15, weight: 700, color: COLORS.slate }),
    wrappedText({ x: 136, y: 948, value, maxChars: 155, size: 15, color: COLORS.muted, lineHeight: 1.18 }),
    text({ x: 1530, y: 948, value: "tienrank.com", size: 16, weight: 700, color: COLORS.green, anchor: "end" })
  ].join("");
}

function brandHeader(label) {
  return [
    text({ x: 70, y: 70, value: "TienRank", size: 28, weight: 800, color: COLORS.green }),
    text({ x: 70, y: 103, value: label, size: 14, weight: 700, color: COLORS.muted }),
    line({ x1: 70, y1: 122, x2: 1530, y2: 122, stroke: "#E4E7EC" })
  ].join("");
}

async function renderPng(svg, filename) {
  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 chartSectorShift() {
  const maxVolume = 1450;
  const maxRevenue = 7600;
  const x0 = 680;
  const barW = 560;
  const rows = [
    {
      label: "Paquetería",
      volume: cnmcAnnual.parcelCourier.shipmentsMillion,
      revenue: cnmcAnnual.parcelCourier.revenueMillionEur,
      volumeChange: cnmcAnnual.parcelCourier.shipmentChange2019Pct,
      revenueChange: cnmcAnnual.parcelCourier.revenueChange2019Pct,
      color: COLORS.green
    },
    {
      label: "Postal tradicional",
      volume: cnmcAnnual.traditionalPostal.shipmentsMillion,
      revenue: cnmcAnnual.traditionalPostal.revenueMillionEur,
      volumeChange: cnmcAnnual.traditionalPostal.shipmentChange2019Pct,
      revenueChange: cnmcAnnual.traditionalPostal.revenueChange2019Pct,
      color: COLORS.blue
    }
  ];
  const parts = [
    brandHeader("INFORME POSTAL CNMC 2025"),
    pill({ x: 70, y: 160, width: 240, label: "SECTOR POSTAL", color: COLORS.green }),
    text({ x: 70, y: 244, value: "Los paquetes ya", size: 46, weight: 800 }),
    text({ x: 70, y: 300, value: "superan a las cartas", size: 46, weight: 800 }),
    wrappedText({
      x: 70,
      y: 352,
      value:
        "En 2025 la mensajería y paquetería movió 171,3 millones de envíos más que el sector postal tradicional. En ingresos, el negocio fue 6,4 veces mayor.",
      maxChars: 48,
      size: 20,
      color: COLORS.slate
    }),
    rect({ x: 70, y: 520, width: 270, height: 126, rx: 10, fill: COLORS.paleGreen }),
    text({ x: 96, y: 574, value: "+147,8%", size: 38, weight: 800, color: COLORS.green }),
    wrappedText({ x: 96, y: 612, value: "crecimiento de envíos de paquetería desde 2019", maxChars: 28, size: 16, color: COLORS.slate }),
    rect({ x: 360, y: 520, width: 230, height: 126, rx: 10, fill: COLORS.paleBlue }),
    text({ x: 386, y: 574, value: "-52,1%", size: 38, weight: 800, color: COLORS.blue }),
    wrappedText({ x: 386, y: 612, value: "caída de envíos postales tradicionales desde 2019", maxChars: 24, size: 16, color: COLORS.slate }),
    text({ x: x0, y: 190, value: "Volumen de envíos, 2025", size: 22, weight: 800 }),
    text({ x: x0, y: 505, value: "Ingresos, 2025", size: 22, weight: 800 })
  ];

  rows.forEach((row, index) => {
    const y = 234 + index * 92;
    const width = (row.volume / maxVolume) * barW;
    parts.push(text({ x: x0, y: y - 10, value: row.label, size: 18, weight: 700, color: COLORS.slate }));
    parts.push(rect({ x: x0, y, width: barW, height: 42, rx: 7, fill: COLORS.surface }));
    parts.push(rect({ x: x0, y, width, height: 42, rx: 7, fill: row.color }));
    parts.push(text({ x: x0 + barW + 18, y: y + 28, value: `${fmtNumber(row.volume)} M`, size: 20, weight: 800, color: COLORS.ink }));
    parts.push(text({ x: 1450, y: y + 28, value: `${row.volumeChange > 0 ? "+" : ""}${pct(row.volumeChange)}`, size: 16, weight: 700, color: row.volumeChange > 0 ? COLORS.green : COLORS.red, anchor: "end" }));
  });

  rows.forEach((row, index) => {
    const y = 548 + index * 92;
    const width = (row.revenue / maxRevenue) * barW;
    parts.push(text({ x: x0, y: y - 10, value: row.label, size: 18, weight: 700, color: COLORS.slate }));
    parts.push(rect({ x: x0, y, width: barW, height: 42, rx: 7, fill: COLORS.surface }));
    parts.push(rect({ x: x0, y, width, height: 42, rx: 7, fill: row.color }));
    parts.push(text({ x: x0 + barW + 18, y: y + 28, value: `${fmtNumber(row.revenue)} M EUR`, size: 20, weight: 800, color: COLORS.ink }));
    parts.push(text({ x: 1450, y: y + 28, value: `${row.revenueChange > 0 ? "+" : ""}${pct(row.revenueChange)}`, size: 16, weight: 700, color: row.revenueChange > 0 ? COLORS.green : COLORS.red, anchor: "end" }));
  });

  parts.push(sourceFooter("CNMC, Informe anual postal 2025. Variaciones desde 2019 y 2025; cálculos de diferencias y ratios de TienRank."));
  await renderPng(svgDocument(parts.join("")), "01-paquetes-superan-cartas-2025.png");
}

async function chartConsumerLogistics() {
  const rows = [
    ["Recibió paquete de compra online", panel.ecommercePackageReceivedPct, "Usuarios de Internet recientes"],
    ["Devolvió alguna compra con paquete", panel.returnedPackagePct, "Compradores online con paquete"],
    ["No pagó envío en el último paquete", panel.didNotPayShippingPct, "Último paquete de compra online"],
    ["Aceptaría recoger por descuento", panel.acceptPickupDiscountPct, "Compradores online con paquete"],
    ["Aceptaría entrega más lenta por descuento", panel.acceptLaterDeliveryDiscountPct, "Compradores online con paquete"],
    ["Tuvo algún problema de entrega", panel.anyDeliveryProblemPct, "Receptores de paquetes"]
  ];
  const x0 = 710;
  const max = 90;
  const barW = 690;
  const parts = [
    brandHeader("PANEL DE HOGARES CNMC | IV TRIMESTRE 2025"),
    pill({ x: 70, y: 160, width: 258, label: "COMPRADOR Y ENTREGA", color: COLORS.blue, fill: COLORS.paleBlue }),
    text({ x: 70, y: 244, value: "La logística decide", size: 46, weight: 800 }),
    text({ x: 70, y: 300, value: "la confianza tras pagar", size: 46, weight: 800 }),
    wrappedText({
      x: 70,
      y: 354,
      value:
        "Los porcentajes no son aditivos: cada fila usa su base declarada. La lectura útil está en la combinación de alcance, devolución, coste, flexibilidad e incidencia.",
      maxChars: 50,
      size: 20,
      color: COLORS.slate
    }),
    rect({ x: 70, y: 548, width: 235, height: 112, rx: 10, fill: COLORS.paleGreen }),
    text({ x: 96, y: 595, value: "5,9x", size: 38, weight: 800, color: COLORS.green }),
    wrappedText({ x: 96, y: 632, value: "más alcance del paquete ecommerce que del paquete no ecommerce", maxChars: 25, size: 15, color: COLORS.slate }),
    rect({ x: 325, y: 548, width: 250, height: 112, rx: 10, fill: COLORS.paleAmber }),
    text({ x: 351, y: 595, value: "21,4%", size: 38, weight: 800, color: COLORS.amber }),
    wrappedText({ x: 351, y: 632, value: "usuarios recientes con paquete ecommerce y devolución aproximada", maxChars: 26, size: 15, color: COLORS.slate })
  ];

  rows.forEach((row, index) => {
    const [label, value, base] = row;
    const y = 184 + index * 96;
    const width = (value / max) * barW;
    const color = index === 5 ? COLORS.red : index === 2 || index === 3 ? COLORS.green : COLORS.blue;
    parts.push(wrappedText({ x: x0, y: y + 6, value: label, maxChars: 34, size: 17, weight: 700, color: COLORS.slate }));
    parts.push(text({ x: x0, y: y + 52, value: base, size: 13, color: COLORS.muted }));
    const barX = x0 + 310;
    const innerBarW = barW - 310;
    const actualW = innerBarW * value / max;
    parts.push(rect({ x: barX, y, width: innerBarW, height: 42, rx: 7, fill: COLORS.surface }));
    parts.push(rect({ x: barX, y, width: actualW, height: 42, rx: 7, fill: color }));
    parts.push(text({ x: Math.min(barX + actualW + 14, 1480), y: y + 28, value: pct(value), size: 20, weight: 800, color: COLORS.ink }));
  });

  parts.push(sourceFooter("CNMCData, Panel de Hogares, sector postal y paquetería, IV-2025. Bases distintas indicadas en cada barra; cálculos de TienRank."));
  await renderPng(svgDocument(parts.join("")), "02-panel-logistica-comprador-2025.png");
}

async function chartReturnsCategory() {
  const rows = ontsi.returnsByCategory.slice(0, 10);
  const x0 = 610;
  const max = 50;
  const barW = 720;
  const parts = [
    brandHeader("ONTSI | COMPRAS ONLINE EN ESPAÑA 2025"),
    pill({ x: 70, y: 160, width: 238, label: "DEVOLUCIONES", color: COLORS.amber, fill: COLORS.paleAmber }),
    text({ x: 70, y: 244, value: "Moda concentra", size: 52, weight: 800 }),
    text({ x: 70, y: 304, value: "la devolución online", size: 52, weight: 800 }),
    wrappedText({
      x: 70,
      y: 354,
      value:
        "Entre quienes devuelven compras online, el 47,4% menciona ropa, zapatos o accesorios. No es una tasa por categoría, sino distribución declarada entre personas que realizan devoluciones.",
      maxChars: 50,
      size: 20,
      color: COLORS.slate
    }),
    rect({ x: 70, y: 556, width: 250, height: 112, rx: 10, fill: COLORS.paleAmber }),
    text({ x: 96, y: 603, value: "35,0%", size: 38, weight: 800, color: COLORS.amber }),
    wrappedText({ x: 96, y: 640, value: "compradores online devolvieron algún producto o servicio", maxChars: 26, size: 15, color: COLORS.slate }),
    rect({ x: 340, y: 556, width: 250, height: 112, rx: 10, fill: COLORS.paleBlue }),
    text({ x: 366, y: 603, value: "10,3 M", size: 38, weight: 800, color: COLORS.blue }),
    wrappedText({ x: 366, y: 640, value: "compradores aproximados con devolución en 2024", maxChars: 26, size: 15, color: COLORS.slate })
  ];

  rows.forEach(([label, value], index) => {
    const y = 176 + index * 66;
    const width = (value / max) * barW;
    const color = index === 0 ? COLORS.amber : index < 3 ? COLORS.blue : COLORS.green;
    parts.push(wrappedText({ x: x0, y: y + 6, value: label, maxChars: 36, size: 15, weight: 700, color: COLORS.slate }));
    parts.push(rect({ x: x0 + 390, y, width: barW - 390, height: 30, rx: 6, fill: COLORS.surface }));
    parts.push(rect({ x: x0 + 390, y, width: (barW - 390) * value / max, height: 30, rx: 6, fill: color }));
    parts.push(text({ x: x0 + 390 + (barW - 390) * value / max + 10, y: y + 22, value: pct(value), size: 16, weight: 800, color: COLORS.ink }));
  });

  parts.push(sourceFooter("ONTSI / Red.es, Compras online en España 2025, datos 2024. Base: compradores online que realizan devoluciones."));
  await renderPng(svgDocument(parts.join("")), "03-devoluciones-por-categoria-2024.png");
}

async function chartSummary() {
  const cards = [
    ["1.334,8 M", "paquetes enviados en 2025", COLORS.green, COLORS.paleGreen],
    ["171,3 M", "paquetes más que envíos postales tradicionales", COLORS.blue, COLORS.paleBlue],
    ["35,0%", "compradores con devolución según CNMC y ONTSI", COLORS.amber, COLORS.paleAmber],
    ["83,2%", "no pagó envío en su último paquete ecommerce", COLORS.green, COLORS.paleGreen],
    ["16,0%", "receptores de paquetes con algún problema", COLORS.red, "#FEF3F2"],
    ["78,1%", "aceptaría punto de recogida por descuento", COLORS.blue, COLORS.paleBlue]
  ];
  const parts = [
    brandHeader("INVESTIGACIÓN TIENRANK | LOGÍSTICA ECOMMERCE"),
    text({ x: 70, y: 214, value: "Los paquetes ya superan a las cartas", size: 58, weight: 800 }),
    wrappedText({
      x: 70,
      y: 270,
      value:
        "Análisis de TienRank a partir del Informe Postal 2025 de la CNMC, el Panel de Hogares y ONTSI: la entrega gratuita y la devolución ya forman parte del contrato de confianza del ecommerce.",
      maxChars: 105,
      size: 22,
      color: COLORS.slate,
      lineHeight: 1.28
    })
  ];

  cards.forEach(([value, label, color, fill], index) => {
    const col = index % 3;
    const row = Math.floor(index / 3);
    const x = 90 + col * 490;
    const y = 420 + row * 190;
    parts.push(rect({ x, y, width: 430, height: 132, rx: 12, fill, stroke: "#E4E7EC", "stroke-width": 1 }));
    parts.push(text({ x: x + 28, y: y + 58, value, size: 42, weight: 800, color }));
    parts.push(wrappedText({ x: x + 28, y: y + 94, value: label, maxChars: 34, size: 17, color: COLORS.slate }));
  });

  parts.push(sourceFooter("CNMC Informe Postal 2025, CNMCData Panel de Hogares IV-2025 y ONTSI Compras online en España 2025. Cálculos de TienRank."));
  await renderPng(svgDocument(parts.join("")), "04-resumen-logistica-ecommerce.png");
}

function buildRows() {
  const sectorRows = [
    [
      "Envíos 2025",
      "millones",
      cnmcAnnual.parcelCourier.shipmentsMillion,
      cnmcAnnual.traditionalPostal.shipmentsMillion,
      calculations.parcelVsTraditionalShipmentDifferenceMillion,
      calculations.parcelVsTraditionalShipmentRatio,
      "CNMC Informe Postal 2025, tablas 3 y 12"
    ],
    [
      "Ingresos 2025",
      "millones EUR",
      cnmcAnnual.parcelCourier.revenueMillionEur,
      cnmcAnnual.traditionalPostal.revenueMillionEur,
      calculations.parcelVsTraditionalRevenueDifferenceMillionEur,
      calculations.parcelVsTraditionalRevenueRatio,
      "CNMC Informe Postal 2025, tablas 3 y 12"
    ],
    [
      "Variación envíos 2019-2025",
      "%",
      cnmcAnnual.parcelCourier.shipmentChange2019Pct,
      cnmcAnnual.traditionalPostal.shipmentChange2019Pct,
      round(cnmcAnnual.parcelCourier.shipmentChange2019Pct - cnmcAnnual.traditionalPostal.shipmentChange2019Pct),
      "",
      "CNMC Informe Postal 2025"
    ],
    [
      "Variación ingresos 2019-2025",
      "%",
      cnmcAnnual.parcelCourier.revenueChange2019Pct,
      cnmcAnnual.traditionalPostal.revenueChange2019Pct,
      round(cnmcAnnual.parcelCourier.revenueChange2019Pct - cnmcAnnual.traditionalPostal.revenueChange2019Pct),
      "",
      "CNMC Informe Postal 2025"
    ]
  ];

  const panelRows = [
    ["Recibió paquete con origen compra online", "Usuarios de Internet en los últimos tres meses", panel.ecommercePackageReceivedPct, "+31,9 pp frente a 2015"],
    ["Recibió paquete sin origen compra online", "Individuos", panel.nonEcommercePackageReceivedPct, "-6,6 pp frente a 2015"],
    ["Devolvió alguna compra online con paquete", "Compradores online con paquete en los últimos seis meses", panel.returnedPackagePct, "Logística inversa"],
    ["Domicilio o trabajo", "Compradores online con paquete", panel.homeOrWorkReceivedPct, "Respuesta múltiple"],
    ["Punto de conveniencia u oficina", "Compradores online con paquete", panel.pickupConvenienceOfficePct, "Respuesta múltiple"],
    ["Taquilla autoservicio", "Compradores online con paquete", panel.pickupLockerPct, "Respuesta múltiple"],
    ["No pagó envío en el último paquete", "Último paquete de compra online", panel.didNotPayShippingPct, "Incluye importe mínimo, envío gratis y tarifa plana"],
    ["Aceptaría recoger fuera del domicilio por descuento", "Compradores online con paquete", panel.acceptPickupDiscountPct, "Hipótesis planteada por la encuesta"],
    ["Aceptaría entrega más lenta por descuento", "Compradores online con paquete", panel.acceptLaterDeliveryDiscountPct, "Hipótesis planteada por la encuesta"],
    ["Tuvo algún problema de entrega", "Receptores de paquetes en los últimos seis meses", panel.anyDeliveryProblemPct, "Paquete ecommerce o no ecommerce"]
  ];

  const returnRows = ontsi.returnsByCategory.map(([category, value]) => [
    category,
    value,
    "Personas usuarias de Internet que compran online y realizan devoluciones",
    "ONTSI Compras online en España 2025, tabla 30"
  ]);

  const problemRows = ontsi.problemTypes.map(([type, value]) => [
    type,
    value,
    "Personas usuarias de Internet que compran online y tuvieron algún problema durante 2024",
    "ONTSI Compras online en España 2025, gráfico 56"
  ]);

  return { sectorRows, panelRows, returnRows, problemRows };
}

const executiveSummary = [
  "España cruzó en 2025 un umbral simbólico y operativo: por primera vez se enviaron más paquetes que envíos postales tradicionales. La CNMC contabilizó 1.334,8 millones de envíos de mensajería y paquetería frente a 1.163,5 millones de cartas, tarjetas, publicidad directa y notificaciones. La diferencia fue de 171,3 millones de envíos y, en ingresos, la paquetería multiplicó por 6,4 al sector postal tradicional.",
  "El cambio no es solo una sustitución de cartas por paquetes. La paquetería creció un 147,8% en volumen desde 2019, mientras el correo tradicional cayó un 52,1%. Los envíos de empresas a particulares representaron el 70,7% de la paquetería y los paquetes de hasta 2 kg, el 66,4% de los envíos. La logística de ecommerce ya no es una fase invisible: es una infraestructura de consumo masivo.",
  "El comprador español ha incorporado esa infraestructura a sus expectativas. El Panel de Hogares de la CNMC muestra que el 61,2% de usuarios recientes de Internet recibió un paquete derivado de una compra online en el segundo semestre de 2025; solo el 10,3% recibió paquetes no vinculados al ecommerce. En el último paquete recibido de una compra online, el 83,2% no pagó gastos de envío.",
  "La devolución aparece como el punto donde la promesa digital se comprueba después de la venta. CNMC estima que el 35,0% de quienes recibieron paquetes de compras online tuvo que devolver alguna compra; ONTSI, con otra encuesta y otro periodo, también sitúa en el 35,0% a los compradores online que devolvieron algún producto o servicio en 2024. Esa coincidencia no permite sumar universos, pero sí indica que la logística inversa ya forma parte normal de la experiencia ecommerce."
];

const findings = [
  "La paquetería superó al sector postal tradicional en 2025: 1.334,8 millones de paquetes frente a 1.163,5 millones de envíos tradicionales, una diferencia de 171,3 millones.",
  "En ingresos, la distancia fue mayor: 7.324,6 millones de euros en paquetería frente a 1.146,2 millones en correo tradicional; la paquetería facturó 6,4 veces más.",
  "Desde 2019, los envíos de paquetería crecieron un 147,8%, mientras los envíos postales tradicionales cayeron un 52,1%.",
  "Los envíos de empresas a particulares concentraron el 70,7% de la paquetería, lo que equivale a unos 943,7 millones de paquetes B2C calculados por TienRank.",
  "El 61,2% de usuarios recientes de Internet recibió un paquete de compra online en el segundo semestre de 2025; el alcance fue 5,9 veces el de los paquetes no ligados al ecommerce.",
  "La devolución ya es un comportamiento masivo: CNMC y ONTSI sitúan en el 35,0% la proporción de compradores online que devolvió alguna compra o producto, aunque con bases y periodos distintos.",
  "El envío gratis domina la expectativa: ONTSI lo señala como el aspecto de entrega más importante para el 61,8% de compradores online y CNMC detecta que el 83,2% no pagó el envío de su último paquete ecommerce."
];

const methodology = [
  `Fecha de cierre del análisis: ${CLOSED_AT}. La investigación usa exclusivamente fuentes externas oficiales o institucionales.`,
  "El hito sectorial procede del Informe Anual del Sector Postal 2025 de la CNMC. Para el sector postal tradicional se usaron las tablas 3 y datos de 7 operadores; para mensajería y paquetería, la tabla 12 y una muestra de 26 operadores.",
  "El análisis de comportamiento de entrega procede del Panel de Hogares CNMC, sector postal y paquetería, IV trimestre de 2025. La metodología de la CNMC describe una muestra representativa de hogares e individuos residentes en España; en el segundo semestre de 2025 la muestra neta fue de 5.846 hogares y 8.745 individuos de 10 o más años.",
  "Los datos de devoluciones, problemas y expectativas de entrega de ONTSI proceden de Compras online en España 2025. El trabajo de campo fue una encuesta online CAWI a 3.000 personas usuarias de Internet de 16 a 74 años, realizada entre el 16 de septiembre y el 3 de octubre de 2025, con preguntas referidas a 2024.",
  "TienRank calculó diferencias absolutas, ratios y estimaciones descriptivas: diferencia entre paquetes y envíos postales tradicionales, ratio de ingresos, envíos B2C estimados por cuota, alcance relativo de paquetes ecommerce frente a no ecommerce y compradores con devolución aproximados según ONTSI.",
  "No se suman encuestas ni se fusionan denominadores incompatibles. Cada porcentaje mantiene su universo: operadores postales, individuos, usuarios recientes de Internet, compradores con paquete, compradores online o compradores que realizaron devoluciones.",
  "La definición de ecommerce se cita en sentido amplio siguiendo la OCDE, pero las mediciones concretas son las declaradas por CNMC, CNMCData y ONTSI en cada fuente.",
  "Los CSV, JSON, gráficos y documentos del kit de prensa se generaron con el script `scripts/research-logistica-devoluciones-ecommerce-2026.mjs`."
];

const limitations = [
  "El estudio describe estructuras, comportamientos declarados y cálculos descriptivos; no establece causalidad entre envío gratuito, devolución, incidencia o fidelización.",
  "Las cifras de CNMC sobre operadores postales miden envíos e ingresos sectoriales; no identifican todos los paquetes como compras ecommerce ni desagregan cuántos C2X son devoluciones.",
  "El Panel de Hogares CNMC y ONTSI usan muestras, periodos y universos diferentes. La coincidencia del 35% en devoluciones se interpreta como señal convergente, no como una única tasa nacional anual.",
  "ONTSI mide respuestas declaradas por personas compradoras online. Los problemas no equivalen a reclamaciones verificadas, expedientes administrativos ni incumplimientos legales individualizados.",
  "El dato de 29,4 millones de compradores online de ONTSI se usa para una estimación aproximada de compradores con devolución; no equivale a número de paquetes devueltos ni a número de pedidos afectados.",
  "Las categorías de devolución de ONTSI se calculan sobre personas que realizan devoluciones y pueden incluir respuesta múltiple. No representan tasas de devolución por categoría vendida."
];

const sections = [
  {
    id: "sorpasso-postal",
    kicker: "Cambio estructural",
    title: "El paquete adelanta al correo tradicional",
    paragraphs: [
      "La CNMC registra en 2025 el primer año en el que la mensajería y paquetería supera al sector postal tradicional tanto en volumen como en ingresos. El salto es material: 1.334,8 millones de paquetes frente a 1.163,5 millones de envíos tradicionales. La diferencia, calculada por TienRank, es de 171,3 millones de envíos.",
      "La brecha económica es aún más marcada. La paquetería generó 7.324,6 millones de euros, frente a 1.146,2 millones del sector postal tradicional. Es decir, 6,4 veces más ingresos. La lectura temporal refuerza el cambio: desde 2019 los envíos de paquetería crecieron un 147,8%, mientras los envíos tradicionales cayeron un 52,1%.",
      "Este cruce de curvas no significa que toda la paquetería sea ecommerce. El informe sectorial incluye flujos B2B, B2C y C2X, y la categoría C2X agrupa paquetes de particulares a empresas o a otros particulares, incluidas devoluciones de ecommerce. La conclusión defendible es más precisa: el consumo digital ha desplazado el centro operativo del sector postal hacia el paquete."
    ]
  },
  {
    id: "infraestructura-b2c",
    kicker: "Operativa",
    title: "La paquetería se organiza alrededor del comprador final",
    paragraphs: [
      "Los envíos de empresas a particulares representaron el 70,7% de los paquetes en 2025. Aplicado al volumen total de la paquetería, TienRank calcula unos 943,7 millones de envíos B2C. El B2B quedó en el 24,9% y el C2X en el 4,5%. Esta composición sitúa el domicilio, el punto de conveniencia, la taquilla y la devolución como decisiones de diseño de canal, no como simples costes logísticos.",
      "También importa el tamaño del paquete. La CNMC indica que los envíos de hasta 2 kg concentraron el 66,4% del volumen y el 47,2% de los ingresos. TienRank estima unos 886,7 millones de paquetes ligeros en 2025. En paralelo, los flujos nacionales representaron el 68,0% de los envíos, las importaciones el 20,5% y las exportaciones el 11,4%.",
      "Para una tienda online, estos datos dibujan un mercado en el que el paquete pequeño, nacional o importado, compite por precio, trazabilidad y conveniencia. La logística de última milla ya no es solo ejecución: condiciona qué comercios pueden sostener promesas de entrega, qué costes absorben y qué margen queda para resolver devoluciones."
    ]
  },
  {
    id: "comprador-envio-gratis",
    kicker: "Expectativas",
    title: "El envío gratis se ha convertido en la referencia mental",
    paragraphs: [
      "El Panel de Hogares de la CNMC muestra que el 61,2% de usuarios recientes de Internet recibió al menos un paquete derivado de una compra online durante el segundo semestre de 2025. La recepción de paquetes sin origen ecommerce fue del 10,3%. La distancia es de 50,9 puntos y el alcance relativo del paquete ecommerce es 5,9 veces mayor.",
      "La gratuidad del envío aparece en dos mediciones distintas. En la última compra online con paquete, el 83,2% de los encuestados por la CNMC no pagó el envío. ONTSI, en su encuesta de compras online, identifica el envío gratis como el aspecto de entrega más importante para el 61,8% de las personas compradoras, por delante del tiempo de entrega, el seguimiento y la flexibilidad.",
      "La gratuidad no elimina la sensibilidad al diseño logístico. Ante un descuento, el 78,1% aceptaría recoger el paquete en una tienda, oficina o taquilla autoservicio y el 57,6% aceptaría recibirlo más tarde. La oportunidad no está solo en prometer rapidez: está en explicar opciones, costes y compensaciones de forma comprensible antes del pago."
    ]
  },
  {
    id: "devolucion-mainstream",
    kicker: "Logística inversa",
    title: "La devolución ya forma parte normal del ecommerce",
    paragraphs: [
      "La devolución aparece con una consistencia llamativa en dos fuentes independientes. En el Panel de Hogares de la CNMC, el 35,0% de quienes hicieron compras online con recepción de paquete en los últimos seis meses tuvo que devolver alguna compra. En ONTSI, el 35,0% de compradores online declaró haber devuelto algún producto o servicio adquirido por Internet durante 2024.",
      "No son el mismo universo ni el mismo periodo, por lo que no deben promediarse. Sí permiten una lectura operativa: la devolución no es una excepción residual. Si se aplica la tasa de ONTSI al universo de 29,4 millones de compradores online que recoge el propio informe, TienRank obtiene una estimación aproximada de 10,3 millones de compradores que devolvieron algo en 2024.",
      "La moda concentra la señal: entre quienes realizan devoluciones, el 47,4% menciona ropa, zapatos o accesorios. El segundo grupo, equipamiento electrónico o electrodomésticos, queda en el 16,1%. La diferencia de 31,3 puntos no debe leerse como tasa de devolución por categoría vendida, pero sí como una alerta clara sobre tallas, expectativas visuales, guía de producto y coste de logística inversa."
    ]
  },
  {
    id: "incidencias-posventa",
    kicker: "Calidad",
    title: "El problema no siempre es llegar tarde",
    paragraphs: [
      "La CNMC registra que el 94,8% recibió su último paquete ecommerce dentro del plazo previsto y solo el 4,4% lo recibió fuera de plazo. Sin embargo, cuando se pregunta por cualquier problema de entrega en los últimos seis meses, el porcentaje sube al 16,0% entre quienes recibieron paquetes de compra online o de otro tipo.",
      "Entre quienes tuvieron problemas de entrega, el aviso de ausencia incorrecto fue el más frecuente, con un 45,2%. Le siguieron la pérdida o extravío de la mercancía, con un 26,9%; la necesidad de recoger en otro lugar tras aviso, con un 17,1%; la mercancía dañada, con un 15,3%; y la entrega a otra persona sin consentimiento, con un 13,2%.",
      "ONTSI ofrece una mirada complementaria desde la compra online completa. El 24,5% de compradores declaró haber tenido algún problema durante 2024; entre quienes tuvieron problemas, los principales fueron retraso en la entrega, producto estropeado, llegada de un producto diferente, no recepción y problemas con la devolución. La incidencia logística y la posventa aparecen juntas."
    ]
  },
  {
    id: "conclusion",
    kicker: "Lectura TienRank",
    title: "La siguiente ventaja competitiva no es enviar más, sino resolver mejor",
    paragraphs: [
      "La investigación muestra un desplazamiento claro: el ecommerce no solo aumenta transacciones, también reorganiza el sistema postal alrededor de paquetes, entregas flexibles, costes de envío y devoluciones. Para el comprador, la experiencia no termina en el botón de pagar; se confirma cuando el paquete llega, cuando puede seguirlo, cuando entiende el coste y cuando puede devolver sin fricción opaca.",
      "La conclusión práctica para tiendas, operadores y responsables de consumo es concreta. La promesa de envío gratis debe ser económicamente sostenible y transparente; las alternativas de entrega deben explicarse con ventajas reales; la información de devolución debe aparecer antes del pago; y los equipos de atención deben tratar la incidencia logística como parte central de la confianza, no como un asunto externalizado al operador.",
      "El dato de 2025 no invita a celebrar simplemente que haya más paquetes que cartas. Invita a medir mejor qué ocurre después de la compra: cuántas entregas fallan, cuántas devoluciones se resuelven a tiempo, cuánto coste se traslada o se oculta y qué comercios pueden competir sin convertir la logística inversa en una pérdida permanente."
    ]
  }
];

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

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

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

function htmlTable(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 chartImage(filename, caption) {
  const src = path.join(PUBLIC_DIR, filename);
  return `<img class="chart" src="file://${src}" alt="${escapeHtml(caption)}" /><p class="caption">${escapeHtml(caption)}</p>`;
}

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: 30px; line-height: 1.08; margin: 0 0 12px; }
    h2 { font-size: 19px; margin: 27px 0 10px; color: #067647; }
    h3 { font-size: 15px; margin: 20px 0 8px; color: #344054; }
    p { font-size: 11.4pt; 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: 8.8pt; }
    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: 13pt; 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: ${CLOSED_AT}</p>
  ${body}
</body>
</html>`;
}

function buildDocuments(rows) {
  const title = "Los paquetes ya superan a las cartas: la devolución online se convierte en la nueva prueba de confianza";
  const subtitle =
    "Análisis de TienRank a partir de CNMC, CNMCData y ONTSI: España movió 1.334,8 millones de paquetes en 2025 y dos fuentes sitúan las devoluciones online en torno al 35% de compradores.";
  const sectorTable = rows.sectorRows.map((row) => row.map((cell) => (typeof cell === "number" ? fmtNumber(cell, Number.isInteger(cell) ? 0 : 1) : cell)));
  const panelTable = rows.panelRows.map(([metric, base, value, note]) => [metric, base, pct(value), note]);
  const returnsTable = rows.returnRows.slice(0, 10).map(([category, value]) => [category, pct(value)]);
  const problemsTable = rows.problemRows.slice(0, 8).map(([type, value]) => [type, pct(value)]);

  const dossierParts = [
    "## Resumen ejecutivo",
    ...executiveSummary,
    "",
    "## Principales hallazgos",
    ...findings.map((finding) => `- ${finding}`),
    "",
    "## Gráfico principal",
    `![Resumen de la investigación](${ASSET_PATH}/04-resumen-logistica-ecommerce.png)`,
    "",
    ...sections.flatMap((section) => [`## ${section.title}`, ...section.paragraphs, ""]),
    "## Tabla 1. Paquetería frente al correo tradicional",
    markdownTable(["Métrica", "Unidad", "Paquetería", "Postal tradicional", "Diferencia", "Ratio", "Fuente"], sectorTable),
    "",
    "## Tabla 2. Panel de Hogares CNMC",
    markdownTable(["Indicador", "Base", "Porcentaje", "Nota"], panelTable),
    "",
    "## Tabla 3. Principales categorías de devolución ONTSI",
    markdownTable(["Categoría", "Porcentaje entre quienes devuelven"], returnsTable),
    "",
    "## Tabla 4. Problemas declarados en compras online ONTSI",
    markdownTable(["Problema", "Porcentaje entre compradores con problemas"], problemsTable),
    "",
    "## Material para medios",
    "### Ángulos editoriales",
    "- Logística: por qué 2025 marca el adelantamiento de la paquetería al correo tradicional.",
    "- Consumo: cómo el envío gratis y la devolución cambian lo que el comprador considera normal.",
    "- Retail: qué significa que moda concentre casi la mitad de las devoluciones declaradas entre quienes devuelven.",
    "- Operadores: dónde aparecen las incidencias cuando el último paquete llega en plazo pero el 16% declara algún problema.",
    "- Regulación y atención al cliente: por qué la información de devolución y la reclamación son parte de la confianza online.",
    "",
    "### Cita propuesta pendiente",
    "> La logística ya no es la trastienda del ecommerce. Cuando hay más paquetes que cartas y uno de cada tres compradores declara devoluciones, la confianza se decide después del pago: en el seguimiento, el coste real, la entrega y la resolución.",
    "David Trotonda, CEO de TienRank. Propuesta editorial pendiente de aprobación; no debe enviarse ni reproducirse como declaración literal sin aprobación expresa.",
    "",
    "### 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",
    ...SOURCES.map((source) => `- ${source.organization}: ${source.label}. ${source.url}`),
    "",
    "## Descargas públicas",
    `- Dossier PDF: ${ASSET_PATH}/dossier-medios.pdf`,
    `- Nota de prensa PDF: ${ASSET_PATH}/nota-prensa.pdf`,
    `- Datos sectoriales CSV: ${ASSET_PATH}/sector-postal-paqueteria-2025.csv`,
    `- Panel CNMC CSV: ${ASSET_PATH}/panel-hogares-paqueteria-iv-2025.csv`,
    `- Devoluciones ONTSI CSV: ${ASSET_PATH}/devoluciones-ontsi-2024.csv`,
    `- JSON de hallazgos y metodología: ${ASSET_PATH}/hallazgos-y-metodologia.json`,
    `- Kit ZIP: ${ASSET_PATH}/kit-prensa-logistica-devoluciones-ecommerce-espana-2026.zip`,
    "",
    "## Cómo citar",
    "TienRank (2026). Los paquetes ya superan a las cartas: la devolución online se convierte en la nueva prueba de confianza. Análisis de TienRank a partir de CNMC, CNMCData, ONTSI y OCDE.",
    ""
  ];

  const dossierHtmlParts = [
    "<h2>Resumen ejecutivo</h2>",
    ...executiveSummary.map(paragraph),
    chartImage("04-resumen-logistica-ecommerce.png", "Resumen de los principales datos de logística y devoluciones ecommerce."),
    "<h2>Principales hallazgos</h2>",
    list(findings),
    ...sections.flatMap((section, index) => {
      const image =
        index === 0
          ? chartImage("01-paquetes-superan-cartas-2025.png", "Paquetería frente al correo tradicional en volumen e ingresos.")
          : index === 2
            ? chartImage("02-panel-logistica-comprador-2025.png", "Indicadores del Panel de Hogares sobre paquetes ecommerce.")
            : index === 3
              ? chartImage("03-devoluciones-por-categoria-2024.png", "Categorías con más devoluciones declaradas según ONTSI.")
              : "";
      return [`<h2>${escapeHtml(section.title)}</h2>`, ...section.paragraphs.map(paragraph), image];
    }),
    "<h2>Tablas</h2>",
    "<h3>Paquetería frente al correo tradicional</h3>",
    htmlTable(["Métrica", "Unidad", "Paquetería", "Postal tradicional", "Diferencia", "Ratio", "Fuente"], sectorTable),
    "<h3>Panel de Hogares CNMC</h3>",
    htmlTable(["Indicador", "Base", "Porcentaje", "Nota"], panelTable),
    "<h3>Principales categorías de devolución ONTSI</h3>",
    htmlTable(["Categoría", "Porcentaje entre quienes devuelven"], returnsTable),
    "<h3>Problemas declarados en compras online ONTSI</h3>",
    htmlTable(["Problema", "Porcentaje entre compradores con problemas"], problemsTable),
    "<h2>Material para medios</h2>",
    paragraph("Ángulos: logística, consumo, retail, operadores y regulación/atención al cliente."),
    '<p class="quote">Cita propuesta pendiente: La logística ya no es la trastienda del ecommerce. Cuando hay más paquetes que cartas y uno de cada tres compradores declara devoluciones, la confianza se decide después del pago: en el seguimiento, el coste real, la entrega y la resolución. David Trotonda, CEO de TienRank. Pendiente de aprobación.</p>',
    "<h2>Metodología</h2>",
    list(methodology),
    "<h2>Limitaciones</h2>",
    list(limitations),
    "<h2>Fuentes</h2>",
    list(SOURCES.map((source) => `${source.organization}: ${source.label}. ${source.url}`)),
    "<h2>Citación</h2>",
    paragraph("TienRank (2026). Los paquetes ya superan a las cartas: la devolución online se convierte en la nueva prueba de confianza. Análisis de TienRank a partir de CNMC, CNMCData, ONTSI y OCDE.")
  ];

  const pressMarkdown = [
    "# Nota de prensa",
    "",
    `## ${title}`,
    "",
    "Valencia, 10 de agosto de 2026. TienRank publica una investigación sobre el nuevo peso de la logística en el ecommerce español a partir del Informe Anual del Sector Postal 2025 de la CNMC, el Panel de Hogares CNMC y Compras online en España 2025 de ONTSI.",
    "",
    "El análisis identifica un cambio estructural: en 2025 se enviaron 1.334,8 millones de paquetes, 171,3 millones más que envíos postales tradicionales. En ingresos, la paquetería alcanzó 7.324,6 millones de euros y multiplicó por 6,4 al correo tradicional.",
    "",
    "El estudio conecta ese hito sectorial con el comportamiento del comprador. El 61,2% de usuarios recientes de Internet recibió un paquete de compra online en el segundo semestre de 2025 y el 83,2% no pagó gastos de envío en su último paquete ecommerce. En devoluciones, CNMC y ONTSI sitúan la proporción en el 35,0% de compradores, aunque con universos y periodos diferentes.",
    "",
    "La investigación concluye que la confianza ecommerce ya no se decide solo en precio, surtido o pago. La información de envío, las alternativas de recogida, la trazabilidad, la devolución y la atención tras una incidencia se han convertido en parte central de la experiencia de compra.",
    "",
    "Contacto: info@tienrank.com",
    ""
  ].join("\n");

  const pressHtml = htmlDoc({
    title: `Nota de prensa: ${title}`,
    subtitle,
    label: "Nota de prensa",
    body: pressMarkdown
      .split("\n")
      .filter((line) => line && !line.startsWith("#"))
      .map(paragraph)
      .join("")
  });

  return {
    title,
    subtitle,
    dossierMarkdown: `# ${title}\n\n${subtitle}\n\n${dossierParts.join("\n")}`,
    dossierHtml: htmlDoc({ title, subtitle, label: "Investigación de ecommerce", body: dossierHtmlParts.join("") }),
    pressMarkdown,
    pressHtml
  };
}

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(`soffice failed for ${htmlPath}: ${result.stderr || result.stdout}`);
  return htmlPath.replace(/\.html$/u, ".pdf");
}

async function zipPressKit(files) {
  const zipPath = path.join(RESEARCH_DIR, "kit-prensa-logistica-devoluciones-ecommerce-espana-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(CHART_DIR, { recursive: true });
  await mkdir(PUBLIC_DIR, { recursive: true });

  const rows = buildRows();
  await Promise.all([chartSectorShift(), chartConsumerLogistics(), chartReturnsCategory(), chartSummary()]);

  const csvFiles = [
    ["sector-postal-paqueteria-2025.csv", toCsv(["metrica", "unidad", "paqueteria_cep", "postal_tradicional_spt", "diferencia", "ratio", "fuente"], rows.sectorRows)],
    ["panel-hogares-paqueteria-iv-2025.csv", toCsv(["indicador", "base", "porcentaje", "nota"], rows.panelRows)],
    ["devoluciones-ontsi-2024.csv", toCsv(["categoria", "porcentaje", "base", "fuente"], rows.returnRows)],
    ["problemas-compras-online-ontsi-2024.csv", toCsv(["problema", "porcentaje", "base", "fuente"], rows.problemRows)]
  ];

  for (const [filename, content] of csvFiles) {
    await writeFile(path.join(DATA_DIR, filename), content);
    await writeFile(path.join(PUBLIC_DIR, filename), content);
  }

  const findingsJson = {
    slug: SLUG,
    title: "Los paquetes ya superan a las cartas: la devolución online se convierte en la nueva prueba de confianza",
    closedAt: CLOSED_AT,
    publishedAt: PUBLISHED_AT,
    observationPeriod: "CNMC sector postal 2025; Panel de Hogares IV-2025; ONTSI compras online 2024",
    sources: SOURCES,
    rawExtracts: { cnmcAnnual, panel, ontsi },
    calculations,
    methodology,
    limitations,
    charts: [
      `${ASSET_PATH}/01-paquetes-superan-cartas-2025.png`,
      `${ASSET_PATH}/02-panel-logistica-comprador-2025.png`,
      `${ASSET_PATH}/03-devoluciones-por-categoria-2024.png`,
      `${ASSET_PATH}/04-resumen-logistica-ecommerce.png`
    ],
    csvFiles: csvFiles.map(([filename]) => `${ASSET_PATH}/${filename}`)
  };
  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 docs = buildDocuments(rows);
  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 parity = [
    "# Matriz de paridad dossier-web",
    "",
    "| Sección del dossier final | Estado | Ubicación web/catálogo | Motivo |",
    "| --- | --- | --- | --- |",
    "| Título, subtítulo, autor, fecha, periodo observado | WEB | `lib/research.ts` y página canónica | Visible en hero y metadatos |",
    "| Resumen ejecutivo completo | WEB | `executiveSummary` | Cuatro párrafos íntegros |",
    "| Principales hallazgos | WEB | `findings` | Siete hallazgos completos |",
    "| Secciones narrativas | WEB | `sections` | Seis secciones completas |",
    "| Gráficos PNG | WEB y DOWNLOAD | `heroImage`, `sections.image`, carpeta pública y ZIP | Cuatro PNG finales |",
    "| Tablas sector, panel, devoluciones e incidencias | WEB y DOWNLOAD | Tablas en `sections`; CSV públicos | Todas las filas clave visibles o descargables con resumen suficiente |",
    "| Metodología | WEB | `methodology` | Ocho puntos íntegros |",
    "| Limitaciones | WEB | `limitations` | Seis limitaciones íntegras |",
    "| Fuentes | WEB | `sources` | Seis enlaces directos |",
    "| Material para medios, perfil y contacto | WEB | `mediaAngles`, `pressQuote`, bloque de contacto del render genérico | Cita marcada pending |",
    "| PDF, nota de prensa, CSV, JSON y ZIP | DOWNLOAD | `downloads` | Archivos finales publicados |",
    "| Cita bibliográfica | WEB | `citation` | Visible al final del artículo |",
    "",
    "Resultado: paridad dossier-web completa frente al dossier final. No hay elementos WITHHELD."
  ].join("\n");
  await writeFile(path.join(RESEARCH_DIR, "web-parity-checklist.md"), parity);

  const readme = [
    "# Investigación: logística y devoluciones ecommerce España 2026",
    "",
    `Slug: \`${SLUG}\``,
    `Cierre: ${CLOSED_AT}`,
    "",
    "## Reproducción",
    "",
    "```bash",
    "node scripts/research-logistica-devoluciones-ecommerce-2026.mjs",
    "```",
    "",
    "El script conserva los valores extraídos de las fuentes primarias en `datos/hallazgos-y-metodologia.json`, recalcula diferencias y ratios, genera CSV, PNG, dossier HTML/PDF, nota de prensa HTML/PDF y kit ZIP.",
    "",
    "## Diferencia frente a estudios previos",
    "",
    "Este estudio no analiza facturación general del ecommerce, experiencia declarada frente a la UE, fraude en pagos, alertas Safety Gate ni adopción empresarial. El ángulo nuevo es la conversión del sistema postal español en infraestructura de paquetes, envío gratuito, devolución y posventa."
  ].join("\n");
  await writeFile(path.join(RESEARCH_DIR, "LEEME.md"), readme);

  const emailDraft = [
    "# Borrador de presentación a medios",
    "",
    "Asunto orientativo: Datos CNMC/ONTSI: los paquetes ya superan a las cartas en España",
    "",
    "Hola [nombre],",
    "",
    "Te escribo porque acabamos de publicar en TienRank un análisis sobre logística ecommerce con datos de CNMC, CNMCData y ONTSI. El dato principal es que en 2025 se enviaron 1.334,8 millones de paquetes, 171,3 millones más que envíos postales tradicionales. Además, dos fuentes sitúan las devoluciones online en torno al 35% de compradores, aunque con bases distintas.",
    "",
    "La página incluye metodología, limitaciones, gráficos y CSV descargables:",
    "https://tienrank.com/investigaciones/logistica-devoluciones-ecommerce-espana-2026",
    "",
    "Si te resulta útil, puedo facilitarte el PDF, los CSV o una explicación breve de los cálculos.",
    "",
    "David Trotonda",
    "CEO de TienRank",
    "https://tienrank.com",
    "davidtroton@gmail.com",
    "",
    "Nota: no incluye la cita propuesta pendiente de aprobación."
  ].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 zipPressKit([
    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(CHART_DIR, "01-paquetes-superan-cartas-2025.png"),
    path.join(CHART_DIR, "02-panel-logistica-comprador-2025.png"),
    path.join(CHART_DIR, "03-devoluciones-por-categoria-2024.png"),
    path.join(CHART_DIR, "04-resumen-logistica-ecommerce.png"),
    ...csvFiles.map(([filename]) => path.join(DATA_DIR, filename)),
    path.join(DATA_DIR, "hallazgos-y-metodologia.json"),
    path.join(ROOT, "scripts", "research-logistica-devoluciones-ecommerce-2026.mjs")
  ]);

  console.log(`Generated ${SLUG}`);
}

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