window.timelineGenerator = {
  generate(items, cfg, template) {
    // Validate template is provided
    if (!template) {
      throw new Error('Template is required. Please provide a template-v2.svg file with template elements.');
    }

    // Validate template has required elements
    this.validateTemplate(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;

    return this.generateFromTemplates(items, cfg, template, {
      months, start, laneMap, laneNames,
      left, top, monthWidth, laneHeight, laneGap
    });
  },

  // Validate template has required elements
  validateTemplate(template) {
    const hasMonthTemplate = template.includes('id="month-template"');
    const hasLaneTemplate = template.includes('id="lane-template"');
    const hasItemTemplate = template.includes('id="item-template"');

    if (!hasMonthTemplate || !hasLaneTemplate || !hasItemTemplate) {
      const missing = [];
      if (!hasMonthTemplate) missing.push('month-template');
      if (!hasLaneTemplate) missing.push('lane-template');
      if (!hasItemTemplate) missing.push('item-template');

      throw new Error(
        `Template is missing required elements: ${missing.join(', ')}. ` +
        `Please use a template-v2.svg file with proper template elements in the <defs> section.`
      );
    }
  },

  // Extract a template element from SVG
  extractTemplate(template, id) {
    const regex = new RegExp(`<g id="${id}"[^>]*>([\\s\\S]*?)</g>`, 'i');
    const match = template.match(regex);

    if (!match) {
      throw new Error(`Failed to extract template element: ${id}`);
    }

    return match[0];
  },

  // Replace placeholders in template string
  replacePlaceholders(template, values) {
    let result = template;
    for (const [key, value] of Object.entries(values)) {
      result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
    }
    return result;
  },

  // Generate SVG using template-based approach
  generateFromTemplates(items, cfg, template, layout) {
    const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;

    // Extract template elements (will throw if not found)
    const monthTemplate = this.extractTemplate(template, 'month-template');
    const laneTemplate = this.extractTemplate(template, 'lane-template');
    const itemTemplate = this.extractTemplate(template, 'item-template');

    const monthLabelY = 90;
    const gridTop = top - 20;
    const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40;

    // Generate months using template
    let monthGraphics = "";
    months.forEach((m, i) => {
      const x = left + i * monthWidth;
      const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" });

      const monthValues = {
        MONTH_X: x,
        GRID_TOP: gridTop,
        GRID_BOTTOM: gridBottom,
        MONTH_X_OFFSET: x - 30,
        MONTH_LABEL_Y_OFFSET: monthLabelY - 20,
        MONTH_TEXT_X: x + 4,
        MONTH_LABEL_Y: monthLabelY,
        MONTH_LABEL: label,
        MONTH_SEP_X: x - 1,
        GRID_HEIGHT: gridBottom - gridTop
      };

      let monthElement = this.replacePlaceholders(monthTemplate, monthValues);
      // Remove display:none and id to make element visible
      monthElement = monthElement.replace(/style="display:\s*none;?"/, '');
      monthElement = monthElement.replace(/id="month-template"/, '');
      monthGraphics += monthElement;
    });

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

    // Generate lanes using template
    let laneBlocks = "";
    laneNames.forEach((laneName, laneIdx) => {
      const laneY = top + laneIdx * (laneHeight + laneGap);
      const laneWidth = left + months.length * monthWidth;

      const laneValues = {
        LANE_X: 40,
        LANE_Y: laneY - 24,
        LANE_WIDTH: laneWidth,
        LANE_HEIGHT: laneHeight,
        LABEL_X: 56,
        LABEL_Y: laneY - 4,
        LANE_NAME: this.escapeXml(laneName)
      };

      let laneElement = this.replacePlaceholders(laneTemplate, laneValues);
      laneElement = laneElement.replace(/style="display:\s*none;?"/, '');
      laneElement = laneElement.replace(/id="lane-template"/, '');
      laneBlocks += laneElement;

      // Generate items for this lane
      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;

        // Start with layout placeholders
        const itemValues = {
          ITEM_X: cx,
          ITEM_Y: cy,
          TEXT_X: cx + 12,
          TEXT_Y: cy + 4
        };

        // Dynamically add data placeholders from all item properties
        const placeholderMapping = cfg.placeholderMapping || {};
        for (const [key, value] of Object.entries(it)) {
          if (key !== 'due') { // Skip due date as it's used for positioning
            // Use custom placeholder name if defined, otherwise use convention
            const placeholderName = placeholderMapping[key] || `ITEM_${key.toUpperCase()}`;
            itemValues[placeholderName] = this.escapeXml(value || "");
          }
        }

        let itemElement = this.replacePlaceholders(itemTemplate, itemValues);
        itemElement = itemElement.replace(/style="display:\s*none;?"/, '');
        itemElement = itemElement.replace(/id="item-template"/, '');
        laneBlocks += itemElement;
      });
    });

    // Calculate dimensions
    const height = top + laneNames.length * (laneHeight + laneGap) + 80;
    const width = left + months.length * monthWidth + 100;

    // Replace placeholders and inject calculated dimensions
    let processedTemplate = template
      .replace("{{MONTHS}}", monthGraphics)
      .replace("{{LANES}}", laneBlocks);

    // Remove template elements from defs (they should not appear in final output)
    processedTemplate = processedTemplate
      .replace(/<g id="month-template"[^>]*>[\s\S]*?<\/g>/, '')
      .replace(/<g id="lane-template"[^>]*>[\s\S]*?<\/g>/, '')
      .replace(/<g id="item-template"[^>]*>[\s\S]*?<\/g>/, '');

    // Add width and height attributes to the SVG element
    processedTemplate = processedTemplate.replace(
      /<svg([^>]*?)>/,
      `<svg$1 width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
    );

    return processedTemplate;
  },

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