const { Worship, EventPlace, LiturgicalDay, WorshipLeader, Sequelize, sequelize } = require('../models');
const { Op, fn, literal } = require('sequelize');
const jwt = require('jsonwebtoken');
const { isTokenBlacklisted, addTokenToBlacklist } = require('../utils/blacklist');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
const mammoth = require('mammoth');
const pdfParse = require('pdf-parse');
const JSZip = require('jszip');
const { DOMParser } = require('@xmldom/xmldom');
const { Document, Packer, Paragraph, Table, TableRow, TableCell, TextRun, WidthType, AlignmentType, VerticalAlign, ShadingType, VerticalMerge, VerticalMergeType, FontFamily, HeadingLevel, PageMargin, SectionType, BorderStyle, HeightRule } = require('docx');

function isAuthorized(req) {
  const authHeader = req.header('Authorization');
  if (!authHeader) {
    return false;
  }
  const token = authHeader.replace('Bearer ', '');
  if (isTokenBlacklisted(token)) {
    console.log('Token is blacklisted');
    return false;
  }
  try {
    const decoded = jwt.verify(token, 'zTxVgptmPl9!_dr%xxx9999(dd)');
    req.user = decoded;
    return true;
  } catch (err) {
    // Token ist ungültig oder abgelaufen – Benutzer gilt einfach als nicht autorisiert.
    // Wichtig: Wir setzen abgelaufene/ungültige Tokens hier NICHT mehr auf die Blacklist,
    // damit ein Seiten-Reload nicht dazu führt, dass der Token als "gesperrt" behandelt wird.
    console.log('Token verification failed:', err.message);
    return false;
  }
}


exports.getAllWorships = async (req, res) => {
  try {
    const authorized = isAuthorized(req);
    const worships = await Worship.findAll({
      where: {
        date: {
          [Op.gt]: literal("DATE_SUB(NOW(), INTERVAL 4 WEEK)")
        },
      },
      attributes: authorized ? undefined : { exclude: ['sacristanService'] },
      order: [
        ['date', 'ASC'],
        ['time', 'ASC']
      ],
    });
    res.status(200).json(worships);
  } catch (error) {
    console.error('Fehler beim Abrufen der Gottesdienste:', error);
    res.status(500).json({ message: 'Fehler beim Abrufen der Gottesdienste', error: error.message });
  }
};

exports.createWorship = async (req, res) => {
  try {
    const worship = await Worship.create(req.body);
    res.status(201).json(worship);
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: 'Fehler beim Erstellen des Gottesdienstes' });
  }
};

exports.updateWorship = async (req, res) => {
  try {
    const worship = await Worship.findByPk(req.params.id);
    if (worship) {
      await worship.update(req.body);
      res.status(200).json(worship);
    } else {
      res.status(404).json({ message: 'Gottesdienst nicht gefunden' });
    }
  } catch (error) {
    res.status(500).json({ message: 'Fehler beim Aktualisieren des Gottesdienstes' });
  }
};

exports.deleteWorship = async (req, res) => {
  try {
    const worship = await Worship.findByPk(req.params.id);
    if (worship) {
      await worship.destroy();
      res.status(200).json({ message: 'Gottesdienst erfolgreich gelöscht' });
    } else {
      res.status(404).json({ message: 'Gottesdienst nicht gefunden' });
    }
  } catch (error) {
    res.status(500).json({ message: 'Fehler beim Löschen des Gottesdienstes' });
  }
};

exports.getFilteredWorships = async (req, res) => {
  const { location, order, neighborInvitation } = req.query;
  const where = {};
  if (order && order.trim() === '') {
    order = 'date DESC';
  }
  const locations = location ? JSON.parse(location) : [];
  if (location && locations.length > 0) {
      where.eventPlaceId = {
        [Sequelize.Op.in]: locations
      }
  }
  const wantsNeighborhood = String(neighborInvitation || '').toLowerCase() === 'true';
  if (wantsNeighborhood) {
    where.neighborInvitation = true;
  }
  where.date = {
    [Op.gte]: fn('CURDATE'), 
  };
  // Nur freigegebene Gottesdienste anzeigen
  where.approved = true;
  try {
    const authorized = isAuthorized(req);
    
    // Attribute: organPlaying und sacristanService nur für nicht-autorisierte Benutzer ausschließen
    const attributesExclude = [];
    if (!authorized) {
      attributesExclude.push('organPlaying', 'sacristanService');
    }
    
    const worships = await Worship.findAll({
      where,
      attributes: { exclude: attributesExclude },
      include: {
        model: EventPlace,
        as: 'eventPlace',
      },
      order: [
        ['date', 'ASC'],
        ['time', 'ASC']
      ],
    });
    res.status(200).json(worships);
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: 'Fehler beim Abrufen der gefilterten Gottesdienste' });
  }
};

exports.getWorshipOptions = async (req, res) => {
  try {
    // Alle Worships mit organizer und sacristanService abrufen
    const worships = await Worship.findAll({
      attributes: ['organizer', 'sacristanService'],
      raw: true
    });

    // Strings aufteilen (kommasepariert) und alle eindeutigen Werte sammeln
    const organizerSet = new Set();
    const sacristanSet = new Set();

    worships.forEach(worship => {
      // Organizer verarbeiten
      if (worship.organizer && worship.organizer.trim() !== '') {
        worship.organizer.split(',').forEach(org => {
          const trimmed = org.trim();
          if (trimmed) organizerSet.add(trimmed);
        });
      }

      // SacristanService verarbeiten
      if (worship.sacristanService && worship.sacristanService.trim() !== '') {
        worship.sacristanService.split(',').forEach(sac => {
          const trimmed = sac.trim();
          if (trimmed) sacristanSet.add(trimmed);
        });
      }
    });

    res.status(200).json({
      organizers: Array.from(organizerSet).sort(),
      sacristanServices: Array.from(sacristanSet).sort()
    });
  } catch (error) {
    console.error('Fehler beim Abrufen der Worship-Optionen:', error);
    res.status(500).json({ message: 'Fehler beim Abrufen der Worship-Optionen', error: error.message });
  }
};

// Multer middleware für File-Upload
exports.uploadImportFile = upload.single('file');

// Hilfsfunktion zum Parsen eines Datums aus dem Tag-String
function parseDateFromDayString(dayString) {
  // Erwartetes Format: "24.08.2025 - 1. Advent" oder "24.08.2025"
  const dateMatch = dayString.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
  if (dateMatch) {
    const day = parseInt(dateMatch[1], 10);
    const month = parseInt(dateMatch[2], 10) - 1; // Monate sind 0-indexiert
    const year = parseInt(dateMatch[3], 10);
    // Erstelle Date-Objekt mit UTC, um Zeitzonenprobleme zu vermeiden
    const date = new Date(Date.UTC(year, month, day));
    return date;
  }
  return null;
}

// Hilfsfunktion zum Extrahieren des Tag-Namens
function extractDayName(dayString) {
  // Erwartetes Format: "24.08.2025 - 1. Advent" oder "24.08.2025\n10. Sonntag nach Trinitatis"
  // Zuerst versuche mit " - " Trennzeichen
  const parts = dayString.split(' - ');
  if (parts.length > 1) {
    return parts.slice(1).join(' - ').trim();
  }
  
  // Falls nicht gefunden, versuche mit Zeilenumbrüchen
  const lines = dayString.split('\n').map(line => line.trim()).filter(line => line.length > 0);
  if (lines.length > 1) {
    // Erste Zeile ist das Datum, weitere Zeilen sind der Tag-Name
    return lines.slice(1).join(' ').trim();
  }
  
  return '';
}

// Hilfsfunktion zum Parsen der Uhrzeit
function parseTime(timeString) {
  // Erwartetes Format: "10:00", "10.00", "10:00 Uhr" oder "10.00 Uhr"
  // Versuche zuerst mit Doppelpunkt
  let timeMatch = timeString.match(/(\d{1,2}):(\d{2})/);
  if (!timeMatch) {
    // Versuche mit Punkt
    timeMatch = timeString.match(/(\d{1,2})\.(\d{2})/);
  }
  if (timeMatch) {
    const hours = parseInt(timeMatch[1], 10);
    const minutes = parseInt(timeMatch[2], 10);
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
  }
  return null;
}

function parseGermanDateString(dateString) {
  const value = String(dateString || '').trim();
  const match = value.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
  if (!match) return null;
  const day = parseInt(match[1], 10);
  const month = parseInt(match[2], 10) - 1;
  const year = parseInt(match[3], 10);
  return new Date(Date.UTC(year, month, day));
}

function parseNbrDateCell(value) {
  const germanDate = parseGermanDateString(value);
  if (germanDate) return germanDate;

  const numericValue = Number(String(value || '').trim());
  if (!Number.isFinite(numericValue) || numericValue <= 0) return null;

  // Excel/LibreOffice serial date, including Excel's 1900 leap-year compatibility offset.
  const millisecondsPerDay = 24 * 60 * 60 * 1000;
  return new Date(Date.UTC(1899, 11, 30) + Math.round(numericValue) * millisecondsPerDay);
}

function normalizeText(value) {
  return String(value || '')
    .replace(/\u00a0/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function getXmlText(node) {
  if (!node) return '';
  let result = '';
  const collect = (current) => {
    if (current.nodeType === 3 || current.nodeType === 4) {
      result += current.nodeValue;
    }
    for (let child = current.firstChild; child; child = child.nextSibling) {
      collect(child);
    }
  };
  collect(node);
  return result;
}

function columnIndexFromCellRef(cellRef) {
  const match = String(cellRef || '').match(/[A-Z]+/i);
  if (!match) return 0;
  let index = 0;
  for (const char of match[0].toUpperCase()) {
    index = index * 26 + char.charCodeAt(0) - 64;
  }
  return index - 1;
}

function normalizeXlsxTarget(target) {
  const normalized = String(target || '').replace(/^\/+/, '');
  if (normalized.startsWith('xl/')) return normalized;
  return `xl/${normalized}`;
}

function parseCsvRecords(csvText) {
  const firstLine = String(csvText || '').split(/\r?\n/, 1)[0] || '';
  const delimiterCounts = [',', ';', '\t'].map((delimiter) => ({
    delimiter,
    count: firstLine.split(delimiter).length - 1,
  }));
  const delimiter = delimiterCounts.sort((a, b) => b.count - a.count)[0].delimiter;
  const records = [];
  let row = [];
  let value = '';
  let inQuotes = false;

  for (let index = 0; index < csvText.length; index++) {
    const char = csvText[index];
    const nextChar = csvText[index + 1];

    if (char === '"') {
      if (inQuotes && nextChar === '"') {
        value += '"';
        index++;
      } else {
        inQuotes = !inQuotes;
      }
      continue;
    }

    if (!inQuotes && char === delimiter) {
      row.push(value);
      value = '';
      continue;
    }

    if (!inQuotes && (char === '\n' || char === '\r')) {
      if (char === '\r' && nextChar === '\n') index++;
      row.push(value);
      records.push(row);
      row = [];
      value = '';
      continue;
    }

    value += char;
  }

  row.push(value);
  if (row.some((cell) => cell !== '') || records.length === 0) {
    records.push(row);
  }
  return records;
}

async function parseXlsxToRecords(buffer) {
  const zip = await JSZip.loadAsync(buffer);
  const parser = new DOMParser();

  const sharedStringsFile = zip.file('xl/sharedStrings.xml');
  let sharedStrings = [];
  if (sharedStringsFile) {
    const sharedDoc = parser.parseFromString(await sharedStringsFile.async('string'), 'text/xml');
    sharedStrings = Array.from(sharedDoc.getElementsByTagName('si')).map(getXmlText);
  }

  let worksheetPath = 'xl/worksheets/sheet1.xml';
  const relationshipsFile = zip.file('xl/_rels/workbook.xml.rels');
  if (relationshipsFile) {
    const relationshipsDoc = parser.parseFromString(await relationshipsFile.async('string'), 'text/xml');
    const firstSheetRel = Array.from(relationshipsDoc.getElementsByTagName('Relationship'))
      .find((relationship) => /\/worksheet$/i.test(relationship.getAttribute('Type') || ''));
    if (firstSheetRel?.getAttribute('Target')) {
      worksheetPath = normalizeXlsxTarget(firstSheetRel.getAttribute('Target'));
    }
  }

  const worksheetFile = zip.file(worksheetPath);
  if (!worksheetFile) {
    throw new Error('Kein Arbeitsblatt in der XLSX-Datei gefunden.');
  }

  const worksheetDoc = parser.parseFromString(await worksheetFile.async('string'), 'text/xml');
  return Array.from(worksheetDoc.getElementsByTagName('row')).map((row) => {
    const values = [];
    for (const cell of Array.from(row.getElementsByTagName('c'))) {
      const cellRef = cell.getAttribute('r');
      const type = cell.getAttribute('t');
      const valueNode = cell.getElementsByTagName('v')[0];
      const inlineStringNode = cell.getElementsByTagName('is')[0];
      let value = '';

      if (type === 's') {
        value = sharedStrings[Number(getXmlText(valueNode))] || '';
      } else if (type === 'inlineStr') {
        value = getXmlText(inlineStringNode);
      } else {
        value = getXmlText(valueNode);
      }

      values[columnIndexFromCellRef(cellRef)] = value;
    }
    return values;
  });
}

function buildLeaderMaps(leaders) {
  const codeToName = new Map();
  const normalizedToName = new Map();
  for (const leader of leaders || []) {
    if (!leader?.active) continue;
    const code = normalizeText(leader.code);
    const name = normalizeText(leader.name);
    if (code) {
      codeToName.set(code, name);
      normalizedToName.set(code.toLowerCase(), name);
    }
    const aliases = String(leader.aliases || '')
      .split(',')
      .map((x) => normalizeText(x))
      .filter(Boolean);
    for (const alias of aliases) {
      normalizedToName.set(alias.toLowerCase(), name);
    }
  }
  return { codeToName, normalizedToName };
}

function resolveEventPlaceIdFromHeader(eventPlaces, headerCell) {
  const raw = normalizeText(headerCell);
  if (!raw) return null;
  const nameOnly = normalizeText(raw.split('(')[0]);
  const normalized = nameOnly.toLowerCase();

  // Prefer exact name match.
  const exact = (eventPlaces || []).find((p) => normalizeText(p.name).toLowerCase() === normalized);
  if (exact) return exact.id;

  // Fallback: contains.
  const contains = (eventPlaces || []).find((p) => normalizeText(p.name).toLowerCase().includes(normalized));
  if (contains) return contains.id;

  // Hardcoded fallbacks for known CSV headers.
  if (/am b[üu]gel/i.test(raw)) return 12;
  if (/bonames/i.test(raw)) return 7;
  if (/kalbach/i.test(raw)) return 2;
  if (/nieder-eschbach/i.test(raw)) return 14;
  if (/harheim/i.test(raw)) return 15;
  if (/nieder-erlenbach/i.test(raw)) return 13;
  return null;
}

function splitNbrCellToSegments(cellText) {
  const text = normalizeText(cellText);
  if (!text) return [];
  // Often multiple items are separated by newlines or commas; keep it conservative.
  return text
    .split(/\n+|,\s*(?=(?:[A-ZÄÖÜa-zäöü]{1,3}\.)?\s*\d{1,2}[:.]\d{2}|Sa\.)/g)
    .map((s) => normalizeText(s))
    .filter(Boolean);
}

function parseNbrSegment(segment, baseDateUtc, leaderNormalizedMap) {
  const raw = normalizeText(segment);
  if (!raw) return null;

  let dateUtc = baseDateUtc;
  let text = raw;
  if (/^(sa|samstag)\.?/i.test(text)) {
    const d = new Date(dateUtc.getTime());
    d.setUTCDate(d.getUTCDate() - 1);
    dateUtc = d;
    text = normalizeText(text.replace(/^(sa|samstag)\.?\s*/i, ''));
  }

  // Time: allow 10.30, 10:30, 10 h, 10h, 10.30 Uhr, etc.
  let time = null;
  const timeMatch = text.match(/(\d{1,2})\s*(?:[:.]\s*(\d{2})|h)(?=\D|$)/i);
  if (timeMatch) {
    const hours = parseInt(timeMatch[1], 10);
    const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0;
    time = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
    text = normalizeText(text.replace(timeMatch[0], ''));
  }

  // Officiant: pick the last token that matches a configured leader code/alias.
  let officiant = '';
  const tokens = text.split(' ').map((t) => t.trim()).filter(Boolean);
  for (let i = tokens.length - 1; i >= 0; i--) {
    const token = tokens[i].replace(/[()]/g, '');
    const resolved = leaderNormalizedMap.get(token.toLowerCase());
    if (resolved) {
      officiant = resolved;
      tokens.splice(i, 1);
      break;
    }
  }
  const title = tokens.join(' ').trim() || 'Gottesdienst';

  return { dateUtc, time, title, officiant };
}

// Hilfsfunktion zum Parsen eines Gottesdienstes aus der zweiten Spalte
function parseWorshipFromCell(cellText, date, dayName) {
  // Zuerst in Zeilen aufteilen (falls Zeilenumbrüche vorhanden)
  const lines = cellText.split('\n').map(line => line.trim()).filter(line => line.length > 0);
  
  if (lines.length === 0) {
    return null;
  }
  
  // Für Debugging: Zeige die Zeilen
  console.log(`    parseWorshipFromCell: ${lines.length} Zeilen gefunden`);
  lines.forEach((line, idx) => {
    console.log(`      Zeile ${idx + 1}: "${line.substring(0, 100)}${line.length > 100 ? '...' : ''}"`);
  });
  
  const fullText = cellText.trim();
  const hasNeighborInvitation = /einladung zum gottesdienst im nachbarschaftsraum/i.test(fullText) || /\[\[FLAG_NEIGHBOR_INVITATION\]\]/.test(fullText);
  const hasSelfInformation = /bitte informieren sie sich auch auf den internetseiten/i.test(fullText) || /\[\[FLAG_SELF_INFORMATION\]\]/.test(fullText);
  
  // Wenn Zeilenumbrüche vorhanden sind, verwende die zeilenbasierte Logik
  if (lines.length > 1) {
    return parseWorshipFromCellWithLines(lines, date, dayName);
  }

  const worship = {
    date: date,
    dayName: dayName,
    time: null,
    title: '',
    organizer: '',
    sacristanService: '',
    collection: '',
    organPlaying: '',
    eventPlaceId: null,
    address: '',
    selfInformation: false,
    highlightTime: false,
    neighborInvitation: false,
    introLine: ''
  };
  worship.neighborInvitation = hasNeighborInvitation;
  worship.selfInformation = hasSelfInformation;

  console.log(`    parseWorshipFromCell: Volltext: "${fullText.substring(0, 200)}..."`);

  // Suche nach Uhrzeit am Anfang (Format: "11.15 Uhr" oder "11:15 Uhr")
  const timeMatch = fullText.match(/^(\d{1,2})[:.](\d{2})\s*Uhr/i);
  if (timeMatch) {
    const hours = parseInt(timeMatch[1], 10);
    const minutes = parseInt(timeMatch[2], 10);
    worship.time = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
    console.log(`    parseWorshipFromCell: Uhrzeit geparst: ${worship.time}`);
    
    // Text nach der Uhrzeit extrahieren
    const textAfterTime = fullText.substring(timeMatch[0].length).trim();
    
    // Titel extrahieren: Alles bis zum ersten "Gestaltung:", "Dienst:", "Kollekte:" oder "Orgel:"
    const titleEndMatch = textAfterTime.match(/(Gestaltung|Dienst|Kollekte|Orgel|Bitte informieren):/i);
    if (titleEndMatch) {
      let title = textAfterTime.substring(0, titleEndMatch.index).trim();
      // Entferne häufige Wörter am Anfang
      title = title.replace(/^(Gottesdienst|Gemeinsamer Gottesdienst|Einladung zum Gottesdienst)\s*/i, '');
      // Entferne "in", "am", "zu" + Ort am Ende, wenn vorhanden (aber behalte den Rest)
      title = title.replace(/\s+(in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)$/, '');
      title = title
        .replace(/\[\[FLAG_NEIGHBOR_INVITATION\]\]/g, '')
        .replace(/\[\[FLAG_SELF_INFORMATION\]\]/g, '')
        .replace(/bitte informieren sie sich auch auf den internetseiten.*$/i, '')
        .replace(/\|/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
      worship.title = title || 'Gottesdienst';
    } else {
      // Falls keine Markierungen gefunden, nimm den gesamten Text als Titel
      let title = textAfterTime;
      title = title.replace(/^(Gottesdienst|Gemeinsamer Gottesdienst|Einladung zum Gottesdienst)\s*/i, '');
      title = title
        .replace(/\[\[FLAG_NEIGHBOR_INVITATION\]\]/g, '')
        .replace(/\[\[FLAG_SELF_INFORMATION\]\]/g, '')
        .replace(/bitte informieren sie sich auch auf den internetseiten.*$/i, '')
        .replace(/\|/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
      worship.title = title.substring(0, 140) || 'Gottesdienst';
    }
    console.log(`    parseWorshipFromCell: Titel extrahiert: "${worship.title}"`);
    
    // Ort extrahieren (aus dem Titel-Bereich)
    const locationMatch = textAfterTime.match(/(?:in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)/i);
    if (locationMatch) {
      const locationName = locationMatch[1].trim();
      const normalizedLocation = locationName.split(/\s*-\s*/).map(word => 
        word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
      ).join('-');
      worship.extractedLocation = `Kirche ${normalizedLocation}`;
      console.log(`    parseWorshipFromCell: Ort extrahiert: "${worship.extractedLocation}"`);
    }
    
    // Gestalter extrahieren
    const organizerMatch = fullText.match(/Gestaltung:\s*([^DienstKollekteOrgelVideoschnitt]+?)(?=Dienst:|Kollekte:|Orgel:|Videoschnitt:|$)/i);
    if (organizerMatch) {
      worship.organizer = organizerMatch[1].trim();
      console.log(`    parseWorshipFromCell: Gestalter extrahiert: "${worship.organizer}"`);
    }
    
    // Dienst extrahieren
    const serviceMatch = fullText.match(/Dienst:\s*([^KollekteOrgelVideoschnitt]+?)(?=Kollekte:|Orgel:|Videoschnitt:|$)/i);
    if (serviceMatch) {
      worship.sacristanService = serviceMatch[1].trim();
      console.log(`    parseWorshipFromCell: Dienst extrahiert: "${worship.sacristanService}"`);
    }
    
    // Kollekte extrahieren
    const collectionMatch = fullText.match(/Kollekte:\s*([^OrgelVideoschnitt]+?)(?=Orgel:|Videoschnitt:|$)/i);
    if (collectionMatch) {
      let collection = collectionMatch[1].trim();
      // Falls am Ende eine Nummer in Klammern steht (z.B. "für XY (12345)"), diese entfernen
      collection = collection.replace(/\s*\(\d+\)\s*$/, '').trim();
      worship.collection = collection;
      console.log(`    parseWorshipFromCell: Kollekte extrahiert: "${worship.collection}"`);
    }
    
    // Orgelspiel extrahieren
    const organMatch = fullText.match(/Orgel:\s*([^GestaltungDienstKollekteVideoschnitt]+?)(?=Gestaltung:|Dienst:|Kollekte:|Videoschnitt:|$)/i);
    if (organMatch) {
      worship.organPlaying = organMatch[1].trim();
      console.log(`    parseWorshipFromCell: Orgelspiel extrahiert: "${worship.organPlaying}"`);
    }
  } else {
    // Falls keine Uhrzeit gefunden, versuche Titel direkt zu extrahieren
    const titleMatch = fullText.match(/^(.+?)(?=Gestaltung:|Dienst:|Kollekte:|Orgel:|$)/i);
    if (titleMatch) {
      worship.title = titleMatch[1]
        .replace(/\[\[FLAG_NEIGHBOR_INVITATION\]\]/g, '')
        .replace(/\[\[FLAG_SELF_INFORMATION\]\]/g, '')
        .replace(/bitte informieren sie sich auch auf den internetseiten.*$/i, '')
        .replace(/\|/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
    } else {
      worship.title = fullText.substring(0, 100);
    }
  }

  // Nur Datum ist Pflichtfeld - alle anderen Felder sind optional
  // Das Datum wird bereits vor dem Aufruf dieser Funktion geprüft
  // Falls keine Uhrzeit gefunden wurde, setze einen Standardwert oder lasse es leer
  if (!worship.time) {
    worship.time = null; // Optional
  }
  
  // Falls kein Titel gefunden wurde, setze einen Standardwert
  if (!worship.title || worship.title.trim().length === 0) {
    worship.title = 'Gottesdienst'; // Standardtitel
  }

  console.log(`    parseWorshipFromCell: Erfolgreich - date: ${worship.date}, time: ${worship.time || 'keine'}, title: "${worship.title}"`);
  return worship;
}

// Hilfsfunktion zum Parsen eines Gottesdienstes mit Zeilenumbrüchen
function parseWorshipFromCellWithLines(lines, date, dayName) {
  const worship = {
    date: date,
    dayName: dayName,
    time: null,
    title: '',
    organizer: '',
    sacristanService: '',
    collection: '',
    organPlaying: '',
    eventPlaceId: null,
    address: '',
    selfInformation: false,
    highlightTime: false,
    neighborInvitation: false,
    introLine: ''
  };

  // Suche nach Uhrzeit in allen Zeilen (beginne mit der ersten)
  let timeFound = false;
  let timeLineIndex = -1;
  
  for (let i = 0; i < lines.length; i++) {
    const time = parseTime(lines[i]);
    if (time) {
      worship.time = time;
      timeLineIndex = i;
      timeFound = true;
      console.log(`    parseWorshipFromCellWithLines: Uhrzeit geparst: ${worship.time} (Zeile ${i + 1})`);
      break;
    }
  }
  
  if (!timeFound) {
    // Keine Uhrzeit gefunden
    return null;
  }
  
  // Titel aus der Zeile mit der Uhrzeit extrahieren
  if (timeLineIndex >= 0 && lines[timeLineIndex]) {
    let timeLine = lines[timeLineIndex];
    let title = timeLine.replace(/\d{1,2}[:.]\d{2}/, '').replace(/Uhr/gi, '').trim();
    
    // Entferne häufige Wörter am Anfang, aber behalte "Gottesdienst"
    title = title.replace(/^(Gemeinsamer|Einladung zum)\s+/i, '');
    
    // Entferne Ort am Ende (wenn vorhanden)
    title = title.replace(/\s+(?:in|am|zu)\s+[A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+$/i, '');
    
    // Falls Titel leer ist, verwende "Gottesdienst"
    worship.title = title.trim() || 'Gottesdienst';
    
    // Ort aus der Zeile mit der Uhrzeit extrahieren
    const locationMatch = timeLine.match(/(?:in|am|zu)\s+([A-ZÄÖÜ][A-ZÄÖÜa-zäöüß-]+)/i);
    if (locationMatch) {
      const locationName = locationMatch[1].trim();
      const normalizedLocation = locationName.split(/\s*-\s*/).map(word => 
        word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
      ).join('-');
      worship.extractedLocation = `Kirche ${normalizedLocation}`;
      console.log(`    parseWorshipFromCellWithLines: Ort extrahiert: "${worship.extractedLocation}"`);
    }
  }

  // Weitere Zeilen durchgehen (beginne nach der Zeile mit der Uhrzeit)
  for (let i = timeLineIndex + 1; i < lines.length; i++) {
    const line = lines[i];
    
    // Wenn eine neue Uhrzeit gefunden wird, stoppe hier (dieser Block gehört zu einem anderen Gottesdienst)
    if (parseTime(line)) {
      break;
    }
    
    if (/einladung zum gottesdienst im nachbarschaftsraum/i.test(line) || /\[\[FLAG_NEIGHBOR_INVITATION\]\]/.test(line)) {
      worship.neighborInvitation = true;
      continue;
    }
    if (/bitte informieren sie sich auch auf den internetseiten/i.test(line) || /\[\[FLAG_SELF_INFORMATION\]\]/.test(line)) {
      worship.selfInformation = true;
      continue;
    }

    // Gestalter
    if (line.toLowerCase().includes('gestaltung:')) {
      worship.organizer = line.replace(/^.*gestaltung:\s*/i, '').trim();
      console.log(`    parseWorshipFromCellWithLines: Gestalter: "${worship.organizer}"`);
    }
    // Dienst
    else if (line.toLowerCase().includes('dienst:') && !line.toLowerCase().includes('gestaltung')) {
      worship.sacristanService = line.replace(/^.*dienst:\s*/i, '').trim();
      console.log(`    parseWorshipFromCellWithLines: Dienst: "${worship.sacristanService}"`);
    }
    // Kollekte
    else if (line.toLowerCase().includes('kollekte:')) {
      let collection = line.replace(/^.*kollekte:\s*/i, '').trim();
      // Falls am Ende eine Nummer in Klammern steht (z.B. "für XY (12345)"), diese entfernen
      collection = collection.replace(/\s*\(\d+\)\s*$/, '').trim();
      worship.collection = collection;
      console.log(`    parseWorshipFromCellWithLines: Kollekte: "${worship.collection}"`);
    }
    // Orgelspiel
    else if (line.toLowerCase().includes('orgel:')) {
      worship.organPlaying = line.replace(/^.*orgel:\s*/i, '').trim();
      console.log(`    parseWorshipFromCellWithLines: Orgelspiel: "${worship.organPlaying}"`);
    }
    // Falls keine spezifische Markierung und Titel noch nicht vollständig
    else if (line && !worship.title.includes(line) && !line.toLowerCase().includes('videoschnitt')) {
      if (worship.title && !worship.title.includes(line)) {
        worship.title += ' ' + line;
      }
    }
  }

  worship.title = worship.title
    .replace(/\[\[FLAG_NEIGHBOR_INVITATION\]\]/g, '')
    .replace(/\[\[FLAG_SELF_INFORMATION\]\]/g, '')
    .replace(/\|/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();

  // Mindestanforderungen prüfen
  if (!worship.time || !worship.title) {
    console.log(`    parseWorshipFromCellWithLines: Fehlgeschlagen - time: ${worship.time}, title: "${worship.title}"`);
    return null;
  }

  console.log(`    parseWorshipFromCellWithLines: Erfolgreich - time: ${worship.time}, title: "${worship.title}"`);
  return worship;
}

// Import-Funktion für Gottesdienste aus .doc/.docx Dateien
exports.importWorships = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ message: 'Keine Datei hochgeladen.' });
    }

    // Validierung: Nur .doc und .docx Dateien erlauben
    const fileName = req.file.originalname.toLowerCase();
    const allowedExtensions = ['.doc', '.docx'];
    const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext));
    
    if (!isValidFile) {
      return res.status(400).json({ message: 'Nur .doc und .docx Dateien sind erlaubt.' });
    }

    // Nur .docx wird aktuell unterstützt (mammoth unterstützt nur .docx)
    if (!fileName.endsWith('.docx')) {
      return res.status(400).json({ message: 'Aktuell werden nur .docx Dateien unterstützt.' });
    }

    // .docx Datei mit mammoth parsen
    const result = await mammoth.convertToHtml({ buffer: req.file.buffer });
    const html = result.value;
    
    // Tabelle aus HTML extrahieren
    const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
    const tableMatch = tableRegex.exec(html);
    
    if (!tableMatch) {
      return res.status(400).json({ message: 'Keine Tabelle in der Datei gefunden.' });
    }

    const tableHtml = tableMatch[1];
    
    // Zeilen aus der Tabelle extrahieren
    const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
    const rows = [];
    let rowMatch;
    
    while ((rowMatch = rowRegex.exec(tableHtml)) !== null) {
      rows.push(rowMatch[1]);
    }

    const importedWorships = [];
    const errors = [];

    // Heutiges Datum (ohne Uhrzeit) zur Filterung von Vergangenheits-Terminen
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    
    console.log(`Gefundene Tabellenzeilen: ${rows.length}`);
    
    // Durch jede Zeile iterieren
    for (const row of rows) {
      // Zellen aus der Zeile extrahieren
      const cellRegex = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
      const cells = [];
      let cellMatch;
      
      while ((cellMatch = cellRegex.exec(row)) !== null) {
        // HTML-Tags entfernen und Text extrahieren, aber Zeilenumbrüche erhalten
        let cellText = cellMatch[1];
        
        // Ersetze <br> und <br/> durch Zeilenumbrüche
        cellText = cellText.replace(/<br\s*\/?>/gi, '\n');
        
        // Ersetze </p> und <p> durch Zeilenumbrüche (aber nicht doppelte)
        cellText = cellText.replace(/<\/p>/gi, '\n');
        cellText = cellText.replace(/<p[^>]*>/gi, '');
        
        // Ersetze </div> und <div> durch Zeilenumbrüche
        cellText = cellText.replace(/<\/div>/gi, '\n');
        cellText = cellText.replace(/<div[^>]*>/gi, '');
        
        // Entferne andere HTML-Tags
        cellText = cellText.replace(/<[^>]+>/g, '');
        
        // HTML-Entities ersetzen
        cellText = cellText.replace(/&nbsp;/g, ' ');
        cellText = cellText.replace(/&amp;/g, '&');
        cellText = cellText.replace(/&lt;/g, '<');
        cellText = cellText.replace(/&gt;/g, '>');
        cellText = cellText.replace(/&quot;/g, '"');
        
        // Mehrfache Zeilenumbrüche auf einen reduzieren
        cellText = cellText.replace(/\n\s*\n\s*\n+/g, '\n\n');
        
        // Trim
        cellText = cellText.trim();
        
        cells.push(cellText);
      }

      // Mindestens 2 Spalten erwartet
      if (cells.length < 2) {
        continue;
      }

      const dayString = cells[0];
      const worshipCell = cells[1];

      console.log(`Zeile ${rows.indexOf(row) + 1}: dayString="${dayString}", worshipCell="${worshipCell.substring(0, 100)}..."`);

      // Wenn Spalte 1 leer ist, Zeile überspringen
      if (!dayString || dayString.trim().length === 0) {
        console.log(`  -> Spalte 1 ist leer, Zeile wird übersprungen`);
        continue;
      }

      // Datum aus der ersten Spalte extrahieren (Pflichtfeld)
      const date = parseDateFromDayString(dayString);
      if (!date) {
        console.log(`  -> Datum konnte nicht geparst werden aus: "${dayString}"`);
        const rowNum = rows.indexOf(row) + 1;
        errors.push(`Zeile ${rowNum}: Konnte Datum nicht aus "${dayString}" extrahieren.\nVollständige Zeile:\nSpalte 1: "${dayString}"\nSpalte 2: "${worshipCell.substring(0, 200)}${worshipCell.length > 200 ? '...' : ''}"`);
        continue;
      }
      
      console.log(`  -> Datum geparst: ${date}`);

      // Gottesdienste in der Vergangenheit nicht weiter verarbeiten/anzeigen
      const worshipDate = new Date(date);
      worshipDate.setHours(0, 0, 0, 0);
      if (worshipDate < today) {
        console.log(`  -> Datum liegt in der Vergangenheit, Zeile wird übersprungen`);
        continue;
      }

      // Gottesdienst(e) aus der zweiten Spalte extrahieren
      // Hinweis: Vergangene Daten werden erst beim Speichern herausgefiltert,
      // damit sie im Dialog zur Bearbeitung angezeigt werden können
      
      // Suche nach allen Uhrzeiten im Format "XX.XX Uhr" oder "XX:XX Uhr"
      const timeMatches = [...worshipCell.matchAll(/(\d{1,2})[:.](\d{2})\s*Uhr/gi)];
      
      if (timeMatches.length === 0) {
        // Keine Uhrzeit gefunden, versuche trotzdem zu parsen (Uhrzeit ist optional)
        const worship = parseWorshipFromCell(worshipCell, date, '');
        if (worship) {
          importedWorships.push(worship);
        } else {
          // Falls Parsing komplett fehlschlägt, erstelle trotzdem einen minimalen Eintrag mit Datum
          const dateStr = date ? date.toISOString().split('T')[0] : 'unbekannt';
          const minimalWorship = {
            date: date,
            dayName: '',
            time: null,
            title: 'Gottesdienst',
            organizer: '',
            sacristanService: '',
            collection: '',
            organPlaying: '',
            eventPlaceId: null
          };
          importedWorships.push(minimalWorship);
          console.log(`  -> Minimaler Gottesdienst erstellt (nur Datum): ${dateStr}`);
        }
        continue;
      }
      
      // Teile die Zelle an jeder Uhrzeit
      const worshipBlocks = [];
      
      for (let i = 0; i < timeMatches.length; i++) {
        const match = timeMatches[i];
        const startIndex = match.index;
        const endIndex = i < timeMatches.length - 1 ? timeMatches[i + 1].index : worshipCell.length;
        const block = worshipCell.substring(startIndex, endIndex).trim();
        if (block.length > 0) {
          worshipBlocks.push(block);
        }
      }

      // Jeden Gottesdienst-Block parsen
      console.log(`  -> ${worshipBlocks.length} Gottesdienst-Blöcke gefunden`);
      for (const block of worshipBlocks) {
        console.log(`  -> Parse Block: "${block.substring(0, 100)}..."`);
        const worship = parseWorshipFromCell(block, date, '');
        if (worship) {
          console.log(`  -> Gottesdienst erfolgreich geparst: ${worship.time || 'keine Uhrzeit'} - ${worship.title}`);
          importedWorships.push(worship);
        } else {
          // Falls Parsing fehlschlägt, erstelle trotzdem einen minimalen Eintrag mit Datum
          console.log(`  -> Gottesdienst konnte nicht geparst werden, erstelle minimalen Eintrag`);
          const dateStr = date ? date.toISOString().split('T')[0] : 'unbekannt';
          const minimalWorship = {
            date: date,
            dayName: '',
            time: null,
            title: 'Gottesdienst',
            organizer: '',
            sacristanService: '',
            collection: '',
            organPlaying: '',
            eventPlaceId: null
          };
          importedWorships.push(minimalWorship);
        }
      }
    }

    console.log(`Geparste Gottesdienste: ${importedWorships.length}`);
    console.log(`Fehler beim Parsen: ${errors.length}`);

    // EventPlace- und Tag-Name-Zuordnung für alle geparsten Gottesdienste durchführen
    // Hinweis: Vergangene Daten werden erst beim Speichern herausgefiltert,
    // damit sie im Dialog zur Bearbeitung angezeigt werden können
    
    // Sammle alle Daten der geparsten Gottesdienste für Datenbankabfrage
    const datesToCheck = new Set();
    for (const worshipData of importedWorships) {
      if (worshipData.date) {
        let dateStr;
        if (worshipData.date instanceof Date) {
          const year = worshipData.date.getFullYear();
          const month = String(worshipData.date.getMonth() + 1).padStart(2, '0');
          const day = String(worshipData.date.getDate()).padStart(2, '0');
          dateStr = `${year}-${month}-${day}`;
        } else if (typeof worshipData.date === 'string') {
          dateStr = worshipData.date.split('T')[0];
        }
        if (dateStr) {
          datesToCheck.add(dateStr);
        }
      }
    }
    
    // Lade alle bestehenden Gottesdienste für die relevanten Daten
    // Verwende DATE() Funktion, um nur das Datum zu vergleichen (ohne Zeit)
    // Lade auch EventPlace-Beziehung für Vergleich
    // WICHTIG: Lade alle Felder inklusive organPlaying für Vergleich
    let existingWorships = [];
    if (datesToCheck.size > 0) {
      existingWorships = await Worship.findAll({
        where: {
          [Op.or]: Array.from(datesToCheck).map(dateStr => 
            sequelize.where(sequelize.fn('DATE', sequelize.col('date')), dateStr)
          )
        },
        // Lade alle Felder (organPlaying ist standardmäßig enthalten)
        include: {
          model: EventPlace,
          as: 'eventPlace'
        }
      });
      console.log(`  -> ${existingWorships.length} bestehende Gottesdienste geladen für Vergleich`);
    }
    
    // Hilfsfunktion: Prüft ob ein Feld mehr Daten hat
    const hasMoreData = (newValue, oldValue) => {
      const newHasData = newValue && String(newValue).trim().length > 0;
      const oldHasData = oldValue && String(oldValue).trim().length > 0;
      return newHasData && !oldHasData;
    };
    
    // Hilfsfunktion: Prüft ob ein Feld weniger Daten hat
    const hasLessData = (newValue, oldValue) => {
      const newHasData = newValue && String(newValue).trim().length > 0;
      const oldHasData = oldValue && String(oldValue).trim().length > 0;
      return !newHasData && oldHasData;
    };
    
    // Hilfsfunktion: Prüft ob ein Eintrag relevante Änderungen hat
    const hasRelevantChanges = (newWorship, existingWorship) => {
      // Wenn kein bestehender Eintrag existiert, prüfe ob der neue Eintrag relevante Daten hat
      if (!existingWorship) {
        // Prüfe ob mindestens ein Feld außer Datum, Zeit und Ort gefüllt ist
        const hasRelevantData = newWorship.title || 
                                newWorship.organizer || 
                                newWorship.sacristanService || 
                                newWorship.collection || 
                                newWorship.organPlaying;
        return !!hasRelevantData; // Nur relevant wenn mindestens ein Feld gefüllt ist
      }
      
      let hasMoreOrChanged = false;
      let hasLess = false;
      
      // Vergleiche alle relevanten Felder
      const fieldsToCompare = ['time', 'title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId'];
      
      // Hilfsfunktion zum Normalisieren von Uhrzeiten (ignoriere Sekunden)
      const normalizeTimeForComparison = (timeStr) => {
        if (!timeStr) return '';
        return String(timeStr).substring(0, 5); // Nur HH:MM
      };
      
      for (const field of fieldsToCompare) {
        let newValue = newWorship[field];
        let oldValue = existingWorship[field];
        
        // Spezielle Behandlung für Uhrzeit: ignoriere Sekunden beim Vergleich
        if (field === 'time') {
          newValue = normalizeTimeForComparison(newValue);
          oldValue = normalizeTimeForComparison(oldValue);
        }
        
        // Normalisiere Werte für Vergleich
        // Behandle null, undefined und leere Strings als gleich
        const normalizeValue = (val) => {
          if (val === null || val === undefined) return '';
          const str = String(val).trim();
          return str.length === 0 ? '' : str;
        };
        
        const newValueStr = normalizeValue(newValue);
        const oldValueStr = normalizeValue(oldValue);
        
        // Wenn beide Werte identisch sind (oder beide leer), überspringe
        if (newValueStr === oldValueStr) {
          continue;
        }
        
        // Prüfe ob neuer Wert mehr Daten hat (war leer, jetzt gefüllt)
        if (newValueStr && !oldValueStr) {
          hasMoreOrChanged = true;
          console.log(`    -> Feld "${field}": Mehr Daten (neu: "${newValueStr}", alt: leer)`);
        }
        
        // Prüfe ob neuer Wert weniger Daten hat (war gefüllt, jetzt leer)
        if (!newValueStr && oldValueStr) {
          hasLess = true;
          console.log(`    -> Feld "${field}": Weniger Daten (neu: leer, alt: "${oldValueStr}")`);
        }
        
        // Prüfe ob Werte unterschiedlich sind (beide gefüllt)
        if (newValueStr && oldValueStr && newValueStr !== oldValueStr) {
          hasMoreOrChanged = true;
          console.log(`    -> Feld "${field}": Geändert (neu: "${newValueStr}", alt: "${oldValueStr}")`);
        }
      }
      
      // Nur relevant wenn mehr/andere Daten vorhanden sind UND nicht weniger Daten
      const isRelevant = hasMoreOrChanged && !hasLess;
      console.log(`    -> hasRelevantChanges: hasMoreOrChanged=${hasMoreOrChanged}, hasLess=${hasLess}, result=${isRelevant}`);
      return isRelevant;
    };
    
    // Hilfsfunktion: Finde passenden bestehenden Eintrag
    const findExistingWorship = (worshipData) => {
      if (!worshipData.date) return null;
      
      let dateStr;
      if (worshipData.date instanceof Date) {
        const year = worshipData.date.getFullYear();
        const month = String(worshipData.date.getMonth() + 1).padStart(2, '0');
        const day = String(worshipData.date.getDate()).padStart(2, '0');
        dateStr = `${year}-${month}-${day}`;
      } else if (typeof worshipData.date === 'string') {
        dateStr = worshipData.date.split('T')[0];
      } else {
        return null;
      }
      
      // Suche nach passendem Eintrag (Datum, Uhrzeit, Ort)
      return existingWorships.find(existing => {
        // Konvertiere bestehendes Datum zu YYYY-MM-DD
        let existingDateStr;
        if (existing.date instanceof Date) {
          const year = existing.date.getFullYear();
          const month = String(existing.date.getMonth() + 1).padStart(2, '0');
          const day = String(existing.date.getDate()).padStart(2, '0');
          existingDateStr = `${year}-${month}-${day}`;
        } else if (typeof existing.date === 'string') {
          existingDateStr = existing.date.split('T')[0];
        } else {
          return false;
        }
        
        if (existingDateStr !== dateStr) return false;
        
        // Vergleiche Uhrzeit (normalisiere beide, ignoriere Sekunden)
        const normalizeTime = (timeStr) => {
          if (!timeStr) return null;
          const str = String(timeStr);
          // Extrahiere nur HH:MM (erste 5 Zeichen)
          return str.substring(0, 5);
        };
        const newTime = normalizeTime(worshipData.time);
        const existingTime = normalizeTime(existing.time);
        if (newTime !== existingTime) return false;
        
        // Vergleiche Ort
        const newPlaceId = worshipData.eventPlaceId || null;
        const existingPlaceId = existing.eventPlaceId || null;
        if (newPlaceId !== existingPlaceId) return false;
        
        return true;
      });
    };
    
    for (const worshipData of importedWorships) {
      // EventPlace zuordnen, falls ein Ort explizit extrahiert wurde
      if (worshipData.extractedLocation) {
        const eventPlace = await EventPlace.findOne({
          where: {
            name: worshipData.extractedLocation
          }
        });
        
        if (eventPlace) {
          worshipData.eventPlaceId = eventPlace.id;
        } else {
          // Falls nicht gefunden, versuche auch ohne "Kirche " Präfix
          const locationWithoutKirche = worshipData.extractedLocation.replace(/^Kirche\s+/i, '').trim();
          
          // Zuerst versuchen wir, einen Ort zu finden, der sowohl den Ortsnamen
          // als auch "Kirche" im Namen enthält (z.B. "Ev. Kirche Nieder-Erlenbach").
          // Damit wird bei mehreren Treffern (Kirche / Gemeindehaus) bevorzugt die Kirche gewählt.
          let eventPlaceAlt = await EventPlace.findOne({
            where: {
              [Op.and]: [
                { name: { [Op.like]: `%${locationWithoutKirche}%` } },
                { name: { [Op.like]: `%Kirche%` } }
              ]
            }
          });

          // Falls keine passende Kirche gefunden wurde, auf die bisherige Logik zurückfallen
          if (!eventPlaceAlt) {
            eventPlaceAlt = await EventPlace.findOne({
              where: {
                name: { [Op.like]: `%${locationWithoutKirche}%` }
              }
            });
          }

          if (eventPlaceAlt) {
            worshipData.eventPlaceId = eventPlaceAlt.id;
          }
        }
        
        // Entferne extractedLocation, da es nicht in der Datenbank gespeichert wird
        delete worshipData.extractedLocation;
      }

      // Fallback: Wenn noch kein Ort gesetzt ist, versuche den Titel als Ortsnamen zu interpretieren
      if (!worshipData.eventPlaceId && worshipData.title && typeof worshipData.title === 'string') {
        const titleTrimmed = worshipData.title.trim();

        // "Einfache" Titel als mögliche Ortsnamen zulassen:
        // - Beginn mit Großbuchstaben
        // - nur Buchstaben, Bindestriche und Leerzeichen
        const isSimpleLocationLike = /^[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*(?:\s+[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*)*$/.test(titleTrimmed);

        if (isSimpleLocationLike) {
          const normalizedLocation = titleTrimmed
            .split(/\s*-\s*/)
            .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
            .join('-');

          // 1. Versuche: exakter Name "Kirche <Ort>"
          let eventPlaceFromTitle = await EventPlace.findOne({
            where: {
              name: `Kirche ${normalizedLocation}`
            }
          });

          // 2. Versuche: Name enthält Ortsnamen und "Kirche"
          if (!eventPlaceFromTitle) {
            eventPlaceFromTitle = await EventPlace.findOne({
              where: {
                [Op.and]: [
                  { name: { [Op.like]: `%${normalizedLocation}%` } },
                  { name: { [Op.like]: `%Kirche%` } }
                ]
              }
            });
          }

          // 3. Versuche: nur der Ortsname (z.B. wenn EventPlace "Nieder-Eschbach" heißt)
          if (!eventPlaceFromTitle) {
            eventPlaceFromTitle = await EventPlace.findOne({
              where: {
                name: { [Op.like]: `%${normalizedLocation}%` }
              }
            });
          }

          if (eventPlaceFromTitle) {
            worshipData.eventPlaceId = eventPlaceFromTitle.id;
          }
        }
      }
      
      // Tag-Name aus der Datenbank basierend auf dem Datum holen
      if (worshipData.date) {
        let dateStr;
        if (worshipData.date instanceof Date) {
          // Konvertiere Date zu YYYY-MM-DD Format
          const year = worshipData.date.getFullYear();
          const month = String(worshipData.date.getMonth() + 1).padStart(2, '0');
          const day = String(worshipData.date.getDate()).padStart(2, '0');
          dateStr = `${year}-${month}-${day}`;
        } else if (typeof worshipData.date === 'string') {
          // Falls bereits String, verwende direkt (sollte YYYY-MM-DD sein)
          dateStr = worshipData.date.split('T')[0];
        } else {
          dateStr = null;
        }
        
        if (dateStr) {
          console.log(`  -> Suche liturgischen Tag für Datum: ${dateStr}`);
          const liturgicalDay = await LiturgicalDay.findOne({
            where: {
              date: dateStr
            }
          });
          
          if (liturgicalDay) {
            worshipData.dayName = liturgicalDay.dayName;
            console.log(`  -> Tag-Name gefunden: "${liturgicalDay.dayName}"`);
          } else {
            // Falls kein liturgischer Tag gefunden, leer lassen
            worshipData.dayName = '';
            console.log(`  -> Kein liturgischer Tag für Datum ${dateStr} gefunden`);
          }
        } else {
          worshipData.dayName = '';
        }
      }
    }

    // Filtere Gottesdienste: Nur die mit relevanten Änderungen behalten
    // Dies geschieht bereits im Backend, bevor Daten an das Frontend gesendet werden
    const worshipsWithChanges = [];
    let skippedCount = 0;
    
    for (const worshipData of importedWorships) {
      const existingWorship = findExistingWorship(worshipData);
      
      // Konvertiere Datum für Vergleich
      let dateStrForCompare = '';
      if (worshipData.date instanceof Date) {
        const year = worshipData.date.getFullYear();
        const month = String(worshipData.date.getMonth() + 1).padStart(2, '0');
        const day = String(worshipData.date.getDate()).padStart(2, '0');
        dateStrForCompare = `${year}-${month}-${day}`;
      } else if (typeof worshipData.date === 'string') {
        dateStrForCompare = worshipData.date.split('T')[0];
      }
      
      console.log(`  -> Prüfe Gottesdienst: Datum=${dateStrForCompare}, Zeit=${worshipData.time}, Ort=${worshipData.eventPlaceId}`);
      console.log(`    -> organPlaying im neuen: "${worshipData.organPlaying || '(leer)'}" (Typ: ${typeof worshipData.organPlaying}, Wert: ${JSON.stringify(worshipData.organPlaying)})`);
      
      if (existingWorship) {
        console.log(`    -> Bestehender Eintrag gefunden: ID=${existingWorship.id}`);
        console.log(`    -> organPlaying im bestehenden: "${existingWorship.organPlaying || '(leer)'}" (Typ: ${typeof existingWorship.organPlaying}, Wert: ${JSON.stringify(existingWorship.organPlaying)})`);
      } else {
        console.log(`    -> Kein bestehender Eintrag gefunden (neuer Eintrag)`);
        // Debug: Zeige alle bestehenden Einträge für dieses Datum
        const existingForDate = existingWorships.filter(ex => {
          let exDateStr = '';
          if (ex.date instanceof Date) {
            const year = ex.date.getFullYear();
            const month = String(ex.date.getMonth() + 1).padStart(2, '0');
            const day = String(ex.date.getDate()).padStart(2, '0');
            exDateStr = `${year}-${month}-${day}`;
          } else if (typeof ex.date === 'string') {
            exDateStr = ex.date.split('T')[0];
          }
          return exDateStr === dateStrForCompare;
        });
        console.log(`    -> Bestehende Einträge für dieses Datum: ${existingForDate.length}`);
        existingForDate.forEach(ex => {
          const exTime = ex.time ? String(ex.time).substring(0, 5) : 'keine';
          const exPlace = ex.eventPlaceId || 'kein Ort';
          console.log(`      -> ID=${ex.id}, Zeit=${exTime}, Ort=${exPlace}`);
        });
      }
      
      // Prüfe ob relevante Änderungen vorhanden sind
      if (hasRelevantChanges(worshipData, existingWorship)) {
        // Markiere als Update, falls bestehender Eintrag existiert
        if (existingWorship) {
          worshipData._isUpdate = true;
          worshipData._isNew = false;
          worshipData._existingId = existingWorship.id;
          
          // Speichere alte Werte für Vergleich
          const fieldsToCompare = ['time', 'title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId'];
          worshipData._oldValues = {};
          worshipData._changedFields = [];
          
          // Verwende die gleiche Normalisierungslogik wie in hasRelevantChanges
          const normalizeValueForComparison = (val) => {
            if (val === null || val === undefined) return '';
            const str = String(val).trim();
            return str.length === 0 ? '' : str;
          };
          
          const normalizeTimeForComparison = (timeStr) => {
            if (!timeStr) return '';
            return String(timeStr).substring(0, 5); // Nur HH:MM
          };
          
          for (const field of fieldsToCompare) {
            let newValue = worshipData[field];
            let oldValue = existingWorship[field];
            
            // Spezielle Behandlung für Uhrzeit: ignoriere Sekunden beim Vergleich
            if (field === 'time') {
              newValue = normalizeTimeForComparison(newValue);
              oldValue = normalizeTimeForComparison(oldValue);
            }
            
            // Normalisiere Werte für Vergleich (behandle null, undefined und leere Strings als gleich)
            const newValueStr = normalizeValueForComparison(newValue);
            const oldValueStr = normalizeValueForComparison(oldValue);
            
            if (newValueStr !== oldValueStr) {
              // Für Anzeige: verwende formatierte Version
              if (field === 'time' && oldValue) {
                // Zeige alte Uhrzeit ohne Sekunden
                const oldTimeStr = String(oldValue);
                worshipData._oldValues[field] = oldTimeStr.substring(0, 5) || '(leer)';
              } else {
                // Zeige alten Wert, oder "(leer)" wenn leer/null
                const displayValue = oldValue ? String(oldValue).trim() : '';
                worshipData._oldValues[field] = displayValue || '(leer)';
              }
              worshipData._changedFields.push(field);
              console.log(`      -> Feld "${field}" als geändert markiert: neu="${newValueStr}", alt="${oldValueStr}"`);
            }
          }
          
          // Speichere auch alten EventPlace-Namen, falls vorhanden
          if (existingWorship.eventPlace && existingWorship.eventPlace.name) {
            worshipData._oldValues.eventPlaceName = existingWorship.eventPlace.name;
          }
          
          console.log(`  -> Relevante Änderungen gefunden für bestehenden Eintrag ID ${existingWorship.id}: ${worshipData._changedFields.join(', ')}`);
        } else {
          // Komplett neuer Eintrag (Datum, Uhrzeit, Ort existieren noch nicht)
          worshipData._isUpdate = false;
          worshipData._isNew = true;
          worshipData._oldValues = {};
          worshipData._changedFields = [];
          console.log(`  -> Neuer Eintrag wird hinzugefügt (Datum, Uhrzeit, Ort existieren noch nicht)`);
        }
        worshipsWithChanges.push(worshipData);
      } else {
        skippedCount++;
        if (existingWorship) {
          console.log(`  -> Eintrag übersprungen (weniger oder keine relevanten Änderungen) für ID ${existingWorship.id}`);
        } else {
          console.log(`  -> Eintrag übersprungen (keine relevanten Daten)`);
        }
      }
    }
    
    console.log(`Gottesdienste mit relevanten Änderungen: ${worshipsWithChanges.length}`);
    console.log(`Übersprungene Gottesdienste: ${skippedCount}`);

    // Geparste Daten zurückgeben (ohne zu speichern)
    // Hinweis: Nur Gottesdienste mit relevanten Änderungen werden zurückgegeben
    
    // Konvertiere Date-Objekte zu YYYY-MM-DD Strings für das Frontend
    const worshipsForFrontend = worshipsWithChanges.map(w => {
      const worshipCopy = { ...w };
      
      // Konvertiere Datum zu YYYY-MM-DD Format
      if (worshipCopy.date instanceof Date) {
        const year = worshipCopy.date.getFullYear();
        const month = String(worshipCopy.date.getMonth() + 1).padStart(2, '0');
        const day = String(worshipCopy.date.getDate()).padStart(2, '0');
        worshipCopy.date = `${year}-${month}-${day}`;
      } else if (typeof worshipCopy.date === 'string') {
        // Falls bereits String, stelle sicher, dass es YYYY-MM-DD ist
        worshipCopy.date = worshipCopy.date.split('T')[0];
      }
      
      return worshipCopy;
    });
    
    res.status(200).json({ 
      message: `Datei erfolgreich geparst. ${worshipsWithChanges.length} Gottesdienste mit relevanten Änderungen gefunden (${skippedCount} übersprungen).`,
      worships: worshipsForFrontend,
      errors: errors.length > 0 ? errors : undefined
    });
  } catch (error) {
    console.error('Fehler beim Importieren der Gottesdienste:', error);
    res.status(500).json({ message: 'Fehler beim Importieren der Gottesdienste', error: error.message });
  }
};

async function parseNbrPlanningRecords(records) {
  if (!Array.isArray(records) || records.length < 3) {
    throw new Error('Datei hat zu wenig Zeilen.');
  }

  const header = records[0] || [];
  const datumCol = 1;
  const groups = [];
  for (let idx = 2; idx < header.length; idx += 3) {
    const placeHeader = header[idx];
    if (!normalizeText(placeHeader)) continue;
    groups.push({
      idx,
      placeHeader,
      musicIdx: idx + 1,
      serviceIdx: idx + 2,
    });
  }

  const eventPlaces = await EventPlace.findAll();
  const leaders = await WorshipLeader.findAll();
  const { normalizedToName } = buildLeaderMaps(leaders);

  // existing worships for change detection
  const existingWorships = await Worship.findAll({
    where: {
      date: {
        [Op.gte]: literal('DATE_SUB(CURDATE(), INTERVAL 1 DAY)'),
      },
    },
  });

  const today = new Date();
  today.setUTCHours(0, 0, 0, 0);

  const imported = [];
  const errors = [];

  const findExisting = (dateUtc, time, eventPlaceId) => {
    const y = dateUtc.getUTCFullYear();
    const m = String(dateUtc.getUTCMonth() + 1).padStart(2, '0');
    const d = String(dateUtc.getUTCDate()).padStart(2, '0');
    const dateKey = `${y}-${m}-${d}`;
    const timeKey = time ? String(time).substring(0, 5) : '';
    return existingWorships.find((w) => {
      const wDate = w.date instanceof Date ? w.date : new Date(w.date);
      const wy = wDate.getUTCFullYear();
      const wm = String(wDate.getUTCMonth() + 1).padStart(2, '0');
      const wd = String(wDate.getUTCDate()).padStart(2, '0');
      const wKey = `${wy}-${wm}-${wd}`;
      const wTime = w.time ? String(w.time).substring(0, 5) : '';
      return wKey === dateKey && wTime === timeKey && String(w.eventPlaceId || '') === String(eventPlaceId || '');
    });
  };

  const hasChanges = (newW, existing) => {
    if (!existing) return true;
    const fields = ['title', 'organizer', 'sacristanService', 'collection', 'organPlaying', 'eventPlaceId'];
    for (const field of fields) {
      const a = normalizeText(newW[field]);
      const b = normalizeText(existing[field]);
      if (a !== b) return true;
    }
    return false;
  };

  for (let r = 1; r < records.length; r++) {
    const row = records[r] || [];
    const dateUtc = parseNbrDateCell(row[datumCol]);
    if (!dateUtc) continue;

    const baseDateUtc = dateUtc;
    const dayName = normalizeText(row[0]).replace(/\s*\(\s*/g, ' (').trim();

    // Skip past days for preview (same behavior as docx import)
    const compare = new Date(baseDateUtc.getTime());
    compare.setUTCHours(0, 0, 0, 0);
    if (compare < today) continue;

    for (const group of groups) {
      const placeHeader = group.placeHeader;
      const eventPlaceId = resolveEventPlaceIdFromHeader(eventPlaces, placeHeader);

      const worshipCell = row[group.idx];
      const music = row[group.musicIdx];
      const service = row[group.serviceIdx];
      const segments = splitNbrCellToSegments(worshipCell);
      if (segments.length === 0) continue;

      for (const seg of segments) {
        const parsed = parseNbrSegment(seg, baseDateUtc, normalizedToName);
        if (!parsed || !parsed.time) {
          continue;
        }
        const worshipData = {
          date: parsed.dateUtc,
          dayName,
          time: parsed.time,
          title: parsed.title,
          // "Gottesdienst haltend" ist bei uns der "Gestalter" (organizer).
          organizer: parsed.officiant || '',
          collection: '',
          sacristanService: normalizeText(service),
          organPlaying: normalizeText(music),
          eventPlaceId,
        };

        const existing = findExisting(worshipData.date, worshipData.time, worshipData.eventPlaceId);
        if (!hasChanges(worshipData, existing)) {
          continue;
        }

        if (existing) {
          worshipData._isUpdate = true;
          worshipData._existingId = existing.id;
          worshipData._oldValues = {
            title: existing.title,
            organizer: existing.organizer,
            sacristanService: existing.sacristanService,
            collection: existing.collection,
            organPlaying: existing.organPlaying,
            eventPlaceId: existing.eventPlaceId,
          };
          worshipData._changedFields = Object.keys(worshipData._oldValues).filter((f) => normalizeText(worshipData[f]) !== normalizeText(existing[f]));
        } else {
          worshipData._isNew = true;
        }

        imported.push(worshipData);
      }
    }
  }

  const worshipsForFrontend = imported.map((w) => {
    const copy = { ...w };
    if (copy.date instanceof Date) {
      const year = copy.date.getUTCFullYear();
      const month = String(copy.date.getUTCMonth() + 1).padStart(2, '0');
      const day = String(copy.date.getUTCDate()).padStart(2, '0');
      copy.date = `${year}-${month}-${day}`;
    }
    if (copy.time && typeof copy.time === 'string' && copy.time.length > 5) {
      copy.time = copy.time.substring(0, 5);
    }
    return copy;
  });

  return { worships: worshipsForFrontend, errors };
}

// Import-Funktion für Gottesdienste aus dem neuen NBR-XLSX/CSV Format (2026+)
exports.importWorshipsNbrPlanning = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ message: 'Keine Datei hochgeladen.' });
    }

    const fileName = req.file.originalname.toLowerCase();
    if (!fileName.endsWith('.xlsx') && !fileName.endsWith('.csv')) {
      return res.status(400).json({ message: 'Nur .xlsx oder .csv Dateien sind erlaubt.' });
    }

    const records = fileName.endsWith('.xlsx')
      ? await parseXlsxToRecords(req.file.buffer)
      : parseCsvRecords(req.file.buffer.toString('utf8'));
    const parsed = await parseNbrPlanningRecords(records);

    res.status(200).json({
      message: `${fileName.endsWith('.xlsx') ? 'XLSX' : 'CSV'} geparst. ${parsed.worships.length} Einträge mit Änderungen gefunden.`,
      worships: parsed.worships,
      errors: parsed.errors.length ? parsed.errors : undefined,
    });
  } catch (error) {
    console.error('Fehler beim Importieren (NBR Planung):', error);
    res.status(500).json({ message: 'Fehler beim Importieren der Planungsdatei', error: error.message });
  }
};

exports.importWorshipsNbrCsv = exports.importWorshipsNbrPlanning;

// Funktion zum Speichern der bearbeiteten Gottesdienste
exports.saveImportedWorships = async (req, res) => {
  try {
    const { worships } = req.body;

    if (!worships || !Array.isArray(worships)) {
      return res.status(400).json({ message: 'Keine Gottesdienste zum Speichern übergeben.' });
    }

    let savedCount = 0;
    let updatedCount = 0;
    const errors = [];
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    for (const worshipData of worships) {
      try {
        // Prüfen ob Datum in der Vergangenheit liegt
        const worshipDate = new Date(worshipData.date);
        worshipDate.setHours(0, 0, 0, 0);
        if (worshipDate < today) {
          continue; // Überspringe vergangene Daten
        }

        // Freigabe-Status aus Import-Dialog übernehmen (Checkbox in der UI).
        // Fallback: wenn kein Wert gesetzt ist, bleibt es false.
        worshipData.approved = !!worshipData.approved;

        // Prüfen ob bereits ein Eintrag für dieses Datum und diese Uhrzeit existiert
        const whereClause = {
          date: {
            [Op.eq]: sequelize.fn('DATE', worshipData.date)
          },
          time: worshipData.time
        };

        // Wenn eventPlaceId gesetzt ist, auch danach suchen
        if (worshipData.eventPlaceId) {
          whereClause.eventPlaceId = worshipData.eventPlaceId;
        } else {
          // Wenn kein eventPlaceId, suche nach Einträgen ohne eventPlaceId
          whereClause.eventPlaceId = { [Op.is]: null };
        }

        const existingWorship = await Worship.findOne({ where: whereClause });

        if (existingWorship) {
          // Update bestehenden Eintrag
          await existingWorship.update(worshipData);
          updatedCount++;
        } else {
          // Neuen Eintrag erstellen
          await Worship.create(worshipData);
          savedCount++;
        }
      } catch (error) {
        console.error('Fehler beim Speichern eines Gottesdienstes:', error);
        errors.push(`Fehler beim Speichern: ${worshipData.date} ${worshipData.time} - ${worshipData.title || 'Unbekannt'}`);
      }
    }

    const totalProcessed = savedCount + updatedCount;
    res.status(200).json({ 
      message: `Import abgeschlossen. ${savedCount} neue Gottesdienste erstellt, ${updatedCount} aktualisiert.`,
      imported: savedCount,
      updated: updatedCount,
      total: totalProcessed,
      skipped: worships.length - totalProcessed,
      errors: errors
    });
  } catch (error) {
    console.error('Fehler beim Speichern der importierten Gottesdienste:', error);
    res.status(500).json({ message: 'Fehler beim Speichern der Gottesdienste', error: error.message });
  }
};

function normalizePdfLines(rawText) {
  return rawText
    .split('\n')
    .map((line) => line.replace(/\s+/g, ' ').trim())
    .filter((line) => line.length > 0)
    .filter((line) => !/^--\s*\d+\s+of\s+\d+\s*--$/i.test(line));
}

function findFirstIndex(lines, predicate, from = 0) {
  for (let i = from; i < lines.length; i++) {
    if (predicate(lines[i])) return i;
  }
  return -1;
}

function getSection(lines, startPredicate, endPredicates = []) {
  const start = findFirstIndex(lines, startPredicate);
  if (start < 0) return [];
  let end = lines.length;
  for (const p of endPredicates) {
    const idx = findFirstIndex(lines, p, start + 1);
    if (idx >= 0) end = Math.min(end, idx);
  }
  return lines.slice(start, end);
}

function normalizeText(input) {
  return String(input || '')
    .toLowerCase()
    .replace(/ä/g, 'ae')
    .replace(/ö/g, 'oe')
    .replace(/ü/g, 'ue')
    .replace(/ß/g, 'ss')
    .replace(/\s+/g, ' ')
    .trim();
}

function isHeading(line, heading) {
  const normalizedLine = normalizeText(line).replace(/[:\-–]\s*$/g, '');
  const normalizedHeading = normalizeText(heading).replace(/[:\-–]\s*$/g, '');
  return (
    normalizedLine === normalizedHeading ||
    normalizedLine.startsWith(`${normalizedHeading} `) ||
    normalizedHeading.startsWith(`${normalizedLine} `)
  );
}

function getSectionByHeading(lines, startHeading, endHeadings = []) {
  const start = findFirstIndex(lines, (l) => isHeading(l, startHeading));
  if (start < 0) return [];
  let end = lines.length;
  for (const endHeading of endHeadings) {
    const idx = findFirstIndex(lines, (l) => isHeading(l, endHeading), start + 1);
    if (idx >= 0) end = Math.min(end, idx);
  }
  return lines.slice(start, end);
}

function extractEventCandidates(lines) {
  const seen = new Set();
  return lines.filter((line) => {
    const normalized = line.toLowerCase();
    const hasDate =
      /\b\d{1,2}\.\d{1,2}\.(\d{4})?\b/.test(line) ||
      /\b\d{1,2}\.\d{2}\s*uhr\b/i.test(line) ||
      /\b\d{1,2}:\d{2}\s*uhr\b/i.test(line);
    const isDuplicate = seen.has(normalized);
    if (!isDuplicate && hasDate) {
      seen.add(normalized);
      return true;
    }
    return false;
  });
}

function looksLikeHeading(line) {
  return /^(gottesdienste|regelmäßige termine|männer und frauen|kinder und jugend|senioren|besondere gottesdienste|und veranstaltungen)$/i.test(line.trim());
}

function hasDateOrTime(line) {
  return (
    /\b\d{1,2}\.\d{1,2}\.(\d{2,4})?\b/.test(line) ||
    /\b\d{1,2}\.\d{1,2}\.?,\s*\d{1,2}\.\d{1,2}\.(\d{2,4})?\b/.test(line) ||
    /\b\d{1,2}[:.]\d{2}\s*uhr\b/i.test(line) ||
    /\b\d{1,2}\.\d{2}\s*-\s*\d{1,2}\.\d{2}\s*uhr\b/i.test(line)
  );
}

function buildDetailedItems(lines) {
  const result = [];
  const seen = new Set();
  const isSectionLabel = (line) => /^(gottesdienste|veranstaltungen)\s*:?\s*$/i.test(String(line || '').trim());
  const youthAnchorPattern = /\b(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden|vorkonfirmandenkurs)\b/i;
  const splitForYouthAnchors = (line) => {
    const compact = String(line || '').replace(/\s+/g, ' ').trim();
    if (!compact) return [];
    const withCuts = compact
      .replace(/\s+(?=(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden|vorkonfirmandenkurs)\b)/gi, ' || ')
      .replace(/\s+(?=(montag:|dienstag:|mittwoch:|donnerstag:|freitag:|samstag:|sonntag:))/gi, ' || ');
    return withCuts.split('||').map((s) => s.trim()).filter(Boolean);
  };
  const expandedLines = lines.flatMap(splitForYouthAnchors);
  const isHardSectionBoundary = (line) => {
    const n = normalizeText(line);
    if (!n) return false;
    return (
      n.startsWith('besondere gottesdienste und veranstaltungen') ||
      n.includes('nieder-erlenbach und harheim') ||
      n.startsWith('wunderbarer norden') ||
      n.startsWith('leben vor dem tod') ||
      /^seite?\s*\d+$/i.test(String(line || '').trim()) ||
      /^\d{1,3}$/.test(String(line || '').trim())
    );
  };
  const isEntryStart = (line) => {
    if (!line || looksLikeHeading(line) || isSectionLabel(line) || isHardSectionBoundary(line)) return false;
    if (/^(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag):/i.test(String(line).trim())) return true;
    if (youthAnchorPattern.test(line)) return true;
    const hasScheduleSignal =
      hasDateOrTime(line) ||
      /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(line) ||
      /\b(von|um)\s+\d{1,2}[:.]\d{2}\b/i.test(line) ||
      /\b(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)s?\b/i.test(line) ||
      /\btermine[:\s]/i.test(line);
    if (youthAnchorPattern.test(line) && hasScheduleSignal) return true;
    // Klassische Startzeilen in den PDFs:
    // "So., 08.02. 11.00 Uhr ..." oder "Mi., 18.02. 19.00 - 20.30 Uhr ..."
    if (/^(so|mo|di|mi|do|fr|sa)\.,?\s+\d{1,2}\.\d{1,2}\./i.test(line)) return true;
    // Fallback: enthalt Datum + Uhrzeit in derselben Zeile.
    return /\b\d{1,2}\.\d{1,2}\./.test(line) && /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(line);
  };
  const hasYouthScheduleSignal = (line) =>
    hasDateOrTime(line) ||
    /\b\d{1,2}[:.]\d{2}\s*(?:-\s*\d{1,2}[:.]\d{2}\s*)?uhr\b/i.test(String(line || '')) ||
    /\b(von|um)\s+\d{1,2}[:.]\d{2}\b/i.test(String(line || '')) ||
    /\b(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)\b/i.test(String(line || '')) ||
    /\btermine[:\s]/i.test(String(line || ''));

  for (let i = 0; i < expandedLines.length; i++) {
    const current = expandedLines[i];
    if (!isEntryStart(current)) continue;

    const parts = [current];
    for (let j = i + 1; j < expandedLines.length; j++) {
      const next = expandedLines[j];
      if (!next) break;
      if (looksLikeHeading(next) || isSectionLabel(next) || isHardSectionBoundary(next)) break;
      if (isEntryStart(next)) {
        const currentIsYouthAnchor = youthAnchorPattern.test(current);
        const currentHasSchedule = hasYouthScheduleSignal(current);
        const nextIsStandaloneScheduleLine = !youthAnchorPattern.test(next);
        if (currentIsYouthAnchor && !currentHasSchedule && nextIsStandaloneScheduleLine) {
          parts.push(next);
          i = j;
          continue;
        }
        break;
      }
      if (isNoiseLine(next)) break;
      parts.push(next);
      i = j; // konsumierte Zeilen überspringen
    }

    const text = parts
      .join(' ')
      .replace(/\s*-\s+(?=[A-Za-zÄÖÜäöüß])/g, '') // harte Zeilentrennung "Gemein- desaal" heilen
      .replace(/\s+\|/g, ' |')
      .replace(/\s{2,}/g, ' ')
      .trim();

    // Falls eine neue Abschnittsüberschrift in derselben Zeile klebt,
    // den Eintrag dort hart abschneiden.
    const textCutAtInlineBoundary = text
      .split(/\s+Besondere Gottesdienste und Veranstaltungen\b/i)[0]
      .split(/\s+Wunderbarer Norden\b/i)[0]
      .split(/\s+Leben vor dem Tod\b/i)[0]
      .trim();

    const key = textCutAtInlineBoundary.toLowerCase();
    if (!seen.has(key) && textCutAtInlineBoundary.length > 0) {
      seen.add(key);
      result.push(textCutAtInlineBoundary);
    }
  }
  return result;
}

function isNoiseLine(line) {
  const n = normalizeText(line);
  return (
    n.includes('impressum') ||
    n.includes('redaktionsschluss') ||
    n.includes('visdp') ||
    n.includes('buerozeiten') ||
    n.includes('@:') ||
    n.includes('@t-online.de') ||
    n.includes('datenschutzerklaerung') ||
    n.includes('logout')
  );
}

function filterNoise(lines) {
  return lines.filter((line) => !isNoiseLine(line));
}

function extractNamedBlock(lines, pattern, maxLookahead = 3, maxParts = 3) {
  const blocks = [];
  for (let i = 0; i < lines.length; i++) {
    if (!pattern.test(lines[i])) continue;
    const parts = [lines[i]];
    for (let j = i + 1; j < Math.min(lines.length, i + 1 + maxLookahead); j++) {
      const candidate = lines[j];
      if (looksLikeHeading(candidate)) break;
      if (isNoiseLine(candidate)) break;
      if (hasDateOrTime(candidate) || /\bum\s+\d{1,2}[:.]\d{2}\s*uhr\b/i.test(candidate)) {
        parts.push(candidate);
      }
      if (parts.length >= maxParts) break;
    }
    blocks.push(parts.join(' | '));
  }
  return [...new Set(blocks)];
}

function extractLinesByKeyword(lines, pattern) {
  return lines.filter((line) => pattern.test(line));
}

function extractRegularTermineDetails(lines) {
  const anchors = [
    /kinderkirche/i,
    /kigosabo/i,
    /jungschar/i,
    /konfirmationsunterricht/i,
    /konfirmanden\s*[„"]/i,
    /was geht abend/i,
    /vorkonfirmandenkurs/i,
    /pfadfinder/i,
    /miriamtreff/i,
    /m[aä]nnerpalaver/i,
    /frauenfr[üu]hst[üu]ck/i,
    /kinder- und jugendb[üu]cherei/i,
    /wunderkiste/i,
    /seniorenclub/i,
    /seniorencaf[eé]/i,
  ];
  const splitRegularLineIntoSegments = (line) => {
    const compact = String(line || '').replace(/\s+/g, ' ').trim();
    if (!compact) return [];

    const withAnchorCuts = compact
      .replace(/\s+(?=(kinderkirche|kigosabo|kindergottesdienst|jungschar|heliand-?pfadfinder(?:innen)?|pfadfinder(?:innen)?|konfirmationsunterricht|konfirmanden\s*[„"]|vorkonfirmandenkurs|m[aä]nnerpalaver|miriamtreff|frauenfr[üu]hst[üu]ck|wunderkiste)\b)/gi, ' || ')
      .replace(/\s+(?=(montag:|dienstag:|mittwoch:|donnerstag:|freitag:|samstag:|sonntag:))/gi, ' || ');

    return withAnchorCuts
      .split('||')
      .map((s) => s.trim())
      .filter(Boolean);
  };

  const expandedLines = lines.flatMap(splitRegularLineIntoSegments);
  const details = [];
  const seen = new Set();
  const isAnchorLine = (line) => anchors.some((r) => r.test(line));
  const isSubHeadingLike = (line) => {
    const t = normalizeText(line);
    return (
      /^kinder und jugendliche$/.test(t) ||
      /^kinder und jugend$/.test(t) ||
      /^maenner und frauen$/.test(t) ||
      /^musik$/.test(t) ||
      /^senioren$/.test(t)
    );
  };

  for (let i = 0; i < expandedLines.length; i++) {
    const line = expandedLines[i];
    if (!isAnchorLine(line)) continue;
    if (isNoiseLine(line)) continue;
    if (/start des neuen konfirmanden-jahrganges/i.test(line)) continue;
    if (/konfirmanden\s*\/\s*geburtstagsgr[üu][ßs]e/i.test(line)) continue;
    if (/jahrgang der miriamgemeinde/i.test(line)) continue;

    const parts = [line];
    let hasScheduleSignal = hasDateOrTime(line) || /termine[:\s]/i.test(line);
    for (let j = i + 1; j < Math.min(expandedLines.length, i + 8); j++) {
      const next = expandedLines[j];
      if (looksLikeHeading(next) || isSubHeadingLike(next) || isNoiseLine(next)) break;
      // Sobald ein neuer Anker startet, endet der aktuelle Block.
      if (isAnchorLine(next)) break;
      if (hasDateOrTime(next) || /termine[:\s]/i.test(next) || /\bmontag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag\b/i.test(next)) {
        parts.push(next);
        hasScheduleSignal = true;
      } else {
        break;
      }
    }
    if (!hasScheduleSignal) continue;
    let text = parts.join(' | ');
    // Manche PDFs liefern Miriamtreff + Männerpalaver in einer Zeile.
    // Für die Kategorie "Regelmäßige Termine" trennen wir das sauber.
    if (/^miriamtreff:/i.test(text) && /\|\s*m[aä]nnerpalaver/i.test(text)) {
      text = text.split(/\|\s*m[aä]nnerpalaver/i)[0].trim();
    }
    const key = text.toLowerCase();
    if (!seen.has(key)) {
      seen.add(key);
      details.push(text);
    }
  }
  return details;
}

function isDateHeaderLine(line) {
  return /\b\d{1,2}\.\d{1,2}\.(\d{2,4})?\b/.test(line);
}

function isLikelyDayNameLine(line) {
  if (!line) return false;
  if (hasDateOrTime(line)) return false;
  if (looksLikeHeading(line)) return false;
  const t = normalizeText(line);
  return (
    /advent|trinitatis|epiphanias|ostern|pfingsten|sonntag|montag|dienstag|mittwoch|donnerstag|freitag|samstag/.test(t) &&
    t.length < 80
  );
}

function splitWorshipLinesByTime(lines) {
  const entries = [];
  let current = null;
  const startsWithTime = (line) => /^\d{1,2}[:.]\d{2}\s*uhr\b/i.test(line);
  const isNeighborInvitationLine = (line) => /einladung zum gottesdienst im nachbarschaftsraum/i.test(line);
  const isSelfInformationLine = (line) => /bitte informieren sie sich auch auf den internetseiten/i.test(line);
  let stickyNeighborInvitation = false;
  let stickySelfInformation = false;

  for (const line of lines) {
    if (!line || isNoiseLine(line) || looksLikeHeading(line)) continue;
    if (isNeighborInvitationLine(line)) {
      stickyNeighborInvitation = true;
      if (current && current.length) current.push('[[FLAG_NEIGHBOR_INVITATION]]');
      continue;
    }
    if (isSelfInformationLine(line)) {
      stickySelfInformation = true;
      if (current && current.length) current.push('[[FLAG_SELF_INFORMATION]]');
      continue;
    }
    if (startsWithTime(line) && current && current.length) {
      entries.push(current.join(' | '));
      current = [line];
      if (stickyNeighborInvitation) current.push('[[FLAG_NEIGHBOR_INVITATION]]');
      if (stickySelfInformation) current.push('[[FLAG_SELF_INFORMATION]]');
      continue;
    }
    if (startsWithTime(line) && (!current || current.length === 0)) {
      current = [];
      current.push(line);
      if (stickyNeighborInvitation) current.push('[[FLAG_NEIGHBOR_INVITATION]]');
      if (stickySelfInformation) current.push('[[FLAG_SELF_INFORMATION]]');
      continue;
    }

    // Zeilen ohne Uhrzeit vor dem ersten Gottesdienst werden nur als Kontext verstanden.
    // Sie dürfen keinen eigenen Gottesdienst-Eintrag erzeugen.
    if (!current || current.length === 0) {
      continue;
    }

    // Zeilen ohne Uhrzeit nach einer Zeit gehören zum laufenden Gottesdienst (z.B. "Audite Nova").
    current.push(line);
  }
  if (current && current.length) entries.push(current.join(' | '));
  return entries;
}

function extractWorshipBlocks(lines) {
  const blocks = [];
  let currentHeader = '';
  let currentDayNameParts = [];
  let rightColumnLines = [];
  let startedWorshipContent = false;

  const flush = () => {
    if (!currentHeader || rightColumnLines.length === 0) return;
    const currentDayName = currentDayNameParts.join(' ').replace(/\s+/g, ' ').trim();
    const header = currentDayName ? `${currentHeader} - ${currentDayName}` : currentHeader;
    const worshipEntries = splitWorshipLinesByTime(rightColumnLines);
    if (worshipEntries.length === 0) {
      const joined = rightColumnLines.join(' | ').trim();
      if (joined) {
        blocks.push(`${header} | ${joined}`);
      }
    } else {
      worshipEntries.forEach((entry) => blocks.push(`${header} | ${entry}`));
    }
  };

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (!line || isNoiseLine(line) || looksLikeHeading(line)) continue;

    if (isDateHeaderLine(line)) {
      flush();
      currentHeader = line;
      currentDayNameParts = [];
      rightColumnLines = [];
      startedWorshipContent = false;
      continue;
    }

    if (currentHeader && !startedWorshipContent && isLikelyDayNameLine(line)) {
      currentDayNameParts.push(line);
      continue;
    }

    if (currentHeader) {
      if (/^\d{1,2}[:.]\d{2}\s*uhr\b/i.test(line)) {
        startedWorshipContent = true;
      }
      rightColumnLines.push(line);
    }
  }

  flush();
  return [...new Set(blocks.map((b) => b.trim()).filter(Boolean))];
}

function buildEventSignature(line) {
  const text = normalizeText(line);
  const anchorPatterns = [
    /kinderkirche/,
    /kigosabo|kindergottesdienst/,
    /jungschar/,
    /konfirmationsunterricht/,
    /konfirmanden/,
    /vorkonfirmandenkurs/,
    /pfadfinder/,
    /miriamtreff/,
    /maennerpalaver/,
    /frauenfruehstueck/,
    /seniorenclub/,
    /seniorencafe|senioren-cafe/,
  ];
  const anchor = (anchorPatterns.find((r) => r.test(text)) || /./).source;

  const dates = [...text.matchAll(/\b(\d{1,2})\.(\d{1,2})\b/g)]
    .map((m) => `${String(m[1]).padStart(2, '0')}.${String(m[2]).padStart(2, '0')}`)
    .sort()
    .join(',');

  const range = text.match(/\b(\d{1,2})[:.](\d{2})\s*-\s*(\d{1,2})[:.](\d{2})\s*uhr\b/i);
  const single = text.match(/\b(\d{1,2})[:.](\d{2})\s*uhr\b/i);
  const startTime = range
    ? `${String(range[1]).padStart(2, '0')}:${range[2]}`
    : (single ? `${String(single[1]).padStart(2, '0')}:${single[2]}` : '');
  const endTime = range ? `${String(range[3]).padStart(2, '0')}:${range[4]}` : '';
  const openTermine = /termine\s*:\s*noch offen|noch offen/.test(text) ? 'open' : '';

  const placePatterns = [
    /kita sternenzelt/,
    /gemeindehaus bonames/,
    /gemeindehaus nieder-eschbach/,
    /gemeindehaus nieder-erlenbach/,
    /crutzenhof kalbach/,
    /bonames/,
    /kalbach/,
    /nieder-eschbach/,
    /nieder-erlenbach/,
    /harheim/,
  ];
  const place = (placePatterns.find((r) => r.test(text)) || /./).source;

  return `${anchor}|${dates}|${startTime}|${endTime}|${openTermine}|${place}`;
}

function dedupeBySignature(lines) {
  const seen = new Set();
  const result = [];
  for (const line of lines || []) {
    const key = buildEventSignature(line);
    if (seen.has(key)) continue;
    seen.add(key);
    result.push(line);
  }
  return result;
}

function removeCrossSectionDuplicates(primaryLines, secondaryLines) {
  const primaryKeys = new Set((primaryLines || []).map((line) => buildEventSignature(line)));
  return (secondaryLines || []).filter((line) => !primaryKeys.has(buildEventSignature(line)));
}

exports.importNewsletterPdf = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ message: 'Keine PDF-Datei hochgeladen.' });
    }

    const fileName = req.file.originalname.toLowerCase();
    if (!fileName.endsWith('.pdf')) {
      return res.status(400).json({ message: 'Bitte eine PDF-Datei hochladen.' });
    }

    const parsed = await pdfParse(req.file.buffer);
    const lines = normalizePdfLines(parsed.text || '');

    const gottesdiensteLines = getSection(
      lines,
      (l) => l.toLowerCase() === 'gottesdienste' || /^8\s+gottesdienste$/i.test(l),
      [
        (l) => /besondere gottesdienste/i.test(l),
      ]
    );

    const regelmaessigSection = getSectionByHeading(
      lines,
      'Regelmäßige Termine',
      ['Neues von den Senioren', 'Kinder und Jugendliche']
    );
    const maennerFrauenSection = getSectionByHeading(
      lines,
      'Männer und Frauen',
      ['Musik', 'Kinder und Jugendliche']
    );
    const seniorenKeywordLines = extractLinesByKeyword(lines, /seniorenclub|senioren-?caf[eé]/i);
    const regelmaessigLines = [...regelmaessigSection, ...maennerFrauenSection, ...seniorenKeywordLines];

    const besondereLines = getSectionByHeading(
      lines,
      'Besondere Gottesdienste',
      ['Regelmäßige Termine', 'Männer und Frauen', 'Kinder und Jugendliche', 'Neues von den Senioren']
    );

    const miriamtreffLines = extractLinesByKeyword(lines, /miriamtreff/i);
    const frauenfruehstueckLines = extractNamedBlock(lines, /frauenfrühstück|frauenfruehstueck/i, 8, 5);

    const kinderJugendLines = getSection(
      lines,
      (l) => /^kinder und jugendliche$/i.test(l),
      [
        (l) => /^senioren$/i.test(l),
      ]
    );

    const cleanedGottesdienste = filterNoise(gottesdiensteLines);
    const cleanedRegelmaessig = filterNoise(regelmaessigLines);
    const cleanedBesondere = filterNoise(besondereLines);
    const cleanedKinderJugend = filterNoise(kinderJugendLines);

    const regelmaessigDetails = dedupeBySignature(extractRegularTermineDetails(cleanedRegelmaessig));
    const seniorenDetails = dedupeBySignature(extractRegularTermineDetails(filterNoise(seniorenKeywordLines)));
    const mergedRegelmaessigDetails = dedupeBySignature([...regelmaessigDetails, ...seniorenDetails]);
    const kinderUndJugendDetails = dedupeBySignature(buildDetailedItems(cleanedKinderJugend));
    const regelmaessigOhneSenioren = removeCrossSectionDuplicates(seniorenDetails, mergedRegelmaessigDetails);
    const regelmaessigOhneJugend = removeCrossSectionDuplicates(kinderUndJugendDetails, regelmaessigOhneSenioren);
    const miriamtreffDetails = dedupeBySignature(miriamtreffLines);
    const regelmaessigBereinigt = removeCrossSectionDuplicates(miriamtreffDetails, regelmaessigOhneJugend);

    const parsedWorshipBlocks = extractWorshipBlocks(cleanedGottesdienste);

    const result = {
      gottesdienste: parsedWorshipBlocks,
      regelmaessigeTermine: regelmaessigBereinigt,
      besondereGottesdienste: extractEventCandidates(cleanedBesondere),
      miriamtreff: miriamtreffDetails,
      kinderUndJugend: kinderUndJugendDetails,
      frauenfruehstueck: frauenfruehstueckLines,
      senioren: seniorenDetails,
    };

    const details = {
      gottesdienste: parsedWorshipBlocks,
      regelmaessigeTermine: regelmaessigBereinigt,
      besondereGottesdienste: buildDetailedItems(cleanedBesondere),
      miriamtreff: miriamtreffDetails,
      kinderUndJugend: kinderUndJugendDetails,
      frauenfruehstueck: frauenfruehstueckLines,
      senioren: seniorenDetails,
      sectionInfo: {
        gottesdiensteLines: gottesdiensteLines.length,
        regelmaessigLines: regelmaessigLines.length,
        seniorenKeywordLines: seniorenKeywordLines.length,
        besondereLines: besondereLines.length,
        kinderJugendLines: kinderJugendLines.length,
      }
    };

    const questions = [];
    if (result.gottesdienste.length === 0) {
      questions.push('Keine Gottesdienste sicher extrahiert. Abschnittsgrenze oder Muster prüfen.');
    }
    if (result.regelmaessigeTermine.length === 0) {
      questions.push('Regelmäßige Termine leer. Soll dieser Bereich seitenübergreifend weiter gefasst werden?');
    }
    if (result.besondereGottesdienste.length === 0) {
      questions.push('Besondere Gottesdienste leer. Eventuell weitere Muster/Orte notwendig.');
    }
    if (result.miriamtreff.length === 0) {
      questions.push('Miriamtreff nicht gefunden. Soll auch "Männer und Frauen" als Fallback gelten?');
    }
    if (result.kinderUndJugend.length === 0) {
      questions.push('Kinder/Jugend leer. Soll zusätzlich der Abschnitt "Kinder und Jugend" (Seite 19) priorisiert werden?');
    }

    res.status(200).json({
      message: 'PDF geparst. Bitte Vorschau prüfen und offene Fragen beantworten.',
      parsed: result,
      details,
      questions,
      meta: {
        pages: parsed.numpages || null,
        lineCount: lines.length,
      },
    });
  } catch (error) {
    console.error('Fehler beim PDF-Import des Gemeindebriefs:', error);
    res.status(500).json({ message: 'Fehler beim Parsen der PDF-Datei.', error: error.message });
  }
};

// Export-Funktion für Gottesdienste
exports.exportWorships = async (req, res) => {
  try {
    const { from, to, format } = req.query;

    if (!from || !to) {
      return res.status(400).json({ message: 'Von- und Bis-Datum müssen angegeben werden.' });
    }

    const fromDate = new Date(from);
    const toDate = new Date(to);
    toDate.setHours(23, 59, 59, 999); // Bis Ende des Tages

    if (fromDate > toDate) {
      return res.status(400).json({ message: 'Das Von-Datum muss vor dem Bis-Datum liegen.' });
    }

    // Gottesdienste im Datumsbereich abrufen
    const worships = await Worship.findAll({
      where: {
        date: {
          [Op.between]: [fromDate, toDate]
        }
      },
      include: {
        model: EventPlace,
        as: 'eventPlace',
      },
      order: [
        ['date', 'ASC'],
        ['time', 'ASC']
      ],
    });

    if (worships.length === 0) {
      return res.status(404).json({ message: 'Keine Gottesdienste im angegebenen Zeitraum gefunden.' });
    }

    // Datum formatieren für Anzeige
    const formatDate = (date) => {
      const d = new Date(date);
      return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
    };

    const formatTime = (time) => {
      if (!time) return '';
      const parts = time.split(':');
      return `${parts[0]}:${parts[1]}`;
    };

    // Hex zu RGB konvertieren
    const hexToRgb = (hex) => {
      if (!hex) return null;
      const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    };

    // Gottesdienste nach Datum gruppieren für rowspan-Berechnung
    const worshipsByDate = {};
    worships.forEach(worship => {
      const dateKey = formatDate(worship.date);
      if (!worshipsByDate[dateKey]) {
        worshipsByDate[dateKey] = [];
      }
      worshipsByDate[dateKey].push(worship);
    });

    // Tabellenzeilen erstellen
    const tableRows = [];
    worships.forEach((worship, index) => {
      const dateKey = formatDate(worship.date);
      const isFirstWorshipOfDay = worshipsByDate[dateKey][0] === worship;
      const rowspan = worshipsByDate[dateKey].length;

      const dateStr = formatDate(worship.date);
      const dayNameStr = worship.dayName || '';
      
      // Erste Spalte: Datum + Tag-Name
      // Formatierungen aus der geparsten Datei: Schriftart Arial, Größe 11pt, fett
      // Breite: 3,45 cm = 1956 DXA (1 cm = 567 DXA)
      let firstCell;
      if (isFirstWorshipOfDay) {
        // Erste Zelle des Tages: VerticalMerge RESTART (startet die Verbindung)
        // Tag-Name in neuer Zeile
        const firstCellChildren = [
          new TextRun({ 
            text: dateStr, 
            color: '000000',
            font: 'Arial',
            size: 22, // 11pt = 22 half-points
            bold: true
          })
        ];
        if (dayNameStr) {
          firstCellChildren.push(new TextRun({ 
            text: '', 
            break: 1 // Zeilenumbruch
          }));
          firstCellChildren.push(new TextRun({ 
            text: dayNameStr, 
            color: '000000',
            font: 'Arial',
            size: 22,
            bold: true
          }));
        }
        
        firstCell = new TableCell({
          children: [new Paragraph({
            children: firstCellChildren,
            alignment: AlignmentType.CENTER, // Spalte 1 zentriert
          })],
          // Breite wird über columnWidths auf Tabellenebene gesetzt
          margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding
          verticalMerge: rowspan > 1 ? VerticalMergeType.RESTART : undefined,
          verticalAlign: VerticalAlign.TOP
        });
      } else {
        // Nachfolgende Zellen: mit VerticalMerge CONTINUE verbinden
        firstCell = new TableCell({
          children: [new Paragraph({ 
            text: '',
            alignment: AlignmentType.CENTER, // Spalte 1 zentriert
          })],
          // Breite wird über columnWidths auf Tabellenebene gesetzt
          margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding
          verticalMerge: VerticalMergeType.CONTINUE
        });
      }

      // Zweite Spalte: Gottesdienst-Details
      // Hintergrundfarbe aus EventPlace-Einstellungen
      const backgroundColor = worship.eventPlace?.backgroundColor || '#ffffff';
      const hexColor = backgroundColor.replace('#', '').toUpperCase();
      
      // Hintergrundfarbe immer setzen (auch wenn weiß)
      // Prüfen, ob die Farbe gültig ist (6-stelliger Hex-Code)
      const validHexColor = hexColor.length === 6 ? hexColor : 'FFFFFF';
      const shading = {
        fill: '#' + validHexColor, // Fill muss mit # beginnen
        type: ShadingType.SOLID,
        color: '#' + validHexColor // Color sollte die gleiche Farbe wie fill haben
      };

      // Format: **bold** Uhrzeit + Titel, dann Gestaltung (bold), dann Kollekte (nicht bold)
      const timeStr = formatTime(worship.time);
      const titleStr = worship.title || 'Gottesdienst';
      const organizerStr = worship.organizer || '';
      const collectionStr = worship.collection || '';

      const secondCellChildren = [];
      
      // Uhrzeit + Titel (bold, schwarz, Arial 11pt)
      secondCellChildren.push(new TextRun({ 
        text: `${timeStr} ${titleStr}`, 
        bold: true,
        color: '000000',
        font: 'Arial',
        size: 22 // 11pt = 22 half-points
      }));
      
      // Gestaltung (bold, schwarz, Arial 11pt)
      if (organizerStr) {
        secondCellChildren.push(new TextRun({ 
          text: '', 
          break: 1 // Zeilenumbruch
        }));
        secondCellChildren.push(new TextRun({ 
          text: 'Gestaltung: ', 
          bold: true,
          color: '000000',
          font: 'Arial',
          size: 22
        }));
        secondCellChildren.push(new TextRun({ 
          text: organizerStr, 
          bold: true,
          color: '000000',
          font: 'Arial',
          size: 22
        }));
      }
      
      // Kollekte (nicht bold, schwarz, Arial 11pt)
      if (collectionStr) {
        secondCellChildren.push(new TextRun({ 
          text: '', 
          break: 1 // Zeilenumbruch
        }));
        secondCellChildren.push(new TextRun({ 
          text: 'Kollekte: ', 
          color: '000000',
          font: 'Arial',
          size: 22
        }));
        secondCellChildren.push(new TextRun({ 
          text: collectionStr, 
          color: '000000',
          font: 'Arial',
          size: 22
        }));
      }

      const secondCell = new TableCell({
        children: [new Paragraph({ 
          children: secondCellChildren,
          alignment: AlignmentType.CENTER, // Spalte 2 zentriert
        })],
        // Breite wird über columnWidths auf Tabellenebene gesetzt
        shading: shading, // Shading vor margins setzen
        margins: { top: 140, bottom: 140, left: 140, right: 140 }, // 6mm Padding
        verticalAlign: VerticalAlign.TOP
      });

      // Zeile erstellen - immer beide Spalten
      const rowChildren = [firstCell, secondCell];

      tableRows.push(new TableRow({
        children: rowChildren,
        height: { value: 1559, rule: HeightRule.EXACT } // 2,75 cm = 1559 DXA - Zeilenhöhe auf TableRow setzen
      }));
    });

    // Word-Dokument erstellen
    // Seitenränder: 2 cm = 1134 DXA (1 cm = 567 DXA)
    // Verfügbare Breite: 21 cm (A4) - 2 cm links - 2 cm rechts = 17 cm = 9638 DXA
    // Spalte 1: 3,45 cm = 1956 DXA
    // Spalte 2: 13,55 cm = 7682 DXA
    const doc = new Document({
      sections: [{
        properties: {
          page: {
            margin: {
              top: 1134, // 2 cm
              right: 1134, // 2 cm
              bottom: 1134, // 2 cm
              left: 1134 // 2 cm
            }
          }
        },
        children: [
          new Paragraph({
            text: `Gottesdienste ${formatDate(fromDate)} - ${formatDate(toDate)}`,
            heading: 'Heading1',
            alignment: AlignmentType.CENTER
          }),
          new Paragraph({ text: '' }), // Leerzeile
          new Table({
            width: { size: 9638, type: WidthType.DXA }, // 17 cm = 3,45 cm (1956) + 13,55 cm (7682)
            columnWidths: [1956, 7682], // Explizite Spaltenbreiten in DXA
            borders: {
              top: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' },
              bottom: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' },
              left: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' },
              right: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' },
              insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' },
              insideVertical: { style: BorderStyle.SINGLE, size: 1, color: 'D0D0D0' }
            },
            rows: tableRows
          })
        ]
      }]
    });

    // Dokument als Buffer generieren
    const buffer = await Packer.toBuffer(doc);

    // Dateiname generieren
    const filename = `gottesdienste_${from}_${to}_${format}.docx`;

    // Response senden
    res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.send(buffer);
  } catch (error) {
    console.error('Fehler beim Exportieren der Gottesdienste:', error);
    res.status(500).json({ message: 'Fehler beim Exportieren der Gottesdienste', error: error.message });
  }
};
