window.timelineGenerator = {
  generate(items, cfg, template) {
    const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18;

    // Determine time window from earliest due date
    const sorted = [...items].sort((a, b) => a.due - b.due);
    const minDue = sorted[0].due;
    const start = new Date(minDue.getFullYear(), minDue.getMonth(), 1);
    const end = new Date(start);
    end.setMonth(end.getMonth() + monthsRange);

    // Build months array
    const months = [];
    const cursor = new Date(start);
    while (cursor <= end) {
      months.push(new Date(cursor));
      cursor.setMonth(cursor.getMonth() + 1);
    }

    // Group items by lane
    const laneMap = new Map();
    for (const it of items) {
      const laneName = it.lane || "Ohne Epic";
      if (!laneMap.has(laneName)) laneMap.set(laneName, []);
      laneMap.get(laneName).push(it);
    }

    const laneNames = Array.from(laneMap.keys()).sort((a, b) =>
      a.localeCompare(b, "de")
    );

    // Layout constants
    const left = 220;
    const top = 140;
    const monthWidth = 120;
    const laneHeight = 80;
    const laneGap = 16;

    // Month grid (labels + vertical lines)
    let monthGraphics = "";
    const monthLabelY = 90;
    const gridTop = top - 20;
    const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40;

    months.forEach((m, i) => {
      const x = left + i * monthWidth;
      const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" });
      monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#E3E8EF" />`;
      monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="#5C6B7A" font-size="12">${label}</text>`;
    });

    // Helper to compute month index
    function monthIndexForDate(d) {
      return (d.getFullYear() - start.getFullYear()) * 12
           + (d.getMonth() - start.getMonth());
    }

    // Lane blocks
    let laneBlocks = "";
    laneNames.forEach((laneName, laneIdx) => {
      const laneY = top + laneIdx * (laneHeight + laneGap);

      // Background band for lane
      laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />`;
      // Lane label
      laneBlocks += `<text x="56" y="${laneY - 4}" fill="#0B1F3B" font-size="14" font-weight="600">${this.escapeXml(laneName)}</text>`;

      const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due);
      laneItems.forEach((it, idx) => {
        const mi = monthIndexForDate(it.due);
        const clampedMi = Math.max(0, Math.min(months.length - 1, mi));
        const cx = left + clampedMi * monthWidth + monthWidth * 0.5;
        const cy = laneY + 10 + idx * 18;

        laneBlocks += `<circle cx="${cx}" cy="${cy}" r="5" fill="#0A4D8C" />`;
        laneBlocks += `<text x="${cx + 10}" y="${cy + 4}" font-size="12" fill="#0B1F3B">
                          <tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
                          <tspan class="item-title">${this.escapeXml(it.title || "")}</tspan>
                       </text>`;
      });
    });

    if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) {
      return template
        .replace("{{MONTHS}}", monthGraphics)
        .replace("{{LANES}}", laneBlocks);
    }

    // Fallback: embed directly in simple SVG
    const height = top + laneNames.length * (laneHeight + laneGap) + 80;
    const width = left + months.length * monthWidth + 100;

    return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
      <rect width="100%" height="100%" fill="#FFFFFF" />
      ${monthGraphics}
      ${laneBlocks}
    </svg>`;
  },

  escapeXml(str) {
    return String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }
};
