/* Exportación Ventas y pagos — CSV, Excel (gráficas embebidas) y PDF */

const EXPORT_ESTADO_LABELS = {
  pagado: 'Pagados',
  pendiente: 'Pendientes',
  vencido: 'Vencidos',
  urgente: 'Urgentes',
  revision: 'En revisión',
  rechazado: 'Rechazados',
};

const EXPORT_ESTADO_COLORS = {
  pagado: '#22c55e',
  pendiente: '#eab308',
  vencido: '#ef4444',
  urgente: '#f97316',
  revision: '#3b82f6',
  rechazado: '#94a3b8',
};

const MONTH_ES_SHORT = {
  ene: 1, feb: 2, mar: 3, abr: 4, may: 5, jun: 6,
  jul: 7, ago: 8, sep: 9, oct: 10, nov: 11, dic: 12,
};

function parsePaymentPeriod (p) {
  if (p?.due_date) {
    const d = new Date(p.due_date);
    if (!Number.isNaN(d.getTime())) {
      const month = d.getMonth() + 1;
      const year = d.getFullYear();
      return {
        key: `${year}-${String(month).padStart(2, '0')}`,
        year,
        month,
        label: d.toLocaleDateString('es-MX', { month: 'short', year: 'numeric' }),
      };
    }
  }
  const raw = String(p?.mes || p?.concepto || '').toLowerCase();
  const yearMatch = raw.match(/20\d{2}/);
  const year = yearMatch ? Number(yearMatch[0]) : new Date().getFullYear();
  let month = null;
  for (const [abbr, num] of Object.entries(MONTH_ES_SHORT)) {
    if (raw.includes(abbr)) { month = num; break; }
  }
  if (!month) return { key: 'sin-fecha', year: 0, month: 0, label: p?.mes || 'Sin fecha' };
  return {
    key: `${year}-${String(month).padStart(2, '0')}`,
    year,
    month,
    label: p.mes || `${Object.keys(MONTH_ES_SHORT).find(k => MONTH_ES_SHORT[k] === month)} ${year}`,
  };
}

function pctChange (current, previous) {
  if (previous == null || previous === undefined) return '—';
  if (previous === 0) return current > 0 ? '+100%' : '0%';
  const pct = ((current - previous) / previous) * 100;
  const sign = pct > 0 ? '+' : '';
  return `${sign}${pct.toFixed(1)}%`;
}

function deltaNum (current, previous) {
  if (previous == null || previous === undefined) return '—';
  const d = current - previous;
  return d > 0 ? `+${d}` : String(d);
}

function emptyMonthBucket (label, year, month, key) {
  return {
    key, label, year, month,
    count: 0, monto: 0, pagadoCount: 0, pagadoMonto: 0,
  };
}

function buildPaymentsExportAnalytics (pagos) {
  const rows = pagos || [];
  const byEstado = {};
  const byMes = {};
  const byMonthKey = {};
  const byYear = {};
  let totalBase = 0;
  let totalRecargo = 0;
  let totalMonto = 0;

  rows.forEach((p) => {
    const est = p.estado || 'pendiente';
    byEstado[est] = (byEstado[est] || 0) + 1;

    const period = parsePaymentPeriod(p);
    const monto = Number(p.monto) || 0;
    const isPagado = est === 'pagado';

    if (!byMonthKey[period.key]) {
      byMonthKey[period.key] = emptyMonthBucket(period.label, period.year, period.month, period.key);
    }
    const mb = byMonthKey[period.key];
    mb.count += 1;
    mb.monto += monto;
    if (isPagado) {
      mb.pagadoCount += 1;
      mb.pagadoMonto += monto;
    }

    if (period.year) {
      if (!byYear[period.year]) {
        byYear[period.year] = { year: period.year, count: 0, monto: 0, pagadoCount: 0, pagadoMonto: 0 };
      }
      const yb = byYear[period.year];
      yb.count += 1;
      yb.monto += monto;
      if (isPagado) {
        yb.pagadoCount += 1;
        yb.pagadoMonto += monto;
      }
    }

    const mesKey = p.mes || period.label;
    if (!byMes[mesKey]) byMes[mesKey] = { count: 0, monto: 0 };
    byMes[mesKey].count += 1;
    byMes[mesKey].monto += monto;

    totalBase += Number(p.montoBase) || 0;
    totalRecargo += Number(p.recargo) || 0;
    totalMonto += monto;
  });

  const monthsSorted = Object.values(byMonthKey)
    .filter(m => m.key !== 'sin-fecha')
    .sort((a, b) => a.key.localeCompare(b.key));

  const yearsSorted = Object.values(byYear).sort((a, b) => a.year - b.year);

  const monthCompare = monthsSorted.map((cur, i) => {
    const prev = i > 0 ? monthsSorted[i - 1] : null;
    const yoyKey = `${cur.year - 1}-${String(cur.month).padStart(2, '0')}`;
    const yoy = byMonthKey[yoyKey] || null;
    return {
      ...cur,
      prev,
      yoy,
      deltaCountVsPrev: prev ? cur.count - prev.count : null,
      deltaMontoVsPrev: prev ? cur.monto - prev.monto : null,
      pctCountVsPrev: prev ? pctChange(cur.count, prev.count) : '—',
      pctMontoVsPrev: prev ? pctChange(cur.monto, prev.monto) : '—',
      deltaCountVsYoy: yoy ? cur.count - yoy.count : null,
      deltaMontoVsYoy: yoy ? cur.monto - yoy.monto : null,
      pctCountVsYoy: yoy ? pctChange(cur.count, yoy.count) : '—',
      pctMontoVsYoy: yoy ? pctChange(cur.monto, yoy.monto) : '—',
    };
  });

  const yearCompare = yearsSorted.map((cur, i) => {
    const prev = i > 0 ? yearsSorted[i - 1] : null;
    return {
      ...cur,
      prev,
      deltaCountVsPrev: prev ? cur.count - prev.count : null,
      deltaMontoVsPrev: prev ? cur.monto - prev.monto : null,
      pctCountVsPrev: prev ? pctChange(cur.count, prev.count) : '—',
      pctMontoVsPrev: prev ? pctChange(cur.monto, prev.monto) : '—',
    };
  });

  return {
    total: rows.length,
    byEstado,
    byMes,
    byMonthKey,
    byYear,
    monthsSorted,
    yearsSorted,
    monthCompare,
    yearCompare,
    totalBase,
    totalRecargo,
    totalMonto,
    alumnos: new Set(rows.map(p => p.id)).size,
  };
}

function buildPaymentsExportSummary (pagos) {
  return buildPaymentsExportAnalytics(pagos);
}

function paymentsExportFilename (ext, filterLabel) {
  const safe = String(filterLabel || 'todos').replace(/\s+/g, '-').toLowerCase();
  const date = new Date().toISOString().slice(0, 10);
  return `tecos-pagos-${safe}-${date}.${ext}`;
}

function paymentsDetailRows (pagos) {
  return (pagos || []).map(p => [
    p.id,
    p.fullName,
    p.tutor,
    p.phone,
    p.mes,
    p.concepto,
    p.montoBase,
    p.recargo || 0,
    p.monto,
    p.lim,
    p.pago,
    EXPORT_ESTADO_LABELS[p.estado] || p.estado,
    p.metodo,
    p.daysLate || 0,
  ]);
}

function chartPngBase64 (dataUrl) {
  return (dataUrl || '').replace(/^data:image\/\w+;base64,/, '');
}

/** Gráfica off-screen con Chart.js → PNG */
async function renderChartPng ({
  type, labels, values, colors, width = 520, height = 300,
  datasetLabel = 'Cantidad', secondValues = null, secondLabel = null,
}) {
  if (typeof Chart === 'undefined') return null;
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  const datasets = [{
    label: datasetLabel,
    data: values,
    backgroundColor: type === 'line' ? colors[0] + '33' : colors,
    borderColor: type === 'line' ? colors[0] : colors,
    borderWidth: type === 'line' ? 2 : 1,
    fill: type === 'line',
    tension: 0.25,
  }];
  if (secondValues) {
    datasets.push({
      label: secondLabel || 'Serie 2',
      data: secondValues,
      backgroundColor: '#22c55e55',
      borderColor: '#22c55e',
      borderWidth: 2,
      fill: false,
      tension: 0.25,
    });
  }
  const chart = new Chart(ctx, {
    type: type === 'line' ? 'line' : type,
    data: { labels, datasets },
    options: {
      animation: false,
      responsive: false,
      plugins: {
        legend: { display: type === 'pie' || type === 'doughnut' || secondValues, position: 'bottom' },
      },
      scales: (type === 'bar' || type === 'line') ? {
        y: { beginAtZero: true },
      } : undefined,
    },
  });
  await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
  chart.update();
  await new Promise(r => setTimeout(r, 80));
  const url = canvas.toDataURL('image/png', 1);
  chart.destroy();
  return url;
}

function monthCompareRows (analytics) {
  return (analytics.monthCompare || []).map(m => [
    m.label,
    m.count,
    m.monto,
    m.pagadoCount,
    m.pagadoMonto,
    m.prev ? m.prev.label : '—',
    deltaNum(m.count, m.prev?.count),
    m.pctCountVsPrev,
    deltaNum(Math.round(m.monto), m.prev ? Math.round(m.prev.monto) : null),
    m.pctMontoVsPrev,
    m.yoy ? m.yoy.label : '—',
    m.yoy ? deltaNum(m.count, m.yoy.count) : '—',
    m.pctCountVsYoy,
    m.yoy ? deltaNum(Math.round(m.monto), Math.round(m.yoy.monto)) : '—',
    m.pctMontoVsYoy,
  ]);
}

function yearCompareRows (analytics) {
  return (analytics.yearCompare || []).map(y => [
    y.year,
    y.count,
    y.monto,
    y.pagadoCount,
    y.pagadoMonto,
    y.prev ? y.prev.year : '—',
    deltaNum(y.count, y.prev?.count),
    y.pctCountVsPrev,
    deltaNum(Math.round(y.monto), y.prev ? Math.round(y.prev.monto) : null),
    y.pctMontoVsPrev,
  ]);
}

function exportPaymentsCsv (pagos, filterLabel = 'todos') {
  const analytics = buildPaymentsExportAnalytics(pagos);
  const tag = filterLabel ? String(filterLabel).replace(/\s+/g, '-').toLowerCase() : 'todos';
  const rows = [
    ['ID alumno', 'Alumno', 'Mes', 'Concepto', 'Base', 'Recargo', 'Total', 'Fecha límite', 'Fecha pago', 'Estado', 'Método'],
    ...pagos.map(p => [p.id, p.fullName, p.mes, p.concepto, p.montoBase, p.recargo || 0, p.monto, p.lim, p.pago, p.estado, p.metodo]),
    [],
    ['COMPARATIVA POR MES'],
    ['Mes', 'Registros', 'Monto', 'Pagados (cant)', 'Recaudado pagado', 'Mes anterior', 'Δ registros', '% reg', 'Δ monto', '% monto', 'Mismo mes año ant.', 'Δ reg YoY', '% reg YoY', 'Δ monto YoY', '% monto YoY'],
    ...monthCompareRows(analytics),
    [],
    ['COMPARATIVA POR AÑO'],
    ['Año', 'Registros', 'Monto', 'Pagados', 'Recaudado', 'Año anterior', 'Δ registros', '% reg', 'Δ monto', '% monto'],
    ...yearCompareRows(analytics),
  ];
  downloadCsv(`pagos-tecos-${tag}-${new Date().toISOString().slice(0, 10)}.csv`, rows);
}

async function exportPaymentsExcelWithCharts (pagos, filterLabel, analytics, theme) {
  const primary = theme?.primary || '#2563eb';
  const wb = new ExcelJS.Workbook();
  wb.creator = 'Tecos Elite VOLLEYBALL';
  wb.created = new Date();

  const wsPagos = wb.addWorksheet('Pagos');
  wsPagos.addRow([
    'ID alumno', 'Alumno', 'Tutor', 'Teléfono', 'Mes', 'Concepto',
    'Base MXN', 'Recargo MXN', 'Total MXN', 'Fecha límite', 'Fecha pago', 'Estado', 'Método', 'Días mora',
  ]);
  paymentsDetailRows(pagos).forEach(r => wsPagos.addRow(r));
  wsPagos.getRow(1).font = { bold: true };

  const wsResumen = wb.addWorksheet('Resumen');
  wsResumen.getColumn(1).width = 28;
  wsResumen.getColumn(2).width = 22;
  let resRow = 1;
  if (typeof loadTecosLogoDataUrl === 'function') {
    try {
      const logoData = await loadTecosLogoDataUrl();
      if (logoData && logoData.includes(',')) {
        const ext = logoData.includes('image/png') ? 'png' : 'jpeg';
        const logoId = wb.addImage({
          base64: logoData.slice(logoData.indexOf(',') + 1),
          extension: ext,
        });
        wsResumen.addImage(logoId, { tl: { col: 0, row: 0 }, ext: { width: 100, height: 48 } });
        resRow = 4;
      }
    } catch (e) { console.warn('[Tecos] logo Excel pagos', e); }
  }
  [
    ['TECOS ELITE — Reporte de ventas y pagos'],
    ['Filtro', filterLabel],
    ['Generado', new Date().toLocaleString('es-MX')],
    ['Total registros', analytics.total],
    ['Alumnos', analytics.alumnos],
    ['Monto base', analytics.totalBase],
    ['Recargos', analytics.totalRecargo],
    ['Monto total (lista)', analytics.totalMonto],
    [],
    ['Estado', 'Cantidad', '%'],
  ].forEach((r, i) => {
    const row = wsResumen.getRow(resRow + i);
    r.forEach((val, ci) => { row.getCell(ci + 1).value = val; });
  });
  Object.keys(EXPORT_ESTADO_LABELS).forEach((k) => {
    const n = analytics.byEstado[k] || 0;
    const pct = analytics.total ? ((n / analytics.total) * 100).toFixed(1) : '0';
    wsResumen.addRow([EXPORT_ESTADO_LABELS[k], n, pct]);
  });

  const wsMes = wb.addWorksheet('Comparativa por mes');
  wsMes.addRow([
    'Mes', 'Registros', 'Monto total MXN', 'Pagados (cant)', 'Recaudado pagado MXN',
    'Mes anterior', 'Δ registros vs ant.', '% registros', 'Δ monto vs ant.', '% monto',
    'Mismo mes año anterior', 'Δ reg vs año ant.', '% reg YoY', 'Δ monto YoY', '% monto YoY',
  ]);
  monthCompareRows(analytics).forEach(r => wsMes.addRow(r));
  wsMes.getRow(1).font = { bold: true };

  const wsAnio = wb.addWorksheet('Comparativa por año');
  wsAnio.addRow([
    'Año', 'Registros', 'Monto total MXN', 'Pagados (cant)', 'Recaudado pagado MXN',
    'Año anterior', 'Δ registros', '% registros', 'Δ monto', '% monto',
  ]);
  yearCompareRows(analytics).forEach(r => wsAnio.addRow(r));
  wsAnio.getRow(1).font = { bold: true };

  const wsGraf = wb.addWorksheet('Gráficas');
  wsGraf.getColumn(1).width = 4;
  let rowCursor = 1;

  const estadoKeys = Object.keys(EXPORT_ESTADO_LABELS).filter(k => (analytics.byEstado[k] || 0) > 0);
  const monthLabels = analytics.monthsSorted.map(m => m.label);
  const monthCounts = analytics.monthsSorted.map(m => m.count);
  const monthMontos = analytics.monthsSorted.map(m => m.monto);
  const yearLabels = analytics.yearsSorted.map(y => String(y.year));
  const yearMontos = analytics.yearsSorted.map(y => y.monto);

  const charts = await Promise.all([
    estadoKeys.length ? renderChartPng({
      type: 'doughnut',
      labels: estadoKeys.map(k => EXPORT_ESTADO_LABELS[k]),
      values: estadoKeys.map(k => analytics.byEstado[k]),
      colors: estadoKeys.map(k => EXPORT_ESTADO_COLORS[k]),
      width: 480,
      height: 280,
    }) : null,
    monthLabels.length ? renderChartPng({
      type: 'bar',
      labels: monthLabels,
      values: monthCounts,
      colors: monthLabels.map(() => primary + '99'),
      width: 640,
      height: 300,
      datasetLabel: 'Registros por mes',
    }) : null,
    monthLabels.length ? renderChartPng({
      type: 'line',
      labels: monthLabels,
      values: monthMontos,
      colors: [primary],
      width: 640,
      height: 300,
      datasetLabel: 'Monto MXN',
    }) : null,
    yearLabels.length ? renderChartPng({
      type: 'bar',
      labels: yearLabels,
      values: yearMontos,
      colors: yearLabels.map(() => '#22c55e99'),
      width: 480,
      height: 280,
      datasetLabel: 'Monto por año',
    }) : null,
  ]);

  const chartTitles = [
    'Distribución por estado',
    'Registros por mes (cronológico)',
    'Monto recaudado por mes',
    'Comparativa por año',
  ];

  for (let i = 0; i < charts.length; i++) {
    const png = charts[i];
    if (!png) continue;
    wsGraf.getCell(rowCursor, 1).value = chartTitles[i];
    wsGraf.getCell(rowCursor, 1).font = { bold: true, size: 12 };
    rowCursor += 1;
    const imgId = wb.addImage({
      base64: chartPngBase64(png),
      extension: 'png',
    });
    wsGraf.addImage(imgId, {
      tl: { col: 0, row: rowCursor - 1 },
      ext: { width: 520, height: 260 },
    });
    rowCursor += 16;
  }

  const wsDatos = wb.addWorksheet('Datos gráficas');
  wsDatos.addRow(['Por estado', 'Cantidad']);
  estadoKeys.forEach(k => wsDatos.addRow([EXPORT_ESTADO_LABELS[k], analytics.byEstado[k]]));
  wsDatos.addRow([]);
  wsDatos.addRow(['Por mes', 'Registros', 'Monto MXN', 'Pagado MXN']);
  analytics.monthsSorted.forEach(m => wsDatos.addRow([m.label, m.count, m.monto, m.pagadoMonto]));
  wsDatos.addRow([]);
  wsDatos.addRow(['Por año', 'Registros', 'Monto MXN']);
  analytics.yearsSorted.forEach(y => wsDatos.addRow([y.year, y.count, y.monto]));

  const buffer = await wb.xlsx.writeBuffer();
  const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = paymentsExportFilename('xlsx', filterLabel);
  a.click();
  URL.revokeObjectURL(url);
}

function exportPaymentsExcelLegacy (pagos, filterLabel, analytics) {
  const wb = XLSX.utils.book_new();
  const detailHeader = [
    'ID alumno', 'Alumno', 'Tutor', 'Teléfono', 'Mes', 'Concepto',
    'Base MXN', 'Recargo MXN', 'Total MXN', 'Fecha límite', 'Fecha pago', 'Estado', 'Método', 'Días mora',
  ];
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([detailHeader, ...paymentsDetailRows(pagos)]), 'Pagos');
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
    ['TECOS ELITE — Reporte de pagos'],
    ['Filtro', filterLabel],
    ['Generado', new Date().toLocaleString('es-MX')],
    ['Total', analytics.total],
    ['Alumnos', analytics.alumnos],
  ]), 'Resumen');
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
    ['Comparativa por mes'],
    ['Mes', 'Registros', 'Monto', 'Pagados', 'Recaudado', 'Mes ant.', 'Δ reg', '% reg', 'Δ $', '% $', 'Mes año ant.', 'Δ reg YoY', '% YoY', 'Δ $ YoY', '% $ YoY'],
    ...monthCompareRows(analytics),
  ]), 'Por mes');
  XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
    ['Comparativa por año'],
    ['Año', 'Registros', 'Monto', 'Pagados', 'Recaudado', 'Año ant.', 'Δ reg', '% reg', 'Δ $', '% $'],
    ...yearCompareRows(analytics),
  ]), 'Por año');
  XLSX.writeFile(wb, paymentsExportFilename('xlsx', filterLabel));
}

async function exportPaymentsExcel (pagos, filterLabel = 'todos', theme = {}) {
  if (typeof tecosEnsureExportLibs === 'function') await tecosEnsureExportLibs();
  const analytics = buildPaymentsExportAnalytics(pagos);
  if (typeof ExcelJS !== 'undefined') {
    try {
      await exportPaymentsExcelWithCharts(pagos, filterLabel, analytics, theme);
      return;
    } catch (e) {
      console.warn('[Tecos] Excel con gráficas', e);
    }
  }
  if (typeof XLSX === 'undefined') {
    exportPaymentsCsv(pagos, filterLabel);
    alert('Librería Excel no cargada; se descargó CSV con comparativas.');
    return;
  }
  exportPaymentsExcelLegacy(pagos, filterLabel, analytics);
  if (typeof ExcelJS === 'undefined') {
    alert('Excel generado con tablas comparativas. Para gráficas embebidas, recarga la página (ExcelJS).');
  }
}

async function exportPaymentsPdf (pagos, filterLabel = 'todos', theme = {}) {
  if (typeof tecosEnsureExportLibs === 'function') await tecosEnsureExportLibs();
  const t = theme || (typeof THEMES !== 'undefined' ? THEMES.light : {});
  const primary = t.primary || '#2563eb';
  const analytics = buildPaymentsExportAnalytics(pagos);
  const { jsPDF } = window.jspdf || {};
  if (!jsPDF) {
    exportPaymentsCsv(pagos, filterLabel);
    alert('Librería PDF no cargada; se descargó CSV con comparativas.');
    return;
  }

  const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
  const pageW = doc.internal.pageSize.getWidth();
  const pageH = doc.internal.pageSize.getHeight();
  const dateStr = new Date().toLocaleString('es-MX', { dateStyle: 'long', timeStyle: 'short' });
  const logoDataUrl = typeof loadTecosLogoDataUrl === 'function'
    ? await loadTecosLogoDataUrl()
    : (typeof loadReceiptImage === 'function'
      ? await loadReceiptImage(window.TECOS_LOGO_PATH || window.RECEIPT_LOGO_PATH)
      : null);

  const drawHeader = (subtitle) => {
    if (typeof drawTecosPdfReportHeader === 'function') {
      drawTecosPdfReportHeader(doc, {
        pageW,
        subtitle,
        meta: `Filtro: ${filterLabel}  ·  ${dateStr}`,
        logoDataUrl,
      });
      return;
    }
    doc.setFillColor(15, 23, 42);
    doc.rect(0, 0, pageW, 32, 'F');
    doc.setTextColor(255, 255, 255);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(20);
    doc.text('TECOS ELITE VOLLEYBALL', 14, 14);
    doc.setFontSize(10);
    doc.setFont('helvetica', 'normal');
    doc.text(subtitle, 14, 22);
    doc.text(`Filtro: ${filterLabel}  ·  ${dateStr}`, 14, 28);
  };

  drawHeader('Reporte de ventas y pagos — mensualidades');
  doc.setTextColor(30, 41, 59);
  const kpis = [
    ['Registros', String(analytics.total)],
    ['Alumnos', String(analytics.alumnos)],
    ['Monto lista', `$${analytics.totalMonto.toLocaleString('es-MX')}`],
    ['Recargos', `$${analytics.totalRecargo.toLocaleString('es-MX')}`],
  ];
  let kpiY = 38;
  kpis.forEach((k, i) => {
    const x = 14 + i * 68;
    doc.setDrawColor(226, 232, 240);
    doc.roundedRect(x, kpiY, 62, 16, 2, 2);
    doc.setFontSize(8);
    doc.setTextColor(100, 116, 139);
    doc.text(k[0], x + 4, kpiY + 6);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(11);
    doc.setTextColor(15, 23, 42);
    doc.text(k[1], x + 4, kpiY + 13);
  });

  const estadoKeys = Object.keys(EXPORT_ESTADO_LABELS).filter(k => (analytics.byEstado[k] || 0) > 0);
  const monthLabels = analytics.monthsSorted.map(m => m.label);
  const monthCounts = analytics.monthsSorted.map(m => m.count);
  const monthMontos = analytics.monthsSorted.map(m => m.monto);
  const yearLabels = analytics.yearsSorted.map(y => String(y.year));
  const yearMontos = analytics.yearsSorted.map(y => y.monto);

  let chartY = 58;
  try {
    const [pieUrl, barMesUrl, lineMontoUrl, barAnioUrl] = await Promise.all([
      estadoKeys.length ? renderChartPng({
        type: 'doughnut',
        labels: estadoKeys.map(k => EXPORT_ESTADO_LABELS[k]),
        values: estadoKeys.map(k => analytics.byEstado[k]),
        colors: estadoKeys.map(k => EXPORT_ESTADO_COLORS[k]),
      }) : null,
      monthLabels.length ? renderChartPng({
        type: 'bar', labels: monthLabels, values: monthCounts,
        colors: monthLabels.map(() => primary + '99'), width: 560, height: 260,
      }) : null,
      monthLabels.length ? renderChartPng({
        type: 'line', labels: monthLabels, values: monthMontos,
        colors: [primary], width: 560, height: 260, datasetLabel: 'Monto MXN',
      }) : null,
      yearLabels.length ? renderChartPng({
        type: 'bar', labels: yearLabels, values: yearMontos,
        colors: yearLabels.map(() => '#22c55e99'), width: 400, height: 240,
        datasetLabel: 'Por año',
      }) : null,
    ]);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(10);
    doc.setTextColor(15, 23, 42);
    if (pieUrl) {
      doc.text('Por estado', 14, chartY);
      doc.addImage(pieUrl, 'PNG', 14, chartY + 2, 72, 42);
    }
    if (barMesUrl) {
      doc.text('Registros por mes', 92, chartY);
      doc.addImage(barMesUrl, 'PNG', 92, chartY + 2, pageW - 106, 42);
    }
    chartY += 48;
    if (lineMontoUrl) {
      doc.text('Monto por mes (vs meses anteriores en serie)', 14, chartY);
      doc.addImage(lineMontoUrl, 'PNG', 14, chartY + 2, (pageW - 28) / 2, 40);
    }
    if (barAnioUrl) {
      doc.text('Por año', pageW / 2 + 4, chartY);
      doc.addImage(barAnioUrl, 'PNG', pageW / 2 + 4, chartY + 2, (pageW - 28) / 2 - 8, 40);
    }
    if (pieUrl || barMesUrl || lineMontoUrl || barAnioUrl) chartY += 46;
  } catch (e) {
    console.warn('[Tecos] PDF charts', e);
  }

  doc.autoTable({
    startY: chartY,
    head: [['Mes', 'Reg.', 'Monto', 'Δ vs ant.', '%', 'Δ vs año ant.', '% YoY']],
    body: analytics.monthCompare.slice(0, 14).map(m => [
      m.label,
      m.count,
      `$${Math.round(m.monto).toLocaleString('es-MX')}`,
      m.prev ? deltaNum(m.count, m.prev.count) : '—',
      m.pctCountVsPrev,
      m.yoy ? deltaNum(m.count, m.yoy.count) : '—',
      m.pctCountVsYoy,
    ]),
    styles: { fontSize: 7, cellPadding: 1.5 },
    headStyles: { fillColor: [37, 99, 235], textColor: 255 },
    margin: { left: 14, right: 14 },
    theme: 'grid',
  });

  doc.addPage();
  drawHeader('Comparativa por mes y por año');
  let y2 = 38;
  doc.setFont('helvetica', 'bold');
  doc.setFontSize(11);
  doc.text('Detalle por año', 14, y2);
  y2 += 4;
  doc.autoTable({
    startY: y2,
    head: [['Año', 'Registros', 'Monto', 'Recaudado pagado', 'Δ vs año ant.', '% monto']],
    body: analytics.yearCompare.map(y => [
      y.year,
      y.count,
      `$${Math.round(y.monto).toLocaleString('es-MX')}`,
      `$${Math.round(y.pagadoMonto).toLocaleString('es-MX')}`,
      y.prev ? deltaNum(Math.round(y.monto), Math.round(y.prev.monto)) : '—',
      y.pctMontoVsPrev,
    ]),
    styles: { fontSize: 8 },
    headStyles: { fillColor: [37, 99, 235], textColor: 255 },
    margin: { left: 14, right: 14 },
  });

  y2 = (doc.lastAutoTable?.finalY || y2) + 10;
  doc.autoTable({
    startY: y2,
    head: [['Alumno', 'ID', 'Mes', 'Concepto', 'Total', 'Estado']],
    body: (pagos || []).slice(0, 400).map(p => [
      p.fullName,
      p.id,
      p.mes,
      p.concepto,
      `$${Number(p.monto).toLocaleString('es-MX')}`,
      EXPORT_ESTADO_LABELS[p.estado] || p.estado,
    ]),
    styles: { fontSize: 7, cellPadding: 1.5 },
    headStyles: { fillColor: [37, 99, 235], textColor: 255 },
    alternateRowStyles: { fillColor: [248, 250, 252] },
    margin: { left: 14, right: 14 },
  });

  if ((pagos || []).length > 400) {
    const fy = doc.lastAutoTable.finalY;
    doc.setFontSize(8);
    doc.setTextColor(100, 116, 139);
    doc.text(`Listado: 400 de ${pagos.length}. Usa Excel para el detalle completo.`, 14, fy + 6);
  }

  doc.save(paymentsExportFilename('pdf', filterLabel));
}

async function ensureAllStudentsBillingCycles () {
  const students = await fetchStudentsAdmin();
  const settings = await fetchAcademySettings();
  let created = 0;
  for (const s of students) {
    if (isDemoStudentCode(s.id)) continue;
    const n = await ensureStudentCurrentMonthPayment(s._uuid, { settings });
    created += n;
  }
  notifyPaymentsChanged();
  return { students: students.length, paymentsCreated: created };
}

Object.assign(window, {
  renderChartPng,
  buildPaymentsExportSummary,
  buildPaymentsExportAnalytics,
  exportPaymentsCsv,
  exportPaymentsExcel,
  exportPaymentsPdf,
  ensureAllStudentsBillingCycles,
});
