import express from "express";
import crypto from "crypto";
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import { z } from "zod";
import { authenticate, requireMfa } from "../../middleware/auth.js";
import { requirePermission } from "../../middleware/rbac.js";
import { query, withDbSession } from "../../config/db.js";
import { upload } from "../../middleware/upload.js";
import { scanBuffer } from "../../services/clamav.js";
import { saveObject, getObjectStream, deleteObject } from "../../services/storage.js";
import { writeAudit } from "../../utils/audit.js";
import { fileTypeFromBuffer } from "file-type";
import { v4 as uuidv4 } from "uuid";
import { validate } from "../../middleware/validate.js";
import { env } from "../../config/env.js";

export const publicDocumentsRouter = express.Router();
const router = express.Router();

const PUBLIC_DOCUMENTS_DEFAULT_INTRO = "Vom Betriebsrat freigegebene Dokumente";
const PUBLIC_DOCUMENTS_TOKEN_SCOPE = "public_documents";
const PUBLIC_DOCUMENTS_TOKEN_EXPIRES_IN = "4h";

router.use(authenticate);

const confEnum = z.enum(["oeffentlich", "intern", "vertraulich", "streng_vertraulich"]);
const nullablePositiveInt = z.preprocess(
  (value) => {
    if (value === "" || value === null || value === undefined) return undefined;
    return value;
  },
  z.coerce.number().int().positive().optional().nullable()
);

const publicDocumentAccessSchema = z.object({
  password: z.string().min(1).max(200),
});

const publicDocumentSettingsSchema = z.object({
  enabled: z.boolean().optional(),
  intro_text: z.string().max(8000).optional().nullable(),
  access_password: z.string().max(200).optional().or(z.literal("")),
  clear_password: z.boolean().optional(),
});

const updateSchema = z.object({
  document_number: z.string().min(1).max(50).optional(),
  title: z.string().min(1).max(255).optional(),
  document_type: z.string().min(1).max(50).optional(),
  description: z.string().max(30000).optional().nullable(),
  confidentiality_level: confEnum.optional(),
  document_category_id: nullablePositiveInt,
  meeting_id: nullablePositiveInt,
  team_role_ids: z.array(z.coerce.number().int().positive()).optional(),
});

function normalizeNullableText(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 generateDocumentNumber() {
  return `D-${new Date().getFullYear()}-${Math.floor(Math.random() * 100000)
    .toString()
    .padStart(5, "0")}`;
}

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 parseTeamRoleIds(value) {
  if (Array.isArray(value)) return value.map(Number).filter(Boolean);
  if (value === undefined || value === null || value === "") return [];
  try {
    const parsed = JSON.parse(String(value));
    if (Array.isArray(parsed)) return parsed.map(Number).filter(Boolean);
  } catch {
    // Fallback: comma-separated form value.
  }
  return String(value)
    .split(",")
    .map((item) => Number(item.trim()))
    .filter(Boolean);
}

function signPublicDocumentToken() {
  return jwt.sign(
    { scope: PUBLIC_DOCUMENTS_TOKEN_SCOPE, setting_id: 1 },
    env.jwtAccessSecret,
    { expiresIn: PUBLIC_DOCUMENTS_TOKEN_EXPIRES_IN }
  );
}

function verifyPublicDocumentToken(token) {
  try {
    const decoded = jwt.verify(token, env.jwtAccessSecret);
    return decoded?.scope === PUBLIC_DOCUMENTS_TOKEN_SCOPE && Number(decoded?.setting_id) === 1;
  } catch {
    return false;
  }
}

function getPublicAccessToken(req) {
  const auth = req.headers.authorization || "";
  if (auth.toLowerCase().startsWith("bearer ")) return auth.slice(7).trim();
  return String(req.query.access_token || req.body?.public_access_token || "").trim();
}

function mapCategory(row) {
  if (!row) return null;
  return {
    id: row.id,
    code: row.code,
    label: row.label,
    description: row.description,
    sort_order: row.sort_order,
    is_active: row.is_active,
  };
}

async function getPublicDocumentSetting(db = null) {
  const executor = db || { query };
  const result = await executor.query(
    `SELECT id, enabled, intro_text, access_password_hash, updated_at
       FROM public_document_setting
      WHERE id = 1`
  );
  return result.rows[0] || { enabled: false, intro_text: PUBLIC_DOCUMENTS_DEFAULT_INTRO, access_password_hash: null };
}

function mapPublicDocument(row) {
  if (!row) return null;
  return {
    id: row.id,
    document_number: row.document_number,
    title: row.title,
    description: row.description,
    document_type: row.document_type,
    category_label: row.category_label,
    file_name: row.file_name,
    uploaded_at: row.uploaded_at,
  };
}

async function sendDocumentFile(row, res, disposition = "attachment") {
  const { stream, contentType } = await getObjectStream(row.storage_path);
  const safeFilename = encodeURIComponent(row.file_name || row.title || `dokument-${row.id}`);
  res.setHeader("Content-Type", contentType || row.mime_type || "application/octet-stream");
  res.setHeader("Content-Disposition", `${disposition}; filename="${safeFilename}"`);
  res.setHeader("Cache-Control", "no-store");
  stream.pipe(res);
}

publicDocumentsRouter.get("/status", async (req, res, next) => {
  try {
    const setting = await getPublicDocumentSetting();
    const enabled = Boolean(setting?.enabled && setting?.access_password_hash);
    const authenticated = enabled && verifyPublicDocumentToken(getPublicAccessToken(req));

    const documents = authenticated
      ? await query(
          `SELECT d.id,
                  d.document_number,
                  d.title,
                  d.description,
                  d.document_type,
                  d.file_name,
                  d.uploaded_at,
                  dc.label AS category_label
             FROM document_item d
             LEFT JOIN document_category_catalog dc ON dc.id = d.document_category_id
            WHERE d.deleted_at IS NULL
              AND d.scan_status = 'clean'
              AND d.confidentiality_level = 'oeffentlich'
            ORDER BY COALESCE(dc.sort_order, 999999), d.uploaded_at DESC, d.id DESC`
        ).then((result) => result.rows.map(mapPublicDocument))
      : [];

    res.json({
      enabled,
      password_required: true,
      authenticated,
      intro_text: setting?.intro_text || PUBLIC_DOCUMENTS_DEFAULT_INTRO,
      documents,
    });
  } catch (err) {
    next(err);
  }
});

publicDocumentsRouter.post("/access", validate(publicDocumentAccessSchema), async (req, res, next) => {
  try {
    const setting = await getPublicDocumentSetting();
    if (!setting?.enabled) {
      return res.status(404).json({ error: "Die öffentliche Dokumentenseite ist derzeit nicht freigeschaltet." });
    }
    if (!setting.access_password_hash) {
      return res.status(403).json({ error: "Für die öffentliche Dokumentenseite ist noch kein Kennwort gesetzt." });
    }

    const ok = await argon2.verify(setting.access_password_hash, req.body.password);
    if (!ok) {
      return res.status(401).json({ error: "Kennwort ist nicht korrekt." });
    }

    res.json({ accessToken: signPublicDocumentToken(), expiresIn: PUBLIC_DOCUMENTS_TOKEN_EXPIRES_IN });
  } catch (err) {
    next(err);
  }
});

async function streamPublicDocument(req, res, next, disposition = "attachment") {
  try {
    const setting = await getPublicDocumentSetting();
    if (!setting?.enabled || !setting?.access_password_hash) {
      return res.status(404).json({ error: "Die öffentliche Dokumentenseite ist derzeit nicht freigeschaltet." });
    }
    if (!verifyPublicDocumentToken(getPublicAccessToken(req))) {
      return res.status(401).json({ error: "Zugriff abgelaufen. Bitte Kennwort erneut eingeben." });
    }

    const result = await query(
      `SELECT id, title, file_name, storage_path, mime_type, scan_status
         FROM document_item
        WHERE id = $1::bigint
          AND deleted_at IS NULL
          AND scan_status = 'clean'
          AND confidentiality_level = 'oeffentlich'`,
      [req.params.id]
    );
    const row = result.rows[0];
    if (!row) return res.status(404).json({ error: "Dokument nicht gefunden" });

    await writeAudit({
      actorId: null,
      action: disposition === "inline" ? "PUBLIC_DOCUMENT_VIEWED" : "PUBLIC_DOCUMENT_DOWNLOADED",
      entityType: "document",
      entityId: row.id,
      ipAddress: req.ip,
    });

    await sendDocumentFile(row, res, disposition);
  } catch (err) {
    next(err);
  }
}

publicDocumentsRouter.get("/:id/view", async (req, res, next) => {
  await streamPublicDocument(req, res, next, "inline");
});

publicDocumentsRouter.get("/:id/download", async (req, res, next) => {
  await streamPublicDocument(req, res, next, "attachment");
});

async function syncDocumentTeamAccess(db, documentId, teamRoleIds, actorId) {
  if (!Array.isArray(teamRoleIds)) return;
  await db.query(`DELETE FROM document_team_access WHERE document_id = $1`, [documentId]);
  if (!teamRoleIds.length) return;

  await db.query(
    `INSERT INTO document_team_access(document_id, team_role_id, can_read, can_write, created_by)
     SELECT $1::bigint, unnest($2::bigint[]), TRUE, FALSE, $3::bigint
     ON CONFLICT (document_id, team_role_id) DO UPDATE SET can_read = TRUE`,
    [documentId, teamRoleIds.map(Number), actorId]
  );
}

async function getBaseDocumentForVersioning(db, requestedId) {
  if (!requestedId) return null;

  const result = await db.query(
    `SELECT id,
            document_number,
            title,
            document_type,
            description,
            confidentiality_level,
            document_category_id,
            meeting_id,
            COALESCE(version_group_id, id) AS version_root_id
       FROM document_item
      WHERE id = $1::bigint
        AND deleted_at IS NULL`,
    [requestedId]
  );

  return result.rows[0] || null;
}

async function getNextVersionNumber(db, versionRootId) {
  const result = await db.query(
    `SELECT COALESCE(MAX(COALESCE(version_number, 1)), 0) + 1 AS next_version
       FROM document_item
      WHERE COALESCE(version_group_id, id) = $1::bigint`,
    [versionRootId]
  );

  return Number(result.rows[0]?.next_version || 1);
}

router.get("/meta", async (req, res, next) => {
  try {
    const data = await withDbSession(req.user, async (db) => {
      const [activeMeetings, teamRoles, categories] = await Promise.all([
        db.query(
          `SELECT id, meeting_number, title, meeting_date
             FROM meeting
            WHERE status = 'gestartet'
            ORDER BY meeting_date DESC, created_at DESC`
        ),
        db.query(
          `SELECT id, code, name, can_view_cases, can_write_cases, can_view_documents, can_write_documents
             FROM app_team_role
            WHERE is_active = TRUE
            ORDER BY name`
        ),
        db.query(
          `SELECT id, code, label, description, sort_order, is_active
             FROM document_category_catalog
            WHERE is_active = TRUE
            ORDER BY sort_order, label`
        ),
      ]);

      return {
        activeMeeting: activeMeetings.rows.length === 1 ? activeMeetings.rows[0] : null,
        teamRoles: teamRoles.rows,
        categories: categories.rows.map(mapCategory),
      };
    });

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

router.get("/", async (req, res, next) => {
  try {
    const internalOnly = parseBoolean(req.query.internal_only, false);

    const rows = await withDbSession(req.user, async (db) => {
      const result = await db.query(
        `WITH ranked_documents AS (
           SELECT d.id,
                  d.document_number,
                  d.title,
                  d.document_type,
                  d.description,
                  d.version_label,
                  COALESCE(d.version_number, 1) AS version_number,
                  COALESCE(d.version_group_id, d.id) AS version_root_id,
                  COUNT(*) OVER (PARTITION BY COALESCE(d.version_group_id, d.id))::int AS version_count,
                  d.confidentiality_level,
                  d.document_category_id,
                  dc.code AS category_code,
                  dc.label AS category_label,
                  d.case_id,
                  d.meeting_id,
                  d.decision_id,
                  d.storage_provider,
                  d.scan_status,
                  d.uploaded_at,
                  d.file_name,
                  m.meeting_number,
                  uploader.full_name AS uploaded_by_name,
                  uploader.email AS uploaded_by_email,
                  COALESCE((
                    SELECT ARRAY_AGG(dta.team_role_id ORDER BY dta.team_role_id)
                      FROM document_team_access dta
                     WHERE dta.document_id = d.id
                       AND dta.can_read = TRUE
                  ), ARRAY[]::BIGINT[]) AS team_role_ids,
                  ROW_NUMBER() OVER (
                    PARTITION BY COALESCE(d.version_group_id, d.id)
                    ORDER BY COALESCE(d.version_number, 1) DESC, d.uploaded_at DESC, d.id DESC
                  ) AS version_rank
             FROM document_item d
             LEFT JOIN meeting m ON m.id = d.meeting_id
             LEFT JOIN document_category_catalog dc ON dc.id = d.document_category_id
             LEFT JOIN app_user uploader ON uploader.id = d.uploaded_by
            WHERE d.deleted_at IS NULL
              AND ($1::boolean = FALSE OR (d.case_id IS NULL AND d.decision_id IS NULL))
         )
         SELECT id,
                document_number,
                title,
                document_type,
                description,
                version_label,
                version_number,
                version_root_id,
                version_count,
                confidentiality_level,
                document_category_id,
                category_code,
                category_label,
                case_id,
                meeting_id,
                decision_id,
                storage_provider,
                scan_status,
                uploaded_at,
                file_name,
                meeting_number,
                uploaded_by_name,
                uploaded_by_email,
                team_role_ids
           FROM ranked_documents
          WHERE version_rank = 1
          ORDER BY uploaded_at DESC, id DESC`,
        [internalOnly]
      );
      return result.rows;
    });
    res.json(rows);
  } catch (err) {
    next(err);
  }
});

router.get("/:id/versions", async (req, res, next) => {
  try {
    const payload = await withDbSession(req.user, async (db) => {
      const current = await db.query(
        `SELECT id,
                document_number,
                title,
                COALESCE(version_group_id, id) AS version_root_id
           FROM document_item
          WHERE id = $1::bigint
            AND deleted_at IS NULL`,
        [req.params.id]
      );

      const row = current.rows[0];
      if (!row) return null;

      const versions = await db.query(
        `SELECT d.id,
                d.document_number,
                d.title,
                d.document_type,
                d.description,
                d.version_label,
                COALESCE(d.version_number, 1) AS version_number,
                d.file_name,
                d.mime_type,
                d.size_bytes,
                d.scan_status,
                d.uploaded_at,
                d.meeting_id,
                m.meeting_number,
                uploader.full_name AS uploaded_by_name,
                uploader.email AS uploaded_by_email
           FROM document_item d
           LEFT JOIN meeting m ON m.id = d.meeting_id
           LEFT JOIN app_user uploader ON uploader.id = d.uploaded_by
          WHERE COALESCE(d.version_group_id, d.id) = $1::bigint
            AND d.deleted_at IS NULL
          ORDER BY COALESCE(d.version_number, 1) DESC, d.uploaded_at DESC, d.id DESC`,
        [row.version_root_id]
      );

      return {
        document_number: row.document_number,
        title: row.title,
        version_root_id: row.version_root_id,
        items: versions.rows,
      };
    });

    if (!payload) return res.status(404).json({ error: "Dokument nicht gefunden" });
    res.json(payload);
  } catch (err) {
    next(err);
  }
});

router.post(
  "/upload",
  requireMfa,
  requirePermission("documents.write", ["admin", "chair", "secretary", "member"]),
  upload.single("file"),
  async (req, res, next) => {
    try {
      if (!req.file) return res.status(400).json({ error: "Datei fehlt" });

      const mime = await fileTypeFromBuffer(req.file.buffer);
      const detectedMime = mime?.mime || req.file.mimetype || "application/octet-stream";
      const sha256 = crypto.createHash("sha256").update(req.file.buffer).digest("hex");

      const scan = await scanBuffer(req.file.buffer);
      const storageKey = `${new Date().toISOString().slice(0, 10)}/${uuidv4()}-${req.file.originalname.replace(/[^a-zA-Z0-9._-]/g, "_")}`;

      const docMeta = await saveObject(storageKey, req.file.buffer, detectedMime);
      const versionOfId = normalizeNullableNumber(req.body.version_of_id);
      const teamRoleIds = parseTeamRoleIds(req.body.team_role_ids);

      const result = await withDbSession(req.user, async (db) => {
        let baseDocument = null;
        let documentNumber = normalizeNullableText(req.body.document_number) || generateDocumentNumber();
        let versionNumber = 1;
        let versionLabel = "v1";
        let versionRootId = null;

        if (versionOfId) {
          baseDocument = await getBaseDocumentForVersioning(db, versionOfId);
          if (!baseDocument) {
            throw Object.assign(new Error("Basisdokument für Versionierung nicht gefunden"), {
              status: 400,
              publicMessage: "Basisdokument für Versionierung nicht gefunden",
            });
          }

          versionRootId = Number(baseDocument.version_root_id || baseDocument.id);
          versionNumber = await getNextVersionNumber(db, versionRootId);
          versionLabel = `v${versionNumber}`;
          documentNumber = baseDocument.document_number;
        }

        const insert = await db.query(
          `INSERT INTO document_item(
             document_number, title, document_type, description, version_label, version_number, version_group_id,
             file_name, storage_path, storage_provider, mime_type, size_bytes, sha256_hash,
             confidentiality_level, document_category_id, case_id, meeting_id, decision_id, uploaded_by,
             scan_status, scan_engine, scan_result, uploaded_at
           ) VALUES (
             $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,NOW()
           ) RETURNING *`,
          [
            documentNumber,
            normalizeNullableText(req.body.title) || baseDocument?.title || req.file.originalname,
            normalizeNullableText(req.body.document_type) || baseDocument?.document_type || "intern",
            normalizeNullableText(req.body.description) ?? baseDocument?.description ?? null,
            versionLabel,
            versionNumber,
            versionRootId,
            req.file.originalname,
            docMeta.storage_path,
            docMeta.storage_provider,
            detectedMime,
            req.file.size,
            sha256,
            req.body.confidentiality_level || baseDocument?.confidentiality_level || "intern",
            normalizeNullableNumber(req.body.document_category_id) ?? baseDocument?.document_category_id ?? null,
            req.body.case_id || null,
            normalizeNullableNumber(req.body.meeting_id) ?? baseDocument?.meeting_id ?? null,
            req.body.decision_id || null,
            req.user.sub,
            scan.clean ? "clean" : "infected",
            "clamav",
            scan.raw,
          ]
        );

        const inserted = insert.rows[0];
        await syncDocumentTeamAccess(db, inserted.id, teamRoleIds, req.user.sub);

        if (!versionRootId) {
          await db.query(
            `UPDATE document_item
                SET version_group_id = $2::bigint
              WHERE id = $1::bigint`,
            [inserted.id, inserted.id]
          );
          inserted.version_group_id = inserted.id;
        }

        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())`,
          [inserted.id, req.user.sub, scan.clean ? "clean" : "infected", "clamav", scan.raw]
        );

        return inserted;
      });

      await writeAudit({
        actorId: req.user.sub,
        action: versionOfId ? "DOCUMENT_VERSION_UPLOADED" : "DOCUMENT_UPLOADED",
        entityType: "document",
        entityId: result.id,
        metadata: { sha256, scan: scan.raw, storage_provider: docMeta.storage_provider, version_of_id: versionOfId, team_role_ids: teamRoleIds },
        ipAddress: req.ip,
      });

      if (!scan.clean) {
        return res.status(422).json({ error: "Datei durch Virenscan abgelehnt", scan_result: scan.raw });
      }

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

router.get(
  "/public-settings",
  requirePermission("public_documents.configure", ["admin", "chair"]),
  async (req, res, next) => {
    try {
      const data = await withDbSession(req.user, async (db) => {
        const setting = await getPublicDocumentSetting(db);
        const documentsResult = await db.query(
          `SELECT d.id,
                  d.document_number,
                  d.title,
                  d.description,
                  d.document_type,
                  d.confidentiality_level,
                  d.scan_status,
                  d.file_name,
                  d.uploaded_at,
                  dc.label AS category_label
             FROM document_item d
             LEFT JOIN document_category_catalog dc ON dc.id = d.document_category_id
            WHERE d.deleted_at IS NULL
              AND d.confidentiality_level = 'oeffentlich'
            ORDER BY COALESCE(dc.sort_order, 999999), d.uploaded_at DESC, d.id DESC`
        );

        return {
          settings: {
            enabled: Boolean(setting?.enabled),
            password_set: Boolean(setting?.access_password_hash),
            intro_text: setting?.intro_text || PUBLIC_DOCUMENTS_DEFAULT_INTRO,
            updated_at: setting?.updated_at || null,
          },
          public_path: "/public-documents",
          documents: documentsResult.rows,
        };
      });
      res.json(data);
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  "/public-settings",
  requireMfa,
  requirePermission("public_documents.configure", ["admin", "chair"]),
  validate(publicDocumentSettingsSchema),
  async (req, res, next) => {
    try {
      const passwordInput = typeof req.body.access_password === "string" ? req.body.access_password.trim() : "";
      const payload = {
        enabled: Boolean(req.body.enabled),
        intro_text: normalizeNullableText(req.body.intro_text) || PUBLIC_DOCUMENTS_DEFAULT_INTRO,
        access_password: passwordInput,
        clear_password: Boolean(req.body.clear_password),
      };

      if (payload.access_password && payload.access_password.length < 4) {
        throw Object.assign(new Error("Kennwort zu kurz"), {
          status: 400,
          publicMessage: "Das globale Kennwort muss mindestens 4 Zeichen lang sein.",
        });
      }

      const data = await withDbSession(req.user, async (db) => {
        const current = await getPublicDocumentSetting(db);
        let passwordHash = current?.access_password_hash || null;

        if (payload.clear_password && !payload.access_password) {
          passwordHash = null;
        }
        if (payload.access_password) {
          passwordHash = await argon2.hash(payload.access_password);
        }

        if (payload.enabled && !passwordHash) {
          throw Object.assign(new Error("Kennwort erforderlich"), {
            status: 400,
            publicMessage: "Zum Einschalten der öffentlichen Dokumentenseite muss zuerst ein Kennwort eingegeben und gespeichert werden.",
          });
        }

        await db.query(
          `INSERT INTO public_document_setting(id, enabled, intro_text, access_password_hash, created_by, updated_by)
           VALUES (1, $1, $2, $3, $4, $4)
           ON CONFLICT (id) DO UPDATE
             SET enabled = EXCLUDED.enabled,
                 intro_text = EXCLUDED.intro_text,
                 access_password_hash = EXCLUDED.access_password_hash,
                 updated_by = EXCLUDED.updated_by`,
          [payload.enabled, payload.intro_text, passwordHash, req.user.sub]
        );
        const setting = await getPublicDocumentSetting(db);
        return {
          settings: {
            enabled: Boolean(setting?.enabled),
            password_set: Boolean(setting?.access_password_hash),
            intro_text: setting?.intro_text || PUBLIC_DOCUMENTS_DEFAULT_INTRO,
            updated_at: setting?.updated_at || null,
          },
          public_path: "/public-documents",
        };
      });

      await writeAudit({
        actorId: req.user.sub,
        action: "PUBLIC_DOCUMENT_SETTINGS_UPDATED",
        entityType: "public_document_setting",
        entityId: 1,
        metadata: { enabled: data.settings.enabled, password_set: data.settings.password_set },
        ipAddress: req.ip,
      });

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

router.put(
  "/:id",
  requireMfa,
  requirePermission("documents.write", ["admin", "chair", "secretary", "member"]),
  validate(updateSchema),
  async (req, res, next) => {
    try {
      const payload = {
        document_number: normalizeNullableText(req.body.document_number),
        title: normalizeNullableText(req.body.title),
        document_type: normalizeNullableText(req.body.document_type),
        description: normalizeNullableText(req.body.description),
        confidentiality_level: req.body.confidentiality_level,
        document_category_id: normalizeNullableNumber(req.body.document_category_id),
        meeting_id: normalizeNullableNumber(req.body.meeting_id),
        team_role_ids: Array.isArray(req.body.team_role_ids) ? req.body.team_role_ids.map(Number).filter(Boolean) : undefined,
      };

      const row = await withDbSession(req.user, async (db) => {
        const current = await db.query(
          `SELECT id, storage_path FROM document_item WHERE id = $1::bigint AND deleted_at IS NULL`,
          [req.params.id]
        );

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

        const result = await db.query(
          `UPDATE document_item
              SET document_number = COALESCE($2, document_number),
                  title = COALESCE($3, title),
                  document_type = COALESCE($4, document_type),
                  description = $5,
                  confidentiality_level = COALESCE($6, confidentiality_level),
                  document_category_id = $7,
                  meeting_id = $8,
                  case_id = NULL,
                  decision_id = NULL,
                  updated_at = NOW()
            WHERE id = $1::bigint
            RETURNING id, document_number, title, document_type, description, version_label, COALESCE(version_number, 1) AS version_number, confidentiality_level, document_category_id, meeting_id, file_name, scan_status, uploaded_at`,
          [
            req.params.id,
            payload.document_number,
            payload.title,
            payload.document_type,
            payload.description,
            payload.confidentiality_level,
            payload.document_category_id,
            payload.meeting_id,
          ]
        );

        const updated = result.rows[0];
        if (payload.team_role_ids !== undefined) {
          await syncDocumentTeamAccess(db, req.params.id, payload.team_role_ids || [], req.user.sub);
        }
        return updated;
      });

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

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

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


router.delete("/:id", requireMfa, requirePermission("documents.write", ["admin", "chair", "secretary", "member"]), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const current = await db.query(
        `SELECT id, storage_path, file_name
           FROM document_item
          WHERE id = $1::bigint AND deleted_at IS NULL`,
        [req.params.id]
      );

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

      await db.query(
        `UPDATE document_item
            SET deleted_at = NOW(),
                updated_at = NOW()
          WHERE id = $1::bigint`,
        [req.params.id]
      );

      return current.rows[0];
    });

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

    await deleteObject(row.storage_path).catch(() => undefined);

    await writeAudit({
      actorId: req.user.sub,
      action: "DOCUMENT_DELETED",
      entityType: "document",
      entityId: row.id,
      metadata: { file_name: row.file_name },
      ipAddress: req.ip,
    });

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

async function getDocumentForTransfer(user, documentId) {
  return withDbSession(user, async (db) => {
    const result = await db.query(
      `SELECT id, title, file_name, storage_path, mime_type, scan_status
         FROM document_item
        WHERE id = $1::bigint AND deleted_at IS NULL`,
      [documentId]
    );
    return result.rows[0];
  });
}

async function streamDocument(req, res, next, disposition = "attachment") {
  try {
    const row = await getDocumentForTransfer(req.user, req.params.id);

    if (!row) return res.status(404).json({ error: "Dokument nicht gefunden" });
    if (row.scan_status !== "clean") return res.status(409).json({ error: "Dokument nicht freigegeben" });

    await writeAudit({
      actorId: req.user.sub,
      action: disposition === "inline" ? "DOCUMENT_VIEWED" : "DOCUMENT_DOWNLOADED",
      entityType: "document",
      entityId: row.id,
      ipAddress: req.ip,
    });

    await sendDocumentFile(row, res, disposition);
  } catch (err) {
    next(err);
  }
}

router.get("/:id/download", requireMfa, async (req, res, next) => {
  await streamDocument(req, res, next, "attachment");
});

router.get("/:id/view", requireMfa, async (req, res, next) => {
  await streamDocument(req, res, next, "inline");
});

export default router;
