import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";
import { getObjectStream, saveObject } from "./storage.js";

export const DEFAULT_MEETING_PROTOCOL_TEMPLATE = `{{template_logo}}

# Betriebsrat {{committee_name}}
# Protokoll der BR-Sitzung am {{meeting_date}}
VERTRAULICH

{{meeting_title_block}}

## Organisatorisches und Feststellungen

Sitzungsnummer: {{meeting_number}}
Sitzungsart: {{meeting_type}}
Sitzungsbeginn: {{start_time}}
Sitzungsort: {{location}}
Sitzungsleitung: {{chair_name}}
Schriftführung: {{secretary_name}}
{{quorum_statement}}
{{attendees_block}}

## Protokollnotizen
{{protocol_notes}}

## Tagesordnung und Beratung
{{agenda_sections}}

## Sitzungsende
Schließung der Sitzung um {{end_time}} Uhr.

{{signature_place}}
______________________________
{{chair_name}}

______________________________
{{secretary_name}}`;

function escapeRtf(value) {
  return String(value ?? "")
    .replace(/\\/g, "\\\\")
    .replace(/\{/g, "\\{")
    .replace(/\}/g, "\\}")
    .replace(/\t/g, "\\tab ");
}


function decodeBasicHtmlEntities(value) {
  return String(value ?? "")
    .replace(/&nbsp;/gi, " ")
    .replace(/&amp;/gi, "&")
    .replace(/&lt;/gi, "<")
    .replace(/&gt;/gi, ">")
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/&#x27;/gi, "'");
}

function htmlToProtocolText(html) {
  let text = String(html ?? "");
  text = text.replace(/<\s*(script|style)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, "");
  text = text.replace(/<\s*img[^>]*(?:data-template-logo=["']true["']|alt=["']template_logo["'])[^>]*>/gi, "\n{{template_logo}}\n");
  text = text.replace(/<\s*h1[^>]*>/gi, "\n# ").replace(/<\s*\/\s*h1\s*>/gi, "\n");
  text = text.replace(/<\s*h2[^>]*>/gi, "\n## ").replace(/<\s*\/\s*h2\s*>/gi, "\n");
  text = text.replace(/<\s*h3[^>]*>/gi, "\n### ").replace(/<\s*\/\s*h3\s*>/gi, "\n");
  text = text.replace(/<\s*li[^>]*>/gi, "\n- ").replace(/<\s*\/\s*li\s*>/gi, "");
  text = text.replace(/<\s*br\s*\/?>/gi, "\n");
  text = text.replace(/<\s*\/\s*(p|div|section|article|tr|table|ul|ol)\s*>/gi, "\n");
  text = text.replace(/<\s*(p|div|section|article|tr|table|ul|ol)[^>]*>/gi, "\n");
  text = text.replace(/<\s*\/\s*(td|th)\s*>/gi, " | ");
  text = text.replace(/<[^>]+>/g, "");
  text = decodeBasicHtmlEntities(text);
  return text
    .split("\n")
    .map((line) => line.replace(/[ \t]+$/g, ""))
    .join("\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function normalizeTemplateContentForProtocol(content, format = "markdown") {
  if (String(format || "markdown").toLowerCase() === "html") {
    return htmlToProtocolText(content);
  }
  return String(content ?? "");
}

function rtfImageBlock(asset) {
  if (!asset?.buffer?.length) return "";
  const mime = String(asset.mime_type || "").toLowerCase();
  const blip = mime === "image/png" ? "pngblip" : mime === "image/jpeg" || mime === "image/jpg" ? "jpegblip" : "";
  if (!blip) return `{\\pard\\qc\\sa180\\fs20 ${escapeRtf(`[Bild kann im RTF nicht eingebettet werden: ${asset.file_name || "Logo"}`)}\\par}`;
  const hex = asset.buffer.toString("hex");
  return `{\\pard\\qc\\sa220{\\pict\\${blip}\\picwgoal3200\\pichgoal1100 ${hex}}\\par}`;
}

async function streamToBuffer(stream) {
  const chunks = [];
  for await (const chunk of stream) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  return Buffer.concat(chunks);
}

async function loadTemplateLogo(db, template) {
  if (!template?.logo_asset_id) return null;
  const result = await db.query(
    `SELECT id, title, file_name, storage_path, mime_type
       FROM template_asset
      WHERE id = $1::bigint
        AND is_active = TRUE`,
    [template.logo_asset_id]
  );
  const asset = result.rows[0];
  if (!asset) return null;
  try {
    const { stream } = await getObjectStream(asset.storage_path);
    return { ...asset, buffer: await streamToBuffer(stream) };
  } catch {
    return null;
  }
}

function formatDate(value) {
  if (!value) return "";
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  return date.toLocaleDateString("de-DE");
}

function formatDateTime(value) {
  if (!value) return "";
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  return date.toLocaleString("de-DE");
}

function formatTime(value) {
  if (!value) return "offen";
  const raw = String(value);
  if (/^\d{2}:\d{2}/.test(raw)) return raw.slice(0, 5);
  return raw;
}

function meetingTypeLabel(value) {
  if (value === "ausserordentlich") return "Außerordentlich";
  return "Ordentlich";
}

function decisionStatusLabel(value) {
  switch (String(value ?? "").trim().toLowerCase()) {
    case "in_bearbeitung":
      return "In Bearbeitung";
    case "rueckfrage":
      return "Rückfrage";
    case "klaerung":
      return "Klärung";
    case "abgeschlossen":
      return "Abgeschlossen";
    default:
      return value || "offen";
  }
}

function sanitizeLine(value, fallback = "-") {
  const text = String(value ?? "").trim();
  return text || fallback;
}

function quorumStatement(value) {
  if (value === true) return "Die Sitzung ist Beschlussfähig.";
  if (value === false) return "Die Sitzung ist nicht Beschlussfähig.";
  return "Die Beschlussfähigkeit wurde nicht dokumentiert.";
}

function roleLabel(value) {
  switch (String(value ?? "").trim().toLowerCase()) {
    case "admin":
      return "Administration";
    case "chair":
      return "Vorsitz";
    case "secretary":
      return "Schriftführung";
    case "member":
      return "Mitglied";
    case "advisor":
      return "Beratung";
    default:
      return value || "";
  }
}

function renderAttendeesBlock(attendees = []) {
  if (!Array.isArray(attendees) || !attendees.length) {
    return "\n## Anwesende Betriebsratsmitglieder\nKeine anwesenden Betriebsratsmitglieder dokumentiert.";
  }

  const lines = ["", "## Anwesende Betriebsratsmitglieder"];
  attendees.forEach((attendee) => {
    const name = sanitizeLine(attendee.full_name, "Unbekannt");
    const parts = [];
    const role = roleLabel(attendee.role);
    if (role) parts.push(role);
    if (attendee.email) parts.push(attendee.email);
    lines.push(parts.length ? `- ${name} (${parts.join(", ")})` : `- ${name}`);
  });

  return lines.join("\n");
}

function computeDecisionOutcome(decision) {
  const yes = Number(decision?.votes_yes ?? 0);
  const no = Number(decision?.votes_no ?? 0);
  if (yes === 0 && no === 0) return "Ergebnis: nicht hinterlegt";
  if (yes > no) return "Ergebnis: angenommen";
  if (no > yes) return "Ergebnis: abgelehnt";
  return "Ergebnis: Stimmengleichheit";
}

function returnTypeLabel(value) {
  const normalized = String(value || "rueckfrage");
  if (normalized === "klaerung") return "Klärung";
  if (normalized === "bemerkung") return "Bemerkung / Wiedervorlage";
  return "Rückfrage";
}

function renderReturnedAgendaSections(returnedItems = []) {
  if (!Array.isArray(returnedItems) || !returnedItems.length) return "";

  const lines = ["## Folge-TOPs / Rückfragen"];
  returnedItems.forEach((item) => {
    const topNumber = item.agenda_number_at_return || item.agenda_number || String(item.agenda_item_id || item.id);
    lines.push(`### TOP ${sanitizeLine(topNumber, "-")}: ${sanitizeLine(item.title_at_return || item.title, "Ohne Titel")}`);
    lines.push(`Status: ${returnTypeLabel(item.return_type)} – Folge-TOP im TOP-Bestand angelegt.`);
    if (item.follow_up_agenda_item_id) {
      lines.push(`Folge-TOP: #${item.follow_up_agenda_item_id}${item.follow_up_title ? ` – ${sanitizeLine(item.follow_up_title, "")}` : ""}`);
    }
    if (item.returned_by_name || item.returned_at) {
      const parts = [];
      if (item.returned_by_name) parts.push(`durch ${sanitizeLine(item.returned_by_name, "")}`);
      if (item.returned_at) parts.push(`am ${formatDateTime(item.returned_at)}`);
      lines.push(`Dokumentiert ${parts.join(" ")}.`);
    }
    lines.push(`Bemerkung: ${sanitizeLine(item.remark, "-")}`);
  });

  return lines.join("\n\n");
}


function renderFollowUpHistory(item) {
  if (!item?.parent_agenda_item_id && !item?.follow_up_created_from_agenda_item_id) return [];

  const lines = ["### Historie / Rückverfolgung"];
  const parentNumber = item.parent_agenda_number || item.follow_up_source_agenda_number || item.parent_agenda_item_id || item.follow_up_created_from_agenda_item_id;
  const parentTitle = item.parent_title || item.follow_up_source_agenda_title;
  const meetingParts = [];

  if (item.follow_up_source_meeting_number || item.parent_meeting_number) {
    meetingParts.push(`Sitzung ${sanitizeLine(item.follow_up_source_meeting_number || item.parent_meeting_number, "")}`);
  }
  if (item.follow_up_source_meeting_date || item.parent_meeting_date) {
    meetingParts.push(`vom ${formatDate(item.follow_up_source_meeting_date || item.parent_meeting_date)}`);
  }

  const parentLabel = `TOP ${sanitizeLine(parentNumber, "-")}${parentTitle ? `: ${sanitizeLine(parentTitle, "")}` : ""}`;
  lines.push(
    meetingParts.length
      ? `Dieser TOP ist eine Fortsetzung von ${parentLabel} aus ${meetingParts.join(" ")}.`
      : `Dieser TOP ist eine Fortsetzung von ${parentLabel}.`
  );

  if (item.follow_up_type) {
    lines.push(`Art: ${returnTypeLabel(item.follow_up_type)}`);
  }

  if (item.follow_up_note) {
    lines.push(`Bemerkung: ${sanitizeLine(item.follow_up_note, "-")}`);
  }

  const createdParts = [];
  if (item.follow_up_created_by_name) createdParts.push(`durch ${sanitizeLine(item.follow_up_created_by_name, "")}`);
  if (item.follow_up_created_at) createdParts.push(`am ${formatDateTime(item.follow_up_created_at)}`);
  if (createdParts.length) {
    lines.push(`Folge-TOP angelegt ${createdParts.join(" ")}.`);
  }

  if (item.root_agenda_item_id && String(item.root_agenda_item_id) !== String(item.parent_agenda_item_id || "")) {
    const rootNumber = item.root_agenda_number || item.root_agenda_item_id;
    const rootTitle = item.root_title ? `: ${sanitizeLine(item.root_title, "")}` : "";
    const rootMeeting = item.root_meeting_number ? ` aus Sitzung ${sanitizeLine(item.root_meeting_number, "")}` : "";
    const rootDate = item.root_meeting_date ? ` vom ${formatDate(item.root_meeting_date)}` : "";
    lines.push(`Ursprung der Kette: TOP ${sanitizeLine(rootNumber, "-")}${rootTitle}${rootMeeting}${rootDate}.`);
  }

  return lines;
}

function renderAgendaSections(agendaItems = [], returnedItems = []) {
  if (!agendaItems.length && !returnedItems.length) {
    return "### TOP-Bestand\nFür diese Sitzung wurden keine TOPs zugeordnet.";
  }

  const assignedBlock = agendaItems.length ? agendaItems
    .map((item) => {
      const lines = [
        `## TOP ${sanitizeLine(item.agenda_number, String(item.id))}: ${sanitizeLine(item.title, "Ohne Titel")}`,
      ];

      const historyLines = renderFollowUpHistory(item);
      if (historyLines.length) {
        lines.push(...historyLines);
      }

      if (item.case_number) {
        lines.push(`Vorgang: ${item.case_number}`);
      }

      lines.push(item.description?.trim() ? item.description.trim() : "Keine zusätzliche Beschreibung hinterlegt.");

      const comments = Array.isArray(item.protocol_comments) ? item.protocol_comments : [];
      if (comments.length) {
        lines.push("### Kommentare zum TOP");
        comments.forEach((comment) => {
          const author = comment.author_name ? sanitizeLine(comment.author_name, "") : "unbekannt";
          const created = comment.created_at ? ` vom ${formatDateTime(comment.created_at)}` : "";
          lines.push(`- Kommentar von: ${author}${created}: ${sanitizeLine(comment.comment_text, "-")}`);
        });
      }

      const decisions = Array.isArray(item.decisions) ? item.decisions : [];
      if (!decisions.length) {
        lines.push("Zu diesem TOP wurde kein Beschluss dokumentiert.");
        return lines.join("\n\n");
      }

      decisions.forEach((decision) => {
        lines.push(`### Beschluss ${sanitizeLine(decision.decision_number, String(decision.id))}: ${sanitizeLine(decision.title, "Ohne Titel")}`);
        lines.push(decision.decision_text?.trim() || "Kein Beschlusstext hinterlegt.");
        lines.push(
          [
            `Ja-Stimmen: ${Number(decision.votes_yes ?? 0)}`,
            `Nein-Stimmen: ${Number(decision.votes_no ?? 0)}`,
            `Enthaltungen: ${Number(decision.votes_abstain ?? 0)}`,
          ].join(" | ")
        );
        lines.push(computeDecisionOutcome(decision));
        lines.push(`Status: ${decisionStatusLabel(decision.status)}`);

        if (decision.responsible_name) {
          lines.push(`Verantwortlich: ${decision.responsible_name}`);
        }

        if (decision.due_date) {
          lines.push(`Frist: ${formatDateTime(decision.due_date)}`);
        }
      });

      return lines.join("\n\n");
    })
    .join("\n\n") : "";

  const returnedBlock = renderReturnedAgendaSections(returnedItems);
  return [assignedBlock, returnedBlock].filter(Boolean).join("\n\n");
}

function ensureProtocolPlaceholders(template) {
  let content = String(template ?? "");

  if (!/\{\{\s*quorum_statement\s*\}\}/.test(content)) {
    if (content.includes("## Protokollnotizen")) {
      content = content.replace("## Protokollnotizen", "{{quorum_statement}}\n\n## Protokollnotizen");
    } else {
      content = `${content.trim()}\n\n{{quorum_statement}}`;
    }
  }

  if (!/\{\{\s*attendees_block\s*\}\}/.test(content)) {
    if (/\{\{\s*quorum_statement\s*\}\}/.test(content)) {
      content = content.replace(/\{\{\s*quorum_statement\s*\}\}/, "{{quorum_statement}}\n{{attendees_block}}");
    } else if (content.includes("## Protokollnotizen")) {
      content = content.replace("## Protokollnotizen", "{{attendees_block}}\n\n## Protokollnotizen");
    } else {
      content = `${content.trim()}\n\n{{attendees_block}}`;
    }
  }

  return content;
}

function replaceTokens(template, values) {
  return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
    const value = values[key];
    if (value === undefined || value === null) return "";
    return String(value);
  });
}

function linesToRtf(content, logoAsset = null) {
  const lines = String(content ?? "").replace(/\r\n/g, "\n").split("\n");
  const rtfLines = [
    "{\\rtf1\\ansi\\deff0",
    "{\\fonttbl{\\f0 Arial;}}",
    "\\paperw11907\\paperh16840\\margl1134\\margr1134\\margt1134\\margb1134",
    "\\viewkind4\\uc1",
  ];

  for (const rawLine of lines) {
    const line = rawLine ?? "";
    const trimmed = line.trim();

    if (!trimmed) {
      rtfLines.push("{\\pard\\sa140\\par}");
      continue;
    }

    if (trimmed === "VERTRAULICH") {
      rtfLines.push(`{\\pard\\qc\\sa220\\b\\fs26 ${escapeRtf(trimmed)}\\par}`);
      continue;
    }

    if (trimmed.startsWith("# ")) {
      rtfLines.push(`{\\pard\\qc\\sa220\\b\\fs32 ${escapeRtf(trimmed.slice(2))}\\par}`);
      continue;
    }

    if (trimmed.startsWith("## ")) {
      rtfLines.push(`{\\pard\\sa200\\b\\fs26 ${escapeRtf(trimmed.slice(3))}\\par}`);
      continue;
    }

    if (trimmed.startsWith("### ")) {
      rtfLines.push(`{\\pard\\sa160\\b\\fs23 ${escapeRtf(trimmed.slice(4))}\\par}`);
      continue;
    }

    if (trimmed.startsWith("- ")) {
      rtfLines.push(`{\\pard\\li540\\fi-240\\sa120\\fs22 ${escapeRtf(trimmed)}\\par}`);
      continue;
    }

    rtfLines.push(`{\\pard\\sa120\\fs22 ${escapeRtf(line)}\\par}`);
  }

  rtfLines.push("}");
  return rtfLines.join("\n");
}

function buildProtocolValues(meeting, agendaItems, attendees = [], returnedItems = []) {
  const committeeName = meeting.committee_name?.trim() || "der Firma";
  const title = meeting.title?.trim();
  const notes = meeting.protocol_text?.trim() || "Keine zusätzlichen Protokollnotizen hinterlegt.";

  return {
    committee_name: committeeName,
    meeting_number: sanitizeLine(meeting.meeting_number, "offen"),
    meeting_title_block: title ? `Titel: ${title}` : "",
    meeting_date: formatDate(meeting.meeting_date),
    meeting_type: meetingTypeLabel(meeting.meeting_type),
    start_time: formatTime(meeting.start_time),
    end_time: formatTime(meeting.end_time),
    location: sanitizeLine(meeting.location, "nicht hinterlegt"),
    chair_name: sanitizeLine(meeting.chair_name, "nicht hinterlegt"),
    secretary_name: sanitizeLine(meeting.secretary_name, "nicht hinterlegt"),
    quorum_statement: quorumStatement(meeting.is_quorate),
    attendees_block: renderAttendeesBlock(attendees),
    protocol_notes: notes,
    agenda_sections: renderAgendaSections(agendaItems, returnedItems),
    signature_place: `${sanitizeLine(meeting.location, "Ort")}, den ${formatDate(meeting.meeting_date)}`,
  };
}

function generateDocumentNumber() {
  return `D-${new Date().getFullYear()}-${Math.floor(Math.random() * 100000)
    .toString()
    .padStart(5, "0")}`;
}

async function fetchActiveTemplate(db) {
  const result = await db.query(
    `SELECT id, title, content, content_format, logo_asset_id, version_label
       FROM template_item
      WHERE category = 'meeting_protocol'
        AND is_active = TRUE
      ORDER BY updated_at DESC, id DESC
      LIMIT 1`
  );
  return result.rows[0] || null;
}

async function fetchMeetingProtocolData(db, meetingId) {
  const meetingResult = await db.query(
    `SELECT m.id,
            m.meeting_number,
            m.title,
            m.meeting_type,
            m.meeting_date,
            m.start_time,
            m.end_time,
            m.location,
            m.is_quorate,
            m.protocol_text,
            m.committee_id,
            m.chair_user_id,
            m.secretary_user_id,
            c.name AS committee_name,
            chair.full_name AS chair_name,
            secretary.full_name AS secretary_name
       FROM meeting m
       LEFT JOIN committee c ON c.id = m.committee_id
       LEFT JOIN app_user chair ON chair.id = m.chair_user_id
       LEFT JOIN app_user secretary ON secretary.id = m.secretary_user_id
      WHERE m.id = $1`,
    [meetingId]
  );

  const meeting = meetingResult.rows[0];
  if (!meeting) return null;

  const attendeeResult = meeting.committee_id
    ? await db.query(
        `SELECT DISTINCT u.id,
                u.full_name,
                u.email,
                u.role
           FROM app_user u
           LEFT JOIN committee_member cm
             ON cm.user_id = u.id
            AND cm.committee_id = $1
          WHERE u.is_active = TRUE
            AND u.role <> 'advisor'
            AND (u.committee_id = $1 OR cm.user_id IS NOT NULL)
          ORDER BY u.full_name ASC, u.id ASC`,
        [meeting.committee_id]
      )
    : await db.query(
        `SELECT DISTINCT u.id,
                u.full_name,
                u.email,
                u.role
           FROM app_user u
          WHERE u.is_active = TRUE
            AND u.role <> 'advisor'
            AND u.id = ANY($1::bigint[])
          ORDER BY u.full_name ASC, u.id ASC`,
        [[meeting.chair_user_id, meeting.secretary_user_id].filter(Boolean)]
      );

  const agendaResult = await db.query(
    `SELECT ai.id,
            ai.agenda_number,
            ai.title,
            ai.description,
            ai.duration_minutes,
            ai.case_id,
            ai.parent_agenda_item_id,
            ai.root_agenda_item_id,
            ai.follow_up_type,
            ai.follow_up_note,
            ai.follow_up_created_from_meeting_id,
            ai.follow_up_created_from_agenda_item_id,
            ai.follow_up_created_at,
            ai.follow_up_created_by,
            c.case_number,
            parent_ai.agenda_number AS parent_agenda_number,
            parent_ai.title AS parent_title,
            parent_meeting.meeting_number AS parent_meeting_number,
            parent_meeting.meeting_date AS parent_meeting_date,
            source_ai.agenda_number AS follow_up_source_agenda_number,
            source_ai.title AS follow_up_source_agenda_title,
            source_meeting.meeting_number AS follow_up_source_meeting_number,
            source_meeting.meeting_date AS follow_up_source_meeting_date,
            root_ai.agenda_number AS root_agenda_number,
            root_ai.title AS root_title,
            root_meeting.meeting_number AS root_meeting_number,
            root_meeting.meeting_date AS root_meeting_date,
            fubu.full_name AS follow_up_created_by_name
       FROM meeting_agenda_item ai
       LEFT JOIN br_case c ON c.id = ai.case_id
       LEFT JOIN meeting_agenda_item parent_ai ON parent_ai.id = ai.parent_agenda_item_id
       LEFT JOIN meeting parent_meeting ON parent_meeting.id = parent_ai.meeting_id
       LEFT JOIN meeting_agenda_item source_ai ON source_ai.id = ai.follow_up_created_from_agenda_item_id
       LEFT JOIN meeting source_meeting ON source_meeting.id = ai.follow_up_created_from_meeting_id
       LEFT JOIN meeting_agenda_item root_ai ON root_ai.id = ai.root_agenda_item_id
       LEFT JOIN meeting root_meeting ON root_meeting.id = root_ai.meeting_id
       LEFT JOIN app_user fubu ON fubu.id = ai.follow_up_created_by
      WHERE ai.meeting_id = $1
      ORDER BY
        CASE WHEN ai.agenda_number ~ '^[0-9]+$' THEN ai.agenda_number::int ELSE NULL END NULLS LAST,
        ai.agenda_number NULLS LAST,
        ai.id`,
    [meetingId]
  );

  const agendaItems = agendaResult.rows;
  const agendaIds = agendaItems.map((item) => item.id);

  const decisionMap = new Map(agendaIds.map((id) => [id, []]));
  if (agendaIds.length) {
    const decisionResult = await db.query(
      `SELECT d.id,
              d.agenda_item_id,
              d.decision_number,
              d.title,
              d.decision_text,
              d.status,
              d.votes_yes,
              d.votes_no,
              d.votes_abstain,
              d.due_date,
              u.full_name AS responsible_name
         FROM decision d
         LEFT JOIN app_user u ON u.id = d.responsible_user_id
        WHERE d.meeting_id = $1
          AND d.agenda_item_id = ANY($2::bigint[])
        ORDER BY d.created_at, d.id`,
      [meetingId, agendaIds]
    );

    decisionResult.rows.forEach((row) => {
      const bucket = decisionMap.get(row.agenda_item_id) || [];
      bucket.push(row);
      decisionMap.set(row.agenda_item_id, bucket);
    });
  }

  const commentMap = new Map(agendaIds.map((id) => [id, []]));
  if (agendaIds.length) {
    const commentResult = await db.query(
      `SELECT c.id,
              c.entity_id AS agenda_item_id,
              c.comment_text,
              c.created_at,
              u.full_name AS author_name
         FROM comment_item c
         LEFT JOIN app_user u ON u.id = c.author_user_id
        WHERE c.deleted_at IS NULL
          AND c.entity_type = 'agenda_item'
          AND COALESCE(c.include_in_protocol, FALSE) = TRUE
          AND c.entity_id = ANY($1::bigint[])
        ORDER BY c.created_at, c.id`,
      [agendaIds]
    );

    commentResult.rows.forEach((row) => {
      const bucket = commentMap.get(row.agenda_item_id) || [];
      bucket.push(row);
      commentMap.set(row.agenda_item_id, bucket);
    });
  }

  const returnedResult = await db.query(
    `SELECT l.id,
            l.agenda_item_id,
            l.meeting_id,
            l.agenda_number_at_return,
            l.title_at_return,
            l.return_type,
            l.remark,
            l.returned_at,
            l.follow_up_agenda_item_id,
            follow_up.title AS follow_up_title,
            u.full_name AS returned_by_name
       FROM meeting_agenda_item_return_log l
       LEFT JOIN meeting_agenda_item follow_up ON follow_up.id = l.follow_up_agenda_item_id
       LEFT JOIN app_user u ON u.id = l.returned_by
      WHERE l.meeting_id = $1
        AND COALESCE(l.include_in_protocol, FALSE) = TRUE
      ORDER BY l.returned_at, l.id`,
    [meetingId]
  );

  const enrichedAgenda = agendaItems.map((item) => ({
    ...item,
    decisions: decisionMap.get(item.id) || [],
    protocol_comments: commentMap.get(item.id) || [],
  }));

  return { meeting, agendaItems: enrichedAgenda, attendees: attendeeResult.rows, returnedItems: returnedResult.rows };
}

export async function ensureMeetingProtocolDocument({ db, meetingId, actorId }) {
  const existing = await db.query(
    `SELECT id, document_number, title, file_name, uploaded_at
       FROM document_item
      WHERE meeting_id = $1
        AND document_type = 'protokoll'
        AND deleted_at IS NULL
      ORDER BY uploaded_at DESC, id DESC
      LIMIT 1`,
    [meetingId]
  );

  if (existing.rows[0]) {
    return existing.rows[0];
  }

  const data = await fetchMeetingProtocolData(db, meetingId);
  if (!data) return null;

  const template = await fetchActiveTemplate(db);
  const rawTemplateContent = template?.content?.trim() || DEFAULT_MEETING_PROTOCOL_TEMPLATE;
  const normalizedTemplateContent = normalizeTemplateContentForProtocol(rawTemplateContent, template?.content_format || "markdown");
  const templateContent = ensureProtocolPlaceholders(normalizedTemplateContent);
  const templateLogo = await loadTemplateLogo(db, template);
  const values = {
    ...buildProtocolValues(data.meeting, data.agendaItems, data.attendees, data.returnedItems),
    template_logo: templateLogo ? "__BR_TEMPLATE_LOGO__" : "",
  };
  const renderedText = replaceTokens(templateContent, values);
  const rtf = linesToRtf(renderedText, templateLogo);
  const buffer = Buffer.from(rtf, "utf8");

  const filenameBase = `${data.meeting.meeting_number || `sitzung-${meetingId}`}-protokoll`;
  const safeFilename = filenameBase.replace(/[^a-zA-Z0-9._-]/g, "_");
  const storageKey = `${new Date().toISOString().slice(0, 10)}/${uuidv4()}-${safeFilename}.rtf`;
  const sha256 = crypto.createHash("sha256").update(buffer).digest("hex");
  const docMeta = await saveObject(storageKey, buffer, "application/rtf");

  const insert = await db.query(
    `INSERT INTO document_item(
       document_number,
       title,
       document_type,
       description,
       version_label,
       file_name,
       storage_path,
       storage_provider,
       mime_type,
       size_bytes,
       sha256_hash,
       confidentiality_level,
       case_id,
       meeting_id,
       decision_id,
       uploaded_by,
       scan_status,
       scan_engine,
       scan_result,
       uploaded_at,
       updated_at
     ) VALUES (
       $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NULL,$13,NULL,$14,$15,$16,$17,NOW(),NOW()
     )
     RETURNING id, document_number, title, file_name, uploaded_at`,
    [
      generateDocumentNumber(),
      `Protokoll ${data.meeting.meeting_number}`,
      "protokoll",
      "Automatisch beim Beenden der Sitzung erzeugtes Sitzungsprotokoll.",
      template?.version_label || "v1",
      `${safeFilename}.rtf`,
      docMeta.storage_path,
      docMeta.storage_provider,
      "application/rtf",
      buffer.length,
      sha256,
      "vertraulich",
      meetingId,
      actorId,
      "clean",
      "system",
      `Automatisch generiert aus Vorlage ${template?.title || "meeting_protocol"}`,
    ]
  );

  await db.query(
    `INSERT INTO document_scan_event(document_id, scanned_by_user_id, status, engine, result_text, scanned_at)
     VALUES ($1,$2,$3,$4,$5,NOW())`,
    [insert.rows[0].id, actorId, "clean", "system", "Automatisch generiertes Protokoll"]
  );

  return insert.rows[0];
}
