import express from "express";
import { z } from "zod";
import { authenticate, requireMfa } from "../../middleware/auth.js";
import { requirePermission, requireRoles } from "../../middleware/rbac.js";
import { validate } from "../../middleware/validate.js";
import { withDbSession } from "../../config/db.js";
import { writeAudit } from "../../utils/audit.js";
import { ensureMeetingProtocolDocument } from "../../services/meetingProtocol.js";
import { ensureMeetingDecisionReportDocument } from "../../services/meetingDecisionReport.js";

const router = express.Router();
router.use(authenticate);

const meetingTypeEnum = z.enum(["ordentlich", "ausserordentlich"]);
const statusEnum = z.enum(["geplant", "ungeplant", "gestartet", "beendet"]);

const nullablePositiveInt = z.preprocess(
  (value) => {
    if (value === "" || value === null || value === undefined) return undefined;
    return value;
  },
  z.coerce.number().int().positive().optional()
);

function normalizeBooleanInput(value) {
  if (value === "" || value === null || value === undefined) return undefined;
  if (typeof value === "boolean") return value;
  const normalized = String(value).trim().toLowerCase();
  if (["1", "true", "yes", "ja", "on"].includes(normalized)) return true;
  if (["0", "false", "no", "nein", "off"].includes(normalized)) return false;
  return value;
}

const requiredBooleanAnswer = z.preprocess(
  normalizeBooleanInput,
  z.boolean({
    required_error: "Bitte Beschlussfähigkeit mit Ja oder Nein bestätigen",
    invalid_type_error: "Bitte Beschlussfähigkeit mit Ja oder Nein bestätigen",
  })
);

const optionalBooleanAnswer = z.preprocess(normalizeBooleanInput, z.boolean().optional());

const dateString = z
  .string()
  .min(1, "Sitzungsdatum ist erforderlich")
  .regex(/^\d{4}-\d{2}-\d{2}$/, "Ungültiges Sitzungsdatum");

const createSchema = z.object({
  meeting_number: z.string().min(3).max(50).optional().nullable(),
  title: z.string().min(3).max(255),
  meeting_type: meetingTypeEnum.default("ordentlich"),
  meeting_date: dateString,
  start_time: z.string().optional().nullable(),
  end_time: z.string().optional().nullable(),
  location: z.string().max(255).optional().nullable(),
  status: statusEnum.default("geplant"),
  is_quorate: optionalBooleanAnswer,
  protocol_text: z.string().max(30000).optional().nullable(),
  committee_id: nullablePositiveInt,
  chair_user_id: nullablePositiveInt,
  secretary_user_id: nullablePositiveInt,
});

const updateSchema = z.object({
  title: z.string().min(3).max(255).optional(),
  meeting_type: meetingTypeEnum.optional(),
  meeting_date: dateString.optional(),
  start_time: z.string().optional().nullable(),
  end_time: z.string().optional().nullable(),
  location: z.string().max(255).optional().nullable(),
  status: statusEnum.optional(),
  is_quorate: optionalBooleanAnswer,
  protocol_text: z.string().max(30000).optional().nullable(),
  committee_id: nullablePositiveInt,
  chair_user_id: nullablePositiveInt,
  secretary_user_id: nullablePositiveInt,
});

const assignAgendaSchema = z.object({
  agenda_item_id: z.coerce.number().int().positive(),
});

const startMeetingSchema = z.object({
  is_quorate: requiredBooleanAnswer,
});

const createAndAssignAgendaSchema = z.object({
  title: z.string().min(3).max(255),
  description: z.string().max(8000).optional().nullable(),
  duration_minutes: z.coerce.number().int().min(0).max(600).optional().nullable(),
  case_id: nullablePositiveInt,
});

const agendaCommentSchema = z.object({
  comment_text: z.string().min(1).max(10000),
  visibility: z.enum(["intern", "vertraulich"]).default("intern"),
  include_in_protocol: z.coerce.boolean().optional().default(false),
});

const agendaCommentUpdateSchema = agendaCommentSchema.partial();

const agendaReturnSchema = z.object({
  remark: z.string().min(1).max(10000),
  return_type: z.enum(["rueckfrage", "klaerung", "bemerkung"]).default("rueckfrage"),
  include_in_protocol: z.coerce.boolean().optional().default(true),
});

function normalizeNullableText(value) {
  if (value === undefined) return undefined;
  if (value === null) return null;
  const trimmed = String(value).trim();
  return trimmed ? trimmed : null;
}

function normalizeOptionalText(value) {
  if (value === undefined) return undefined;
  if (value === null) return null;
  const trimmed = String(value).trim();
  return trimmed || null;
}

function normalizeNullableNumber(value) {
  if (value === undefined) return undefined;
  if (value === null || value === "") return null;
  return Number(value);
}

function parsePositiveInt(value, fallback) {
  const num = Number(value);
  if (!Number.isInteger(num) || num <= 0) return fallback;
  return num;
}

function parseBoolean(value, fallback = false) {
  if (value === undefined || value === null || value === "") return fallback;
  if (typeof value === "boolean") return value;
  const normalized = String(value).trim().toLowerCase();
  if (["1", "true", "yes", "ja", "on"].includes(normalized)) return true;
  if (["0", "false", "no", "nein", "off"].includes(normalized)) return false;
  return fallback;
}

function assertQuorumAnswer(value) {
  if (typeof value === "boolean") return;
  throw Object.assign(new Error("Beschlussfähigkeit muss bestätigt werden"), {
    status: 400,
    publicMessage: "Bitte vor dem Starten bestätigen, ob die Sitzung beschlussfähig ist.",
  });
}

function buildMeetingSearchPattern(raw) {
  const value = String(raw ?? "").trim();
  if (!value) return null;

  const escaped = value.replace(/\\/g, "\\\\");
  const hasWildcard = /[%_]/.test(escaped);

  return hasWildcard ? escaped : `%${escaped}%`;
}

function mapMeetingRow(row) {
  if (!row) return null;
  return {
    id: row.id,
    meeting_number: row.meeting_number,
    title: row.title,
    meeting_type: row.meeting_type,
    meeting_date: row.meeting_date,
    start_time: row.start_time,
    end_time: row.end_time,
    location: row.location,
    status: row.status,
    is_quorate: row.is_quorate,
    quorum_recorded_at: row.quorum_recorded_at,
    quorum_recorded_by: row.quorum_recorded_by,
    quorum_recorded_by_name: row.quorum_recorded_by_name,
    protocol_text: row.protocol_text,
    committee_id: row.committee_id,
    committee_name: row.committee_name,
    chair_user_id: row.chair_user_id,
    chair_name: row.chair_name,
    secretary_user_id: row.secretary_user_id,
    secretary_name: row.secretary_name,
    agenda_item_count: row.agenda_item_count ?? 0,
    created_at: row.created_at,
    updated_at: row.updated_at,
  };
}

async function getMeetingList(db, options = {}) {
  const page = parsePositiveInt(options.page, 1);
  const pageSize = Math.min(parsePositiveInt(options.pageSize, 25), 100);
  const showEnded = parseBoolean(options.showEnded, false);
  const searchPattern = buildMeetingSearchPattern(options.q);

  const where = [];
  const values = [];

  if (!showEnded) {
    where.push(`m.status <> 'beendet'`);
  }

  if (searchPattern) {
    values.push(searchPattern);
    const idx = values.length;

    where.push(`(
      m.meeting_number ILIKE $${idx} ESCAPE '\'
      OR m.title ILIKE $${idx} ESCAPE '\'
      OR COALESCE(m.location, '') ILIKE $${idx} ESCAPE '\'
      OR COALESCE(m.meeting_type, '') ILIKE $${idx} ESCAPE '\'
      OR COALESCE(m.status, '') ILIKE $${idx} ESCAPE '\'
      OR COALESCE(c.name, '') ILIKE $${idx} ESCAPE '\'
      OR COALESCE(cu.full_name, '') ILIKE $${idx} ESCAPE '\'
      OR COALESCE(su.full_name, '') ILIKE $${idx} ESCAPE '\'
      OR EXISTS (
        SELECT 1
          FROM meeting_agenda_item ai_search
          LEFT JOIN br_case case_search ON case_search.id = ai_search.case_id
         WHERE ai_search.meeting_id = m.id
           AND (
             COALESCE(ai_search.agenda_number, '') ILIKE $${idx} ESCAPE '\'
             OR ai_search.title ILIKE $${idx} ESCAPE '\'
             OR COALESCE(ai_search.description, '') ILIKE $${idx} ESCAPE '\'
             OR COALESCE(case_search.case_number, '') ILIKE $${idx} ESCAPE '\'
             OR COALESCE(case_search.title, '') ILIKE $${idx} ESCAPE '\'
             OR COALESCE(case_search.summary, '') ILIKE $${idx} ESCAPE '\'
           )
      )
    )`);
  }

  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";

  const countResult = await db.query(
    `
      SELECT COUNT(*)::int AS total
        FROM meeting m
        LEFT JOIN committee c ON c.id = m.committee_id
        LEFT JOIN app_user cu ON cu.id = m.chair_user_id
        LEFT JOIN app_user su ON su.id = m.secretary_user_id
        LEFT JOIN app_user qru ON qru.id = m.quorum_recorded_by
        ${whereSql}
    `,
    values
  );

  const total = countResult.rows[0]?.total ?? 0;
  const totalPages = Math.max(1, Math.ceil(total / pageSize));
  const offset = (page - 1) * pageSize;

  const listValues = [...values, pageSize, offset];
  const limitIdx = listValues.length - 1;
  const offsetIdx = listValues.length;

  const listResult = 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.status,
             m.is_quorate,
             m.quorum_recorded_at,
             m.quorum_recorded_by,
             qru.full_name AS quorum_recorded_by_name,
             m.protocol_text,
             m.committee_id,
             c.name AS committee_name,
             m.chair_user_id,
             cu.full_name AS chair_name,
             m.secretary_user_id,
             su.full_name AS secretary_name,
             m.created_at,
             m.updated_at,
             (
               SELECT COUNT(*)::int
               FROM meeting_agenda_item mai
               WHERE mai.meeting_id = m.id
             ) AS agenda_item_count
        FROM meeting m
        LEFT JOIN committee c ON c.id = m.committee_id
        LEFT JOIN app_user cu ON cu.id = m.chair_user_id
        LEFT JOIN app_user su ON su.id = m.secretary_user_id
        LEFT JOIN app_user qru ON qru.id = m.quorum_recorded_by
        ${whereSql}
       ORDER BY m.meeting_date DESC, m.start_time NULLS LAST, m.id DESC
       LIMIT $${limitIdx}
      OFFSET $${offsetIdx}
    `,
    listValues
  );

  return {
    items: listResult.rows.map(mapMeetingRow),
    total,
    page,
    page_size: pageSize,
    total_pages: totalPages,
    show_ended: showEnded,
    q: options.q ?? "",
  };
}

async function getMeetingById(db, id) {
  const result = 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.status,
             m.is_quorate,
             m.quorum_recorded_at,
             m.quorum_recorded_by,
             qru.full_name AS quorum_recorded_by_name,
             m.protocol_text,
             m.committee_id,
             c.name AS committee_name,
             m.chair_user_id,
             cu.full_name AS chair_name,
             m.secretary_user_id,
             su.full_name AS secretary_name,
             m.created_at,
             m.updated_at,
             (
               SELECT COUNT(*)::int
               FROM meeting_agenda_item mai
               WHERE mai.meeting_id = m.id
             ) AS agenda_item_count
        FROM meeting m
        LEFT JOIN committee c ON c.id = m.committee_id
        LEFT JOIN app_user cu ON cu.id = m.chair_user_id
        LEFT JOIN app_user su ON su.id = m.secretary_user_id
        LEFT JOIN app_user qru ON qru.id = m.quorum_recorded_by
       WHERE m.id = $1
    `,
    [id]
  );

  return mapMeetingRow(result.rows[0]);
}

async function assertReferences(db, payload) {
  if (payload.committee_id !== undefined && payload.committee_id !== null) {
    const ref = await db.query(`SELECT id FROM committee WHERE id = $1`, [payload.committee_id]);
    if (!ref.rows[0]) {
      throw Object.assign(new Error("Ausschuss nicht gefunden"), {
        status: 400,
        publicMessage: "Ausschuss nicht gefunden",
      });
    }
  }

  for (const [field, label] of [
    ["chair_user_id", "Leitung"],
    ["secretary_user_id", "Schriftführung"],
  ]) {
    if (payload[field] !== undefined && payload[field] !== null) {
      const ref = await db.query(
        `
          SELECT id
            FROM app_user
           WHERE id = $1
             AND is_active = TRUE
        `,
        [payload[field]]
      );

      if (!ref.rows[0]) {
        throw Object.assign(new Error(`${label} nicht gefunden`), {
          status: 400,
          publicMessage: `${label} nicht gefunden`,
        });
      }
    }
  }
}

async function completeMeetingDecisions(db, meetingId, actorId) {
  const result = await db.query(
    `UPDATE decision
        SET status = 'abgeschlossen',
            updated_by = $2,
            updated_at = NOW()
      WHERE meeting_id = $1
        AND status <> 'abgeschlossen'
      RETURNING id`,
    [meetingId, actorId]
  );

  return result.rows.map((row) => row.id);
}

async function cleanupMeetingAttachments(db, meetingId) {
  const result = await db.query(
    `WITH candidate_documents AS (
        SELECT DISTINCT d.id
          FROM document_item d
          LEFT JOIN br_case c ON c.id = d.case_id
          LEFT JOIN meeting_agenda_item ai
            ON ai.case_id = d.case_id
           AND ai.meeting_id = $1
         WHERE d.deleted_at IS NULL
           AND (
             (d.meeting_id = $1 AND COALESCE(c.legal_hold, FALSE) = FALSE)
             OR (ai.meeting_id = $1 AND COALESCE(c.legal_hold, FALSE) = FALSE)
           )
      )
      UPDATE document_item d
         SET deleted_at = NOW(),
             updated_at = NOW()
        FROM candidate_documents cd
       WHERE d.id = cd.id
       RETURNING d.id, d.file_name, d.case_id, d.meeting_id`,
    [meetingId]
  );

  return result.rows;
}

async function finalizeMeetingClosure(db, meetingId, actorId) {
  const completedDecisionIds = await completeMeetingDecisions(db, meetingId, actorId);
  const cleanedAttachments = await cleanupMeetingAttachments(db, meetingId);
  const protocolDocument = await ensureMeetingProtocolDocument({
    db,
    meetingId: Number(meetingId),
    actorId,
  });
  const decisionReportDocument = await ensureMeetingDecisionReportDocument({
    db,
    meetingId: Number(meetingId),
    actorId,
  });

  return {
    completedDecisionIds,
    cleanedAttachments,
    protocolDocument,
    decisionReportDocument,
  };
}

async function updateMeetingStatus(db, meetingId, status, actorId, options = {}) {
  const current = await getMeetingById(db, meetingId);
  if (!current) return null;

  const transitionedToEnded = status === "beendet" && current.status !== "beendet";

  let result;
  if (status === "gestartet") {
    assertQuorumAnswer(options.isQuorate);
    result = await db.query(
      `
        UPDATE meeting
           SET status = $2,
               is_quorate = $3,
               quorum_recorded_at = NOW(),
               quorum_recorded_by = $4,
               updated_by = $4
         WHERE id = $1
         RETURNING id
      `,
      [meetingId, status, options.isQuorate, actorId]
    );
  } else {
    result = await db.query(
      `
        UPDATE meeting
           SET status = $2,
               updated_by = $3
         WHERE id = $1
         RETURNING id
      `,
      [meetingId, status, actorId]
    );
  }

  if (!result.rows[0]) return null;

  let completedDecisionIds = [];
  let cleanedAttachments = [];
  let protocolDocument = null;
  let decisionReportDocument = null;

  if (transitionedToEnded) {
    const finalized = await finalizeMeetingClosure(db, meetingId, actorId);
    completedDecisionIds = finalized.completedDecisionIds;
    cleanedAttachments = finalized.cleanedAttachments;
    protocolDocument = finalized.protocolDocument;
    decisionReportDocument = finalized.decisionReportDocument;
  }

  return {
    meeting: await getMeetingById(db, meetingId),
    transitionedToEnded,
    completedDecisionIds,
    cleanedAttachments,
    protocolDocument,
    decisionReportDocument,
  };
}

async function getAgendaModalData(db, meetingId) {
  const meeting = await getMeetingById(db, meetingId);
  if (!meeting) return null;

  const [assigned, unassigned] = await Promise.all([
    db.query(
      `
        SELECT ai.id,
               ai.meeting_id,
               ai.case_id,
               c.case_number,
               ai.agenda_number,
               ai.title,
               ai.description,
               ai.duration_minutes,
               ai.status,
               ai.parent_agenda_item_id,
               parent_ai.agenda_number AS parent_agenda_number,
               parent_ai.title AS parent_title,
               ai.root_agenda_item_id,
               ai.follow_up_type,
               ai.follow_up_note,
               ai.follow_up_created_from_meeting_id,
               source_meeting.meeting_number AS follow_up_source_meeting_number,
               ai.follow_up_created_from_agenda_item_id,
               ai.follow_up_created_at,
               ai.follow_up_created_by,
               fubu.full_name AS follow_up_created_by_name,
               ai.returned_from_meeting_id,
               ai.returned_at,
               ai.returned_by,
               ru.full_name AS returned_by_name,
               COALESCE(ai.follow_up_note, last_return.remark) AS last_return_remark,
               COALESCE(ai.follow_up_type, last_return.return_type) AS last_return_type,
               last_return.follow_up_agenda_item_id,
               ai.created_at,
               (
                 SELECT COUNT(*)::int
                   FROM document_item d
                  WHERE d.deleted_at IS NULL
                    AND d.scan_status = 'clean'
                    AND d.document_type <> 'protokoll'
                    AND (
                      (ai.case_id IS NOT NULL AND d.case_id = ai.case_id)
                      OR (ai.meeting_id IS NOT NULL AND d.meeting_id = ai.meeting_id)
                    )
               ) AS report_count,
               (
                 SELECT COUNT(*)::int
                   FROM comment_item ci
                  WHERE ci.deleted_at IS NULL
                    AND ci.entity_type = 'agenda_item'
                    AND ci.entity_id = ai.id
               ) AS comment_count,
               (
                 SELECT COUNT(*)::int
                   FROM comment_item ci
                  WHERE ci.deleted_at IS NULL
                    AND ci.entity_type = 'agenda_item'
                    AND ci.entity_id = ai.id
                    AND COALESCE(ci.include_in_protocol, FALSE) = TRUE
               ) AS protocol_comment_count
          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 source_meeting ON source_meeting.id = ai.follow_up_created_from_meeting_id
          LEFT JOIN app_user fubu ON fubu.id = ai.follow_up_created_by
          LEFT JOIN app_user ru ON ru.id = ai.returned_by
          LEFT JOIN LATERAL (
            SELECT l.remark, l.return_type, l.follow_up_agenda_item_id
              FROM meeting_agenda_item_return_log l
             WHERE l.agenda_item_id = ai.id
             ORDER BY l.returned_at DESC, l.id DESC
             LIMIT 1
          ) last_return ON TRUE
         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]
    ),
    db.query(
      `
        SELECT ai.id,
               ai.meeting_id,
               ai.case_id,
               c.case_number,
               ai.agenda_number,
               ai.title,
               ai.description,
               ai.duration_minutes,
               ai.status,
               ai.parent_agenda_item_id,
               parent_ai.agenda_number AS parent_agenda_number,
               parent_ai.title AS parent_title,
               ai.root_agenda_item_id,
               ai.follow_up_type,
               ai.follow_up_note,
               ai.follow_up_created_from_meeting_id,
               source_meeting.meeting_number AS follow_up_source_meeting_number,
               ai.follow_up_created_from_agenda_item_id,
               ai.follow_up_created_at,
               ai.follow_up_created_by,
               fubu.full_name AS follow_up_created_by_name,
               ai.returned_from_meeting_id,
               ai.returned_at,
               ai.returned_by,
               ru.full_name AS returned_by_name,
               COALESCE(ai.follow_up_note, last_return.remark) AS last_return_remark,
               COALESCE(ai.follow_up_type, last_return.return_type) AS last_return_type,
               last_return.follow_up_agenda_item_id,
               ai.created_at,
               (
                 SELECT COUNT(*)::int
                   FROM document_item d
                  WHERE d.deleted_at IS NULL
                    AND d.scan_status = 'clean'
                    AND d.document_type <> 'protokoll'
                    AND ai.case_id IS NOT NULL
                    AND d.case_id = ai.case_id
               ) AS report_count
          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 source_meeting ON source_meeting.id = ai.follow_up_created_from_meeting_id
          LEFT JOIN app_user fubu ON fubu.id = ai.follow_up_created_by
          LEFT JOIN app_user ru ON ru.id = ai.returned_by
          LEFT JOIN LATERAL (
            SELECT l.remark, l.return_type, l.follow_up_agenda_item_id
              FROM meeting_agenda_item_return_log l
             WHERE l.agenda_item_id = ai.id
             ORDER BY l.returned_at DESC, l.id DESC
             LIMIT 1
          ) last_return ON TRUE
         WHERE ai.meeting_id IS NULL
         ORDER BY ai.created_at DESC, ai.id DESC
      `
    ),
  ]);

  return {
    meeting,
    assigned: assigned.rows,
    unassigned: unassigned.rows,
  };
}

async function nextAgendaNumber(db, meetingId) {
  const result = await db.query(
    `
      SELECT COALESCE(
               MAX(
                 CASE
                   WHEN agenda_number ~ '^[0-9]+$' THEN agenda_number::int
                   ELSE NULL
                 END
               ),
               0
             ) + 1 AS next_number
        FROM meeting_agenda_item
       WHERE meeting_id = $1
    `,
    [meetingId]
  );

  return String(result.rows[0].next_number);
}

async function getAgendaItemReportData(db, meetingId, agendaItemId) {
  const agendaResult = await db.query(
    `
      SELECT ai.id,
             ai.meeting_id,
             ai.case_id,
             ai.agenda_number,
             ai.title,
             ai.description,
             ai.duration_minutes,
             ai.created_at,
             c.case_number,
             c.title AS case_title,
             c.summary AS case_summary
        FROM meeting_agenda_item ai
        LEFT JOIN br_case c ON c.id = ai.case_id
       WHERE ai.id = $2::bigint
         AND ai.meeting_id = $1::bigint
    `,
    [meetingId, agendaItemId]
  );

  const agendaItem = agendaResult.rows[0];
  if (!agendaItem) return null;

  const orderBy = `
       ORDER BY CASE WHEN d.document_type = 'bericht' THEN 0 ELSE 1 END,
                d.uploaded_at DESC,
                d.id DESC
  `;

  async function runReportQuery(includeDeletedAtFilter) {
    const deletedAtFilter = includeDeletedAtFilter ? "d.deleted_at IS NULL AND" : "";

    if (agendaItem.case_id) {
      return db.query(
        `
          SELECT d.id,
                 d.document_number,
                 d.title,
                 d.document_type,
                 d.description,
                 d.file_name,
                 d.mime_type,
                 d.size_bytes,
                 d.uploaded_at,
                 d.scan_status,
                 d.case_id,
                 d.meeting_id,
                 uploader.full_name AS uploaded_by_name
            FROM document_item d
            LEFT JOIN app_user uploader ON uploader.id = d.uploaded_by
           WHERE ${deletedAtFilter}
                 d.scan_status = 'clean'
             AND d.document_type <> 'protokoll'
             AND (d.case_id = $2::bigint OR d.meeting_id = $1::bigint)
          ${orderBy}
        `,
        [meetingId, agendaItem.case_id]
      );
    }

    return db.query(
      `
        SELECT d.id,
               d.document_number,
               d.title,
               d.document_type,
               d.description,
               d.file_name,
               d.mime_type,
               d.size_bytes,
               d.uploaded_at,
               d.scan_status,
               d.case_id,
               d.meeting_id,
               uploader.full_name AS uploaded_by_name
          FROM document_item d
          LEFT JOIN app_user uploader ON uploader.id = d.uploaded_by
         WHERE ${deletedAtFilter}
               d.scan_status = 'clean'
           AND d.document_type <> 'protokoll'
           AND d.meeting_id = $1::bigint
        ${orderBy}
      `,
      [meetingId]
    );
  }

  let reportRows = [];
  try {
    reportRows = (await runReportQuery(true)).rows;
  } catch (err) {
    if (["42703", "42P18"].includes(err?.code)) {
      reportRows = (await runReportQuery(false)).rows;
    } else {
      throw err;
    }
  }

  return {
    meeting_id: Number(meetingId),
    agenda_item: agendaItem,
    reports: reportRows,
  };
}

function mapAgendaComment(row) {
  if (!row) return null;
  return {
    id: row.id,
    entity_type: row.entity_type,
    entity_id: row.entity_id,
    comment_text: row.comment_text,
    visibility: row.visibility,
    include_in_protocol: Boolean(row.include_in_protocol),
    author_user_id: row.author_user_id,
    author_name: row.author_name,
    created_at: row.created_at,
  };
}

async function assertMeetingAgendaItem(db, meetingId, agendaItemId) {
  const result = await db.query(
    `SELECT id,
            meeting_id,
            agenda_number,
            title,
            description,
            duration_minutes,
            case_id,
            status,
            parent_agenda_item_id,
            root_agenda_item_id
       FROM meeting_agenda_item
      WHERE id = $2::bigint
        AND meeting_id = $1::bigint`,
    [meetingId, agendaItemId]
  );

  const row = result.rows[0];
  if (!row) {
    throw Object.assign(new Error("TOP oder Sitzung nicht gefunden"), {
      status: 404,
      publicMessage: "TOP oder Sitzung nicht gefunden",
    });
  }
  return row;
}

async function getAgendaComments(db, meetingId, agendaItemId) {
  const agendaItem = await assertMeetingAgendaItem(db, meetingId, agendaItemId);
  const result = await db.query(
    `SELECT c.id,
            c.entity_type,
            c.entity_id,
            c.comment_text,
            c.visibility,
            COALESCE(c.include_in_protocol, FALSE) AS include_in_protocol,
            c.author_user_id,
            au.full_name AS author_name,
            c.created_at
       FROM comment_item c
       LEFT JOIN app_user au ON au.id = c.author_user_id
      WHERE c.deleted_at IS NULL
        AND c.entity_type = 'agenda_item'
        AND c.entity_id = $1::bigint
      ORDER BY c.created_at DESC, c.id DESC`,
    [agendaItemId]
  );

  return {
    meeting_id: Number(meetingId),
    agenda_item: agendaItem,
    comments: result.rows.map(mapAgendaComment),
  };
}

async function getAgendaComment(db, meetingId, agendaItemId, commentId) {
  await assertMeetingAgendaItem(db, meetingId, agendaItemId);
  const result = await db.query(
    `SELECT c.id,
            c.entity_type,
            c.entity_id,
            c.comment_text,
            c.visibility,
            COALESCE(c.include_in_protocol, FALSE) AS include_in_protocol,
            c.author_user_id,
            au.full_name AS author_name,
            c.created_at
       FROM comment_item c
       LEFT JOIN app_user au ON au.id = c.author_user_id
      WHERE c.deleted_at IS NULL
        AND c.id = $1::bigint
        AND c.entity_type = 'agenda_item'
        AND c.entity_id = $2::bigint`,
    [commentId, agendaItemId]
  );
  return mapAgendaComment(result.rows[0]);
}

router.get("/", async (req, res, next) => {
  try {
    const hasQueryMode =
      req.query.show_ended !== undefined ||
      req.query.page !== undefined ||
      req.query.page_size !== undefined ||
      req.query.q !== undefined;

    if (hasQueryMode) {
      const data = await withDbSession(req.user, (db) =>
        getMeetingList(db, {
          showEnded: req.query.show_ended,
          page: req.query.page,
          pageSize: req.query.page_size,
          q: req.query.q,
        })
      );
      return res.json(data);
    }

    const data = await withDbSession(req.user, (db) =>
      getMeetingList(db, {
        showEnded: false,
        page: 1,
        pageSize: 100,
        q: "",
      })
    );

    res.json(data.items);
  } catch (err) {
    next(err);
  }
});

router.get("/:id", async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, (db) => getMeetingById(db, req.params.id));
    if (!row) return res.status(404).json({ error: "Nicht gefunden" });
    res.json(row);
  } catch (err) {
    next(err);
  }
});

router.get("/:id/agenda-items", async (req, res, next) => {
  try {
    const data = await withDbSession(req.user, (db) => getAgendaModalData(db, req.params.id));
    if (!data) return res.status(404).json({ error: "Sitzung nicht gefunden" });
    res.json(data);
  } catch (err) {
    next(err);
  }
});

router.get("/:id/agenda-items/:agendaItemId/reports", async (req, res, next) => {
  try {
    const data = await withDbSession(req.user, (db) =>
      getAgendaItemReportData(db, req.params.id, req.params.agendaItemId)
    );
    if (!data) return res.status(404).json({ error: "TOP oder Sitzung nicht gefunden" });
    res.json(data);
  } catch (err) {
    next(err);
  }
});

router.get("/:id/agenda-items/:agendaItemId/comments", async (req, res, next) => {
  try {
    const data = await withDbSession(req.user, (db) =>
      getAgendaComments(db, req.params.id, req.params.agendaItemId)
    );
    res.json(data);
  } catch (err) {
    next(err);
  }
});

router.post(
  "/:id/agenda-items/:agendaItemId/comments",
  requireMfa,
  requirePermission("comments.write", ["admin", "chair", "secretary", "member"]),
  validate(agendaCommentSchema),
  async (req, res, next) => {
    try {
      const row = await withDbSession(req.user, async (db) => {
        await assertMeetingAgendaItem(db, req.params.id, req.params.agendaItemId);
        const result = await db.query(
          `INSERT INTO comment_item(entity_type, entity_id, comment_text, visibility, author_user_id, include_in_protocol)
           VALUES ('agenda_item', $1, $2, $3, $4, $5)
           RETURNING id`,
          [
            req.params.agendaItemId,
            req.body.comment_text.trim(),
            req.body.visibility || "intern",
            Number(req.user.sub),
            Boolean(req.body.include_in_protocol),
          ]
        );
        return getAgendaComment(db, req.params.id, req.params.agendaItemId, result.rows[0].id);
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_COMMENT_CREATED",
        entityType: "comment",
        entityId: row.id,
        ipAddress: req.ip,
        metadata: {
          meeting_id: Number(req.params.id),
          agenda_item_id: Number(req.params.agendaItemId),
          include_in_protocol: row.include_in_protocol,
        },
      });

      res.status(201).json(row);
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  "/:id/agenda-items/:agendaItemId/comments/:commentId",
  requireMfa,
  requirePermission("comments.write", ["admin", "chair", "secretary", "member"]),
  validate(agendaCommentUpdateSchema),
  async (req, res, next) => {
    try {
      const row = await withDbSession(req.user, async (db) => {
        const existing = await getAgendaComment(db, req.params.id, req.params.agendaItemId, req.params.commentId);
        if (!existing) return null;

        const fields = [];
        const values = [];
        let idx = 1;
        const push = (column, value) => {
          fields.push(`${column} = $${idx}`);
          values.push(value);
          idx += 1;
        };

        if (req.body.comment_text !== undefined) push("comment_text", req.body.comment_text.trim());
        if (req.body.visibility !== undefined) push("visibility", req.body.visibility);
        if (req.body.include_in_protocol !== undefined) push("include_in_protocol", Boolean(req.body.include_in_protocol));

        if (!fields.length) {
          throw Object.assign(new Error("Keine Felder zum Aktualisieren"), {
            status: 400,
            publicMessage: "Keine Felder zum Aktualisieren.",
          });
        }

        values.push(req.params.commentId);
        await db.query(`UPDATE comment_item SET ${fields.join(", ")} WHERE id = $${idx}`, values);
        return getAgendaComment(db, req.params.id, req.params.agendaItemId, req.params.commentId);
      });

      if (!row) return res.status(404).json({ error: "Kommentar nicht gefunden" });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_COMMENT_UPDATED",
        entityType: "comment",
        entityId: row.id,
        ipAddress: req.ip,
        metadata: {
          meeting_id: Number(req.params.id),
          agenda_item_id: Number(req.params.agendaItemId),
          changes: Object.keys(req.body || {}),
        },
      });

      res.json(row);
    } catch (err) {
      next(err);
    }
  }
);

router.delete(
  "/:id/agenda-items/:agendaItemId/comments/:commentId",
  requireMfa,
  requirePermission("comments.write", ["admin", "chair", "secretary", "member"]),
  async (req, res, next) => {
    try {
      const row = await withDbSession(req.user, async (db) => {
        const existing = await getAgendaComment(db, req.params.id, req.params.agendaItemId, req.params.commentId);
        if (!existing) return null;
        await db.query(`UPDATE comment_item SET deleted_at = NOW() WHERE id = $1`, [req.params.commentId]);
        return existing;
      });

      if (!row) return res.status(404).json({ error: "Kommentar nicht gefunden" });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_COMMENT_DELETED",
        entityType: "comment",
        entityId: row.id,
        ipAddress: req.ip,
        metadata: {
          meeting_id: Number(req.params.id),
          agenda_item_id: Number(req.params.agendaItemId),
        },
      });

      res.json({ ok: true, row });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/:id/agenda-items/assign",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(assignAgendaSchema),
  async (req, res, next) => {
    try {
      const data = await withDbSession(req.user, async (db) => {
        const meeting = await getMeetingById(db, req.params.id);
        if (!meeting) {
          throw Object.assign(new Error("Sitzung nicht gefunden"), {
            status: 404,
            publicMessage: "Sitzung nicht gefunden",
          });
        }

        const agendaItemResult = await db.query(
          `
            SELECT id, meeting_id
              FROM meeting_agenda_item
             WHERE id = $1
          `,
          [req.body.agenda_item_id]
        );

        const item = agendaItemResult.rows[0];
        if (!item) {
          throw Object.assign(new Error("TOP nicht gefunden"), {
            status: 404,
            publicMessage: "TOP nicht gefunden",
          });
        }

        if (item.meeting_id) {
          throw Object.assign(new Error("TOP ist bereits einer Sitzung zugeordnet"), {
            status: 400,
            publicMessage: "TOP ist bereits einer Sitzung zugeordnet",
          });
        }

        const agendaNumber = await nextAgendaNumber(db, req.params.id);

        await db.query(
          `
            UPDATE meeting_agenda_item
               SET meeting_id = $2,
                   agenda_number = $3,
                   status = 'in_sitzung'
             WHERE id = $1
          `,
          [req.body.agenda_item_id, req.params.id, agendaNumber]
        );

        return getAgendaModalData(db, req.params.id);
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_ITEM_ASSIGNED",
        entityType: "meeting",
        entityId: Number(req.params.id),
        ipAddress: req.ip,
        metadata: { agenda_item_id: req.body.agenda_item_id },
      });

      res.json(data);
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/:id/agenda-items/create-and-assign",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(createAndAssignAgendaSchema),
  async (req, res, next) => {
    try {
      const payload = {
        ...req.body,
        description: normalizeNullableText(req.body.description),
        case_id: normalizeNullableNumber(req.body.case_id),
      };

      const row = await withDbSession(req.user, async (db) => {
        const meeting = await getMeetingById(db, req.params.id);
        if (!meeting) {
          throw Object.assign(new Error("Sitzung nicht gefunden"), {
            status: 404,
            publicMessage: "Sitzung nicht gefunden",
          });
        }

        if (payload.case_id) {
          const caseRef = await db.query(`SELECT id FROM br_case WHERE id = $1`, [payload.case_id]);
          if (!caseRef.rows[0]) {
            throw Object.assign(new Error("Vorgang nicht gefunden"), {
              status: 400,
              publicMessage: "Vorgang nicht gefunden",
            });
          }
        }

        const agendaNumber = await nextAgendaNumber(db, req.params.id);

        const result = await db.query(
          `
            INSERT INTO meeting_agenda_item(
              meeting_id,
              case_id,
              agenda_number,
              title,
              description,
              duration_minutes,
              status
            )
            VALUES ($1, $2, $3, $4, $5, $6, 'in_sitzung')
            RETURNING *
          `,
          [
            req.params.id,
            payload.case_id ?? null,
            agendaNumber,
            payload.title.trim(),
            payload.description,
            payload.duration_minutes ?? null,
          ]
        );

        return result.rows[0];
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_ITEM_CREATED_AND_ASSIGNED",
        entityType: "meeting",
        entityId: Number(req.params.id),
        ipAddress: req.ip,
        metadata: { agenda_item_id: row.id },
      });

      res.status(201).json(row);
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/:id/agenda-items/:agendaItemId/return-to-pool",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(agendaReturnSchema),
  async (req, res, next) => {
    try {
      const payload = {
        remark: req.body.remark.trim(),
        return_type: req.body.return_type || "rueckfrage",
        include_in_protocol: Boolean(req.body.include_in_protocol ?? true),
      };

      const result = await withDbSession(req.user, async (db) => {
        const meeting = await getMeetingById(db, req.params.id);
        if (!meeting) {
          throw Object.assign(new Error("Sitzung nicht gefunden"), {
            status: 404,
            publicMessage: "Sitzung nicht gefunden",
          });
        }

        if (meeting.status === "beendet") {
          throw Object.assign(new Error("Sitzung ist beendet"), {
            status: 400,
            publicMessage: "Aus einer beendeten Sitzung kann kein Folge-TOP mehr erzeugt werden.",
          });
        }

        const agendaItem = await assertMeetingAgendaItem(db, req.params.id, req.params.agendaItemId);
        const rootAgendaItemId = agendaItem.root_agenda_item_id || agendaItem.parent_agenda_item_id || agendaItem.id;
        const followUpStatus = payload.return_type === "klaerung" ? "klaerung" : "rueckfrage";
        const followUpLabel = payload.return_type === "klaerung" ? "Klärung" : payload.return_type === "bemerkung" ? "Bemerkung / Wiedervorlage" : "Rückfrage";

        const followUpInsert = await db.query(
          `INSERT INTO meeting_agenda_item(
             meeting_id,
             agenda_number,
             title,
             description,
             duration_minutes,
             case_id,
             status,
             parent_agenda_item_id,
             root_agenda_item_id,
             follow_up_type,
             follow_up_note,
             follow_up_created_from_meeting_id,
             follow_up_created_from_agenda_item_id,
             follow_up_created_at,
             follow_up_created_by
           ) VALUES (
             NULL, NULL, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), $12
           )
           RETURNING id`,
          [
            agendaItem.title,
            agendaItem.description,
            agendaItem.duration_minutes,
            agendaItem.case_id,
            followUpStatus,
            agendaItem.id,
            rootAgendaItemId,
            payload.return_type,
            payload.remark,
            req.params.id,
            agendaItem.id,
            req.user.sub,
          ]
        );

        const followUpAgendaItemId = followUpInsert.rows[0].id;

        await db.query(
          `INSERT INTO meeting_agenda_item_return_log(
             agenda_item_id,
             meeting_id,
             agenda_number_at_return,
             title_at_return,
             return_type,
             remark,
             include_in_protocol,
             returned_by,
             follow_up_agenda_item_id
           ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
          [
            agendaItem.id,
            req.params.id,
            agendaItem.agenda_number,
            agendaItem.title,
            payload.return_type,
            payload.remark,
            payload.include_in_protocol,
            req.user.sub,
            followUpAgendaItemId,
          ]
        );

        await db.query(
          `INSERT INTO comment_item(entity_type, entity_id, comment_text, visibility, author_user_id, include_in_protocol)
           VALUES ('agenda_item', $1, $2, 'intern', $3, FALSE)`,
          [agendaItem.id, `${followUpLabel}: ${payload.remark}`, req.user.sub]
        );

        await db.query(
          `INSERT INTO comment_item(entity_type, entity_id, comment_text, visibility, author_user_id, include_in_protocol)
           VALUES ('agenda_item', $1, $2, 'intern', $3, FALSE)`,
          [followUpAgendaItemId, `Folge-TOP aus Sitzung ${meeting.meeting_number || meeting.id}: ${followUpLabel}. ${payload.remark}`, req.user.sub]
        );

        await db.query(
          `UPDATE meeting_agenda_item
              SET status = $3
            WHERE id = $1
              AND meeting_id = $2`,
          [agendaItem.id, req.params.id, followUpStatus]
        );

        return {
          data: await getAgendaModalData(db, req.params.id),
          follow_up_agenda_item_id: Number(followUpAgendaItemId),
        };
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_ITEM_FOLLOW_UP_CREATED",
        entityType: "meeting",
        entityId: Number(req.params.id),
        ipAddress: req.ip,
        metadata: {
          agenda_item_id: Number(req.params.agendaItemId),
          follow_up_agenda_item_id: result.follow_up_agenda_item_id,
          return_type: payload.return_type,
          include_in_protocol: payload.include_in_protocol,
        },
      });

      res.json(result.data);
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/:id/agenda-items/:agendaItemId/unassign",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  async (req, res, next) => {
    try {
      const data = await withDbSession(req.user, async (db) => {
        const result = await db.query(
          `
            UPDATE meeting_agenda_item
               SET meeting_id = NULL,
                   agenda_number = NULL,
                   status = 'offen'
             WHERE id = $1
               AND meeting_id = $2
           RETURNING id
          `,
          [req.params.agendaItemId, req.params.id]
        );

        if (!result.rows[0]) {
          throw Object.assign(new Error("TOP nicht gefunden"), {
            status: 404,
            publicMessage: "TOP nicht gefunden",
          });
        }

        return getAgendaModalData(db, req.params.id);
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_AGENDA_ITEM_UNASSIGNED",
        entityType: "meeting",
        entityId: Number(req.params.id),
        ipAddress: req.ip,
        metadata: { agenda_item_id: Number(req.params.agendaItemId) },
      });

      res.json(data);
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(createSchema),
  async (req, res, next) => {
    try {
      const payload = {
        ...req.body,
        meeting_number: normalizeOptionalText(req.body.meeting_number),
        title: req.body.title.trim(),
        location: normalizeNullableText(req.body.location),
        is_quorate: req.body.is_quorate,
        protocol_text: normalizeNullableText(req.body.protocol_text),
        committee_id: normalizeNullableNumber(req.body.committee_id),
        chair_user_id: normalizeNullableNumber(req.body.chair_user_id),
        secretary_user_id: normalizeNullableNumber(req.body.secretary_user_id),
      };

      const row = await withDbSession(req.user, async (db) => {
        await assertReferences(db, payload);

        if (payload.status === "gestartet") {
          assertQuorumAnswer(payload.is_quorate);
        }

        const result = await db.query(
          `
            INSERT INTO meeting(
              meeting_number,
              title,
              meeting_type,
              meeting_date,
              start_time,
              end_time,
              location,
              status,
              is_quorate,
              quorum_recorded_at,
              quorum_recorded_by,
              protocol_text,
              committee_id,
              chair_user_id,
              secretary_user_id,
              created_by,
              updated_by
            ) VALUES (
              COALESCE(NULLIF($1::text, ''), next_meeting_number($4::date)),
              $2, $3, $4, $5, $6, $7, $8, $9,
              CASE WHEN $9::boolean IS NULL THEN NULL ELSE NOW() END,
              CASE WHEN $9::boolean IS NULL THEN NULL ELSE $14 END,
              $10, $11, $12, $13, $14, $14
            )
            RETURNING id
          `,
          [
            payload.meeting_number ?? null,
            payload.title,
            payload.meeting_type,
            payload.meeting_date,
            payload.start_time || null,
            payload.end_time || null,
            payload.location,
            payload.status,
            payload.status === "gestartet" ? payload.is_quorate : null,
            payload.protocol_text,
            payload.committee_id,
            payload.chair_user_id,
            payload.secretary_user_id,
            req.user.sub,
          ]
        );

        return getMeetingById(db, result.rows[0].id);
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_CREATED",
        entityType: "meeting",
        entityId: row.id,
        ipAddress: req.ip,
        metadata: { payload },
      });

      res.status(201).json(row);
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  "/:id",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(updateSchema),
  async (req, res, next) => {
    try {
      const payload = {
        ...req.body,
        title: req.body.title !== undefined ? req.body.title.trim() : undefined,
        location: normalizeNullableText(req.body.location),
        is_quorate: req.body.is_quorate,
        protocol_text: normalizeNullableText(req.body.protocol_text),
        committee_id: normalizeNullableNumber(req.body.committee_id),
        chair_user_id: normalizeNullableNumber(req.body.chair_user_id),
        secretary_user_id: normalizeNullableNumber(req.body.secretary_user_id),
      };

      const row = await withDbSession(req.user, async (db) => {
        const existing = await getMeetingById(db, req.params.id);
        if (!existing) return null;

        await assertReferences(db, payload);

        const fields = [];
        const values = [];
        let idx = 1;

        const push = (column, value) => {
          fields.push(`${column} = $${idx}`);
          values.push(value);
          idx += 1;
        };

        const allowed = [
          ["title", payload.title],
          ["meeting_type", payload.meeting_type],
          ["meeting_date", payload.meeting_date],
          ["start_time", payload.start_time],
          ["end_time", payload.end_time],
          ["location", payload.location],
          ["status", payload.status],
          ["protocol_text", payload.protocol_text],
          ["committee_id", payload.committee_id],
          ["chair_user_id", payload.chair_user_id],
          ["secretary_user_id", payload.secretary_user_id],
        ];

        for (const [column, value] of allowed) {
          if (value !== undefined) push(column, value);
        }

        const current = await getMeetingById(db, req.params.id);
        const nextStatus = payload.status ?? current?.status;
        const transitionedToStarted = current && current.status !== "gestartet" && nextStatus === "gestartet";
        const transitionedToEnded = current && current.status !== "beendet" && nextStatus === "beendet";

        if (transitionedToStarted || payload.is_quorate !== undefined) {
          assertQuorumAnswer(payload.is_quorate);
          push("is_quorate", payload.is_quorate);
          fields.push("quorum_recorded_at = NOW()");
          push("quorum_recorded_by", req.user.sub);
        }

        if (!fields.length) {
          throw Object.assign(new Error("Keine Felder zum Aktualisieren"), {
            status: 400,
            publicMessage: "Keine Felder zum Aktualisieren",
          });
        }

        push("updated_by", req.user.sub);
        values.push(req.params.id);

        await db.query(
          `
            UPDATE meeting
               SET ${fields.join(", ")}
             WHERE id = $${idx}
             RETURNING id
          `,
          values
        );

        let completedDecisionIds = [];
        let cleanedAttachments = [];
        let protocolDocument = null;
        let decisionReportDocument = null;

        if (transitionedToEnded) {
          const finalized = await finalizeMeetingClosure(db, req.params.id, req.user.sub);
          completedDecisionIds = finalized.completedDecisionIds;
          cleanedAttachments = finalized.cleanedAttachments;
          protocolDocument = finalized.protocolDocument;
          decisionReportDocument = finalized.decisionReportDocument;
        }

        return {
          meeting: await getMeetingById(db, req.params.id),
          completedDecisionIds,
          cleanedAttachments,
          protocolDocument,
          decisionReportDocument,
        };
      });

      if (!row?.meeting) return res.status(404).json({ error: "Nicht gefunden" });

      await writeAudit({
        actorId: req.user.sub,
        action: "MEETING_UPDATED",
        entityType: "meeting",
        entityId: row.meeting.id,
        ipAddress: req.ip,
        metadata: {
          changes: payload,
          completed_decision_ids: row.completedDecisionIds || [],
          cleaned_attachment_ids: (row.cleanedAttachments || []).map((item) => item.id),
          protocol_document_id: row.protocolDocument?.id || null,
          decision_report_document_id: row.decisionReportDocument?.id || null,
        },
      });

      res.json({
        ...row.meeting,
        protocol_document: row.protocolDocument || null,
        decision_report_document: row.decisionReportDocument || null,
      });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  "/:id/start",
  requireMfa,
  requirePermission("meetings.write", ["admin", "chair", "secretary"]),
  validate(startMeetingSchema),
  async (req, res, next) => {
  try {
    const result = await withDbSession(req.user, async (db) =>
      updateMeetingStatus(db, req.params.id, "gestartet", req.user.sub, {
        isQuorate: req.body.is_quorate,
      })
    );

    if (!result?.meeting) return res.status(404).json({ error: "Nicht gefunden" });

    await writeAudit({
      actorId: req.user.sub,
      action: "MEETING_STARTED",
      entityType: "meeting",
      entityId: result.meeting.id,
      ipAddress: req.ip,
      metadata: { is_quorate: req.body.is_quorate },
    });

    res.json(result.meeting);
  } catch (err) {
    next(err);
  }
});

router.post("/:id/stop", requireMfa, requirePermission("meetings.write", ["admin", "chair", "secretary"]), async (req, res, next) => {
  try {
    const result = await withDbSession(req.user, async (db) =>
      updateMeetingStatus(db, req.params.id, "beendet", req.user.sub)
    );

    if (!result?.meeting) return res.status(404).json({ error: "Nicht gefunden" });

    await writeAudit({
      actorId: req.user.sub,
      action: "MEETING_STOPPED",
      entityType: "meeting",
      entityId: result.meeting.id,
      ipAddress: req.ip,
      metadata: {
        protocol_document_id: result.protocolDocument?.id || null,
        decision_report_document_id: result.decisionReportDocument?.id || null,
        completed_decision_ids: result.completedDecisionIds || [],
      },
    });

    res.json({
      ...result.meeting,
      protocol_document: result.protocolDocument || null,
      decision_report_document: result.decisionReportDocument || null,
    });
  } catch (err) {
    next(err);
  }
});

router.delete("/:id", requireMfa, requirePermission("meetings.write", ["admin", "chair", "secretary"]), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const existing = await getMeetingById(db, req.params.id);
      if (!existing) return null;

      await db.query(`DELETE FROM meeting WHERE id = $1`, [req.params.id]);
      return existing;
    });

    if (!row) return res.status(404).json({ error: "Nicht gefunden" });

    await writeAudit({
      actorId: req.user.sub,
      action: "MEETING_DELETED",
      entityType: "meeting",
      entityId: row.id,
      ipAddress: req.ip,
    });

    res.json({ ok: true, row });
  } catch (err) {
    next(err);
  }
});

export default router;
