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

const router = express.Router();
router.use(authenticate);
router.use(requirePermission("finance.read", ["admin", "chair"]));

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

const money = z.coerce.number().min(0).max(999999999999.99);
const taxRate = z.coerce.number().min(0).max(100);

const projectSchema = z.object({
  project_code: z.string().max(50).optional().nullable(),
  name: z.string().min(1).max(180),
  description: z.string().max(30000).optional().nullable(),
  status: z.enum(["aktiv", "pausiert", "abgeschlossen", "archiviert"]).optional().default("aktiv"),
  calculation_mode: z.enum(["sum_only", "project_budget", "annual_budget"]).optional().default("sum_only"),
  committee_id: nullablePositiveInt,
  project_budget_amount: money.optional().nullable(),
  annual_budget_amount: money.optional().nullable(),
  annual_budget_year: z.coerce.number().int().min(2000).max(2100).optional().nullable(),
});

const expenseLineSchema = z.object({
  id: nullablePositiveInt,
  description: z.string().max(255).optional().nullable(),
  tax_code_id: nullablePositiveInt,
  tax_code_key: z.string().max(160).optional().nullable(),
  tax_rate_percent: taxRate.optional().nullable(),
  net_amount: money,
});

const expenseSchema = z.object({
  project_id: z.coerce.number().int().positive(),
  expense_date: z.string().min(10).max(10),
  title: z.string().min(1).max(255),
  vendor: z.string().max(180).optional().nullable(),
  cost_category: z.string().max(120).optional().nullable(),
  net_amount: money.optional().default(0),
  tax_rate_percent: taxRate.optional().default(19),
  currency: z.string().length(3).optional().default("EUR"),
  payment_status: z.enum(["geplant", "offen", "bezahlt", "storniert"]).optional().default("offen"),
  notes: z.string().max(30000).optional().nullable(),
  lines: z.array(expenseLineSchema).optional().default([]),
});

const taxCodeSchema = z.object({
  code: z.string().min(1).max(160),
  label: z.string().min(1).max(220),
  tax_rate_percent: taxRate,
  is_active: z.boolean().optional().default(true),
  sort_order: z.coerce.number().int().min(0).max(999999).optional().default(1000),
});

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 normalizeYear(value) {
  const parsed = Number(value || new Date().getFullYear());
  if (!Number.isInteger(parsed) || parsed < 2000 || parsed > 2100) return new Date().getFullYear();
  return parsed;
}

function normalizeTaxPercent(value) {
  const raw = Number(value || 0);
  // Akzeptiert sowohl 19 als auch 0.19, falls Daten aus einer externen Liste kommen.
  const percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
  return Math.round(percent * 1000) / 1000;
}

function calculateAmounts(netAmount, taxRatePercent) {
  const net = Math.round(Number(netAmount || 0) * 100) / 100;
  const rate = normalizeTaxPercent(taxRatePercent);
  const tax = Math.round((net * rate / 100) * 100) / 100;
  const gross = Math.round((net + tax) * 100) / 100;
  return { net, rate, tax, gross };
}

function sumAmounts(lines) {
  return lines.reduce(
    (acc, line) => {
      acc.net = Math.round((acc.net + Number(line.net_amount || 0)) * 100) / 100;
      acc.tax = Math.round((acc.tax + Number(line.tax_amount || 0)) * 100) / 100;
      acc.gross = Math.round((acc.gross + Number(line.gross_amount || 0)) * 100) / 100;
      return acc;
    },
    { net: 0, tax: 0, gross: 0 }
  );
}

function normalizeDateString(value) {
  if (!value) return new Date().toISOString().slice(0, 10);
  if (value instanceof Date) return value.toISOString().slice(0, 10);
  return String(value).slice(0, 10);
}

function yearFromDate(dateString) {
  const normalized = normalizeDateString(dateString);
  const date = new Date(`${normalized}T00:00:00Z`);
  if (Number.isNaN(date.getTime())) return new Date().getFullYear();
  return date.getUTCFullYear();
}

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

function formatProjectCode(year, sequence) {
  return `FIN-${year}-${String(sequence).padStart(4, "0")}`;
}

function parseGeneratedProjectCodeYear(projectCode) {
  const match = String(projectCode || "").trim().match(/^FIN-(\d{4})-\d+$/);
  return match ? Number(match[1]) : null;
}

async function getNextProjectCode(db, year = new Date().getFullYear()) {
  const normalizedYear = normalizeYear(year);
  await db.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`finance_project_code:${normalizedYear}`]);
  const result = await db.query(
    `SELECT COALESCE(MAX((regexp_match(project_code, $1))[1]::int), 0) + 1 AS next_sequence
       FROM finance_project
      WHERE project_code ~ $2`,
    [`^FIN-${normalizedYear}-([0-9]+)$`, `^FIN-${normalizedYear}-[0-9]+$`]
  );
  return formatProjectCode(normalizedYear, result.rows[0]?.next_sequence || 1);
}

async function syncProjectBudgets(db, projectId, body, actorId) {
  const projectBudgetAmount = normalizeNullableNumber(body.project_budget_amount);
  if (projectBudgetAmount !== undefined) {
    await db.query(
      `INSERT INTO finance_project_budget(project_id, budget_type, budget_year, amount, currency, created_by, updated_by)
       VALUES ($1, 'project', NULL, $2, 'EUR', $3, $3)
       ON CONFLICT (project_id) WHERE budget_type = 'project'
       DO UPDATE SET amount = EXCLUDED.amount, updated_by = EXCLUDED.updated_by, updated_at = NOW()`,
      [projectId, projectBudgetAmount || 0, actorId]
    );
  }

  const annualBudgetAmount = normalizeNullableNumber(body.annual_budget_amount);
  const annualBudgetYear = normalizeYear(body.annual_budget_year);
  if (annualBudgetAmount !== undefined) {
    await db.query(
      `INSERT INTO finance_project_budget(project_id, budget_type, budget_year, amount, currency, created_by, updated_by)
       VALUES ($1, 'annual', $2, $3, 'EUR', $4, $4)
       ON CONFLICT (project_id, budget_year) WHERE budget_type = 'annual'
       DO UPDATE SET amount = EXCLUDED.amount, updated_by = EXCLUDED.updated_by, updated_at = NOW()`,
      [projectId, annualBudgetYear, annualBudgetAmount || 0, actorId]
    );
  }
}

async function getTaxCodeRows(db, activeOnly = false) {
  const result = await db.query(
    `SELECT id, code, label, tax_rate_percent, is_active, sort_order, created_at, updated_at
       FROM finance_tax_code
      WHERE ($1::boolean = FALSE OR is_active = TRUE)
      ORDER BY sort_order ASC, label ASC, code ASC`,
    [activeOnly]
  );
  return result.rows;
}

async function normalizeExpenseLines(db, body, old = null) {
  let sourceLines = Array.isArray(body.lines) ? body.lines : [];

  if (!sourceLines.length) {
    const fallbackNet = body.net_amount !== undefined ? body.net_amount : old?.net_amount || 0;
    const fallbackRate = body.tax_rate_percent !== undefined ? body.tax_rate_percent : old?.tax_rate_percent || 0;
    const fallbackDescription = normalizeNullableText(body.cost_category) || normalizeNullableText(body.title) || old?.cost_category || old?.title || "Rechnungsposition";
    sourceLines = [{ description: fallbackDescription, tax_code_key: fallbackDescription, net_amount: fallbackNet, tax_rate_percent: fallbackRate }];
  }

  const taxCodeIds = sourceLines.map((line) => Number(line.tax_code_id || 0)).filter(Boolean);
  const taxCodeKeys = sourceLines.map((line) => normalizeNullableText(line.tax_code_key)).filter(Boolean);
  const params = [];
  const conditions = [];

  if (taxCodeIds.length) {
    params.push(taxCodeIds);
    conditions.push(`id = ANY($${params.length}::bigint[])`);
  }
  if (taxCodeKeys.length) {
    params.push(taxCodeKeys);
    conditions.push(`code = ANY($${params.length}::text[])`);
  }

  const taxCodes = conditions.length
    ? await db.query(`SELECT id, code, label, tax_rate_percent FROM finance_tax_code WHERE ${conditions.join(" OR ")}`, params)
    : { rows: [] };
  const byId = new Map(taxCodes.rows.map((row) => [String(row.id), row]));
  const byCode = new Map(taxCodes.rows.map((row) => [row.code, row]));

  const normalized = sourceLines.map((line, index) => {
    const taxCode = byId.get(String(line.tax_code_id || "")) || byCode.get(normalizeNullableText(line.tax_code_key));
    const description = normalizeNullableText(line.description) || taxCode?.label || taxCode?.code || `Position ${index + 1}`;
    const amounts = calculateAmounts(line.net_amount, taxCode?.tax_rate_percent ?? line.tax_rate_percent ?? body.tax_rate_percent ?? 0);
    return {
      line_no: index + 1,
      description,
      tax_code_id: taxCode?.id || null,
      tax_code_key: taxCode?.code || normalizeNullableText(line.tax_code_key),
      net_amount: amounts.net,
      tax_rate_percent: amounts.rate,
      tax_amount: amounts.tax,
      gross_amount: amounts.gross,
    };
  });

  return { lines: normalized, totals: sumAmounts(normalized) };
}

async function replaceExpenseLines(db, expenseId, lines) {
  await db.query(`DELETE FROM finance_expense_line WHERE expense_id = $1::bigint`, [expenseId]);
  for (const line of lines) {
    await db.query(
      `INSERT INTO finance_expense_line(
         expense_id, line_no, description, tax_code_id, tax_code_key,
         net_amount, tax_rate_percent, tax_amount, gross_amount
       ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
      [
        expenseId,
        line.line_no,
        line.description,
        line.tax_code_id,
        line.tax_code_key,
        line.net_amount,
        line.tax_rate_percent,
        line.tax_amount,
        line.gross_amount,
      ]
    );
  }
}

async function getProjects(db, year) {
  const result = await db.query(
    `WITH expense_totals AS (
       SELECT project_id,
              COUNT(*) FILTER (WHERE deleted_at IS NULL)::int AS expense_count,
              COALESCE(SUM(net_amount) FILTER (WHERE deleted_at IS NULL AND payment_status <> 'storniert'), 0)::numeric(14,2) AS net_total,
              COALESCE(SUM(tax_amount) FILTER (WHERE deleted_at IS NULL AND payment_status <> 'storniert'), 0)::numeric(14,2) AS tax_total,
              COALESCE(SUM(gross_amount) FILTER (WHERE deleted_at IS NULL AND payment_status <> 'storniert'), 0)::numeric(14,2) AS gross_total,
              COALESCE(SUM(gross_amount) FILTER (WHERE deleted_at IS NULL AND payment_status <> 'storniert' AND budget_year = $1::int), 0)::numeric(14,2) AS gross_year_total
         FROM finance_expense
        GROUP BY project_id
     )
     SELECT p.id,
            p.project_code,
            p.name,
            p.description,
            p.status,
            p.calculation_mode,
            p.committee_id,
            c.name AS committee_name,
            p.created_at,
            p.updated_at,
            COALESCE(pb.amount, 0)::numeric(14,2) AS project_budget_amount,
            COALESCE(ab.amount, 0)::numeric(14,2) AS annual_budget_amount,
            $1::int AS annual_budget_year,
            COALESCE(et.expense_count, 0)::int AS expense_count,
            COALESCE(et.net_total, 0)::numeric(14,2) AS net_total,
            COALESCE(et.tax_total, 0)::numeric(14,2) AS tax_total,
            COALESCE(et.gross_total, 0)::numeric(14,2) AS gross_total,
            COALESCE(et.gross_year_total, 0)::numeric(14,2) AS gross_year_total,
            CASE
              WHEN p.calculation_mode = 'project_budget' THEN COALESCE(pb.amount, 0)
              WHEN p.calculation_mode = 'annual_budget' THEN COALESCE(ab.amount, 0)
              ELSE NULL
            END::numeric(14,2) AS active_budget_amount,
            CASE
              WHEN p.calculation_mode = 'project_budget' THEN COALESCE(et.gross_total, 0)
              WHEN p.calculation_mode = 'annual_budget' THEN COALESCE(et.gross_year_total, 0)
              ELSE COALESCE(et.gross_total, 0)
            END::numeric(14,2) AS active_spent_amount,
            CASE
              WHEN p.calculation_mode = 'project_budget' THEN COALESCE(pb.amount, 0) - COALESCE(et.gross_total, 0)
              WHEN p.calculation_mode = 'annual_budget' THEN COALESCE(ab.amount, 0) - COALESCE(et.gross_year_total, 0)
              ELSE NULL
            END::numeric(14,2) AS active_remaining_amount
       FROM finance_project p
       LEFT JOIN committee c ON c.id = p.committee_id
       LEFT JOIN expense_totals et ON et.project_id = p.id
       LEFT JOIN finance_project_budget pb ON pb.project_id = p.id AND pb.budget_type = 'project'
       LEFT JOIN finance_project_budget ab ON ab.project_id = p.id AND ab.budget_type = 'annual' AND ab.budget_year = $1::int
      WHERE p.deleted_at IS NULL
      ORDER BY p.status, p.name`,
    [year]
  );
  return result.rows;
}

async function getExpenseRows(db, { year, projectId }) {
  const params = [year, projectId || null];
  const result = await db.query(
    `SELECT e.id,
            e.project_id,
            p.name AS project_name,
            e.expense_date,
            e.budget_year,
            e.title,
            e.vendor,
            e.cost_category,
            e.net_amount,
            e.tax_rate_percent,
            e.tax_amount,
            e.gross_amount,
            e.currency,
            e.payment_status,
            e.notes,
            e.created_at,
            e.updated_at,
            COALESCE((
              SELECT jsonb_agg(jsonb_build_object(
                'id', l.id,
                'line_no', l.line_no,
                'description', l.description,
                'tax_code_id', l.tax_code_id,
                'tax_code_key', l.tax_code_key,
                'tax_code_label', tc.label,
                'net_amount', l.net_amount,
                'tax_rate_percent', l.tax_rate_percent,
                'tax_amount', l.tax_amount,
                'gross_amount', l.gross_amount
              ) ORDER BY l.line_no ASC, l.id ASC)
                FROM finance_expense_line l
                LEFT JOIN finance_tax_code tc ON tc.id = l.tax_code_id
               WHERE l.expense_id = e.id
            ), '[]'::jsonb) AS lines,
            COALESCE((
              SELECT jsonb_agg(jsonb_build_object(
                'id', d.id,
                'document_number', d.document_number,
                'title', d.title,
                'file_name', d.file_name,
                'mime_type', d.mime_type,
                'size_bytes', d.size_bytes,
                'scan_status', d.scan_status,
                'uploaded_at', d.uploaded_at
              ) ORDER BY d.uploaded_at DESC, d.id DESC)
                FROM finance_expense_document fed
                JOIN document_item d ON d.id = fed.document_id
               WHERE fed.expense_id = e.id
                 AND d.deleted_at IS NULL
            ), '[]'::jsonb) AS documents
       FROM finance_expense e
       JOIN finance_project p ON p.id = e.project_id
      WHERE e.deleted_at IS NULL
        AND ($1::int IS NULL OR e.budget_year = $1::int)
        AND ($2::bigint IS NULL OR e.project_id = $2::bigint)
      ORDER BY e.expense_date DESC, e.id DESC`,
    params
  );
  return result.rows;
}

router.get("/meta", async (req, res, next) => {
  try {
    const data = await withDbSession(req.user, async (db) => {
      const [committees, years, taxCodes] = await Promise.all([
        db.query(`SELECT id, name FROM committee ORDER BY name`),
        db.query(
          `SELECT DISTINCT budget_year AS year
             FROM finance_expense
            WHERE deleted_at IS NULL
           UNION
           SELECT DISTINCT budget_year AS year
             FROM finance_project_budget
            WHERE budget_year IS NOT NULL
           ORDER BY year DESC`
        ),
        getTaxCodeRows(db, true),
      ]);

      return {
        tax_rates: [0, 7, 19],
        tax_codes: taxCodes,
        payment_statuses: ["geplant", "offen", "bezahlt", "storniert"],
        calculation_modes: [
          { value: "sum_only", label: "Nur aufsummieren" },
          { value: "project_budget", label: "Vom Projektbudget abziehen" },
          { value: "annual_budget", label: "Vom Jahresbudget abziehen" },
        ],
        committees: committees.rows,
        years: years.rows.map((row) => row.year),
      };
    });
    res.json(data);
  } catch (err) {
    next(err);
  }
});

router.get("/tax-codes", requirePermission("finance.tax_codes", ["admin", "chair"]), async (req, res, next) => {
  try {
    const activeOnly = req.query.active === "true";
    const rows = await withDbSession(req.user, (db) => getTaxCodeRows(db, activeOnly));
    res.json(rows);
  } catch (err) {
    next(err);
  }
});

router.post("/tax-codes", requireMfa, requirePermission("finance.tax_codes", ["admin", "chair"]), validate(taxCodeSchema), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const result = await db.query(
        `INSERT INTO finance_tax_code(code, label, tax_rate_percent, is_active, sort_order, created_by, updated_by)
         VALUES ($1,$2,$3,$4,$5,$6,$6)
         RETURNING *`,
        [
          req.body.code.trim(),
          req.body.label.trim(),
          normalizeTaxPercent(req.body.tax_rate_percent),
          req.body.is_active,
          req.body.sort_order,
          req.user.sub,
        ]
      );
      return result.rows[0];
    });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_TAX_CODE_CREATED", entityType: "finance_tax_code", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.status(201).json(row);
  } catch (err) {
    if (err.code === "23505") return res.status(409).json({ error: "Diese MwSt.-/Kostenart existiert bereits." });
    next(err);
  }
});

router.put("/tax-codes/:id", requireMfa, requirePermission("finance.tax_codes", ["admin", "chair"]), validate(taxCodeSchema.partial()), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const current = await db.query(`SELECT * FROM finance_tax_code WHERE id = $1::bigint`, [req.params.id]);
      if (!current.rows[0]) return null;
      const old = current.rows[0];
      const result = await db.query(
        `UPDATE finance_tax_code
            SET code = $2,
                label = $3,
                tax_rate_percent = $4,
                is_active = $5,
                sort_order = $6,
                updated_by = $7,
                updated_at = NOW()
          WHERE id = $1::bigint
          RETURNING *`,
        [
          req.params.id,
          req.body.code !== undefined ? req.body.code.trim() : old.code,
          req.body.label !== undefined ? req.body.label.trim() : old.label,
          req.body.tax_rate_percent !== undefined ? normalizeTaxPercent(req.body.tax_rate_percent) : old.tax_rate_percent,
          req.body.is_active !== undefined ? req.body.is_active : old.is_active,
          req.body.sort_order !== undefined ? req.body.sort_order : old.sort_order,
          req.user.sub,
        ]
      );
      return result.rows[0];
    });
    if (!row) return res.status(404).json({ error: "MwSt.-/Kostenart nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_TAX_CODE_UPDATED", entityType: "finance_tax_code", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.json(row);
  } catch (err) {
    if (err.code === "23505") return res.status(409).json({ error: "Diese MwSt.-/Kostenart existiert bereits." });
    next(err);
  }
});

router.delete("/tax-codes/:id", requireMfa, requirePermission("finance.tax_codes", ["admin", "chair"]), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const result = await db.query(
        `UPDATE finance_tax_code
            SET is_active = FALSE, updated_by = $2, updated_at = NOW()
          WHERE id = $1::bigint
          RETURNING id, code, label`,
        [req.params.id, req.user.sub]
      );
      return result.rows[0] || null;
    });
    if (!row) return res.status(404).json({ error: "MwSt.-/Kostenart nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_TAX_CODE_DISABLED", entityType: "finance_tax_code", entityId: row.id, metadata: row, ipAddress: req.ip });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});

router.get("/projects", async (req, res, next) => {
  try {
    const year = normalizeYear(req.query.year);
    const rows = await withDbSession(req.user, (db) => getProjects(db, year));
    res.json(rows);
  } catch (err) {
    next(err);
  }
});

router.get("/projects/next-code", requirePermission("finance.write", ["admin", "chair"]), async (req, res, next) => {
  try {
    const year = normalizeYear(req.query.year);
    const projectCode = await withDbSession(req.user, (db) => getNextProjectCode(db, year));
    res.json({ project_code: projectCode, year });
  } catch (err) {
    next(err);
  }
});

router.post("/projects", requireMfa, requirePermission("finance.write", ["admin", "chair"]), validate(projectSchema), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const requestedProjectCode = normalizeNullableText(req.body.project_code);
      const requestedProjectCodeYear = parseGeneratedProjectCodeYear(requestedProjectCode);
      let projectCode = requestedProjectCode;

      if (requestedProjectCodeYear) {
        await db.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`finance_project_code:${requestedProjectCodeYear}`]);
        const duplicate = await db.query(`SELECT 1 FROM finance_project WHERE project_code = $1`, [requestedProjectCode]);
        if (duplicate.rows[0]) {
          projectCode = await getNextProjectCode(db, requestedProjectCodeYear);
        }
      }

      if (!projectCode) {
        projectCode = await getNextProjectCode(db, req.body.annual_budget_year);
      }

      const insert = await db.query(
        `INSERT INTO finance_project(project_code, name, description, status, calculation_mode, committee_id, created_by, updated_by)
         VALUES ($1,$2,$3,$4,$5,$6,$7,$7)
         RETURNING *`,
        [
          projectCode,
          req.body.name,
          normalizeNullableText(req.body.description),
          req.body.status,
          req.body.calculation_mode,
          normalizeNullableNumber(req.body.committee_id),
          req.user.sub,
        ]
      );
      const project = insert.rows[0];
      await syncProjectBudgets(db, project.id, req.body, req.user.sub);
      return project;
    });

    await writeAudit({ actorId: req.user.sub, action: "FINANCE_PROJECT_CREATED", entityType: "finance_project", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.status(201).json(row);
  } catch (err) {
    if (err.code === "23505") return res.status(409).json({ error: "Die Projekt-Nr. ist bereits vergeben." });
    next(err);
  }
});

router.put("/projects/:id", requireMfa, requirePermission("finance.write", ["admin", "chair"]), validate(projectSchema.partial()), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const current = await db.query(`SELECT id FROM finance_project WHERE id = $1::bigint AND deleted_at IS NULL`, [req.params.id]);
      if (!current.rows[0]) return null;

      const result = await db.query(
        `UPDATE finance_project
            SET project_code = COALESCE($2, project_code),
                name = COALESCE($3, name),
                description = $4,
                status = COALESCE($5, status),
                calculation_mode = COALESCE($6, calculation_mode),
                committee_id = $7,
                updated_by = $8,
                updated_at = NOW()
          WHERE id = $1::bigint
          RETURNING *`,
        [
          req.params.id,
          normalizeNullableText(req.body.project_code),
          req.body.name,
          normalizeNullableText(req.body.description),
          req.body.status,
          req.body.calculation_mode,
          normalizeNullableNumber(req.body.committee_id),
          req.user.sub,
        ]
      );
      await syncProjectBudgets(db, req.params.id, req.body, req.user.sub);
      return result.rows[0];
    });

    if (!row) return res.status(404).json({ error: "Finanzprojekt nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_PROJECT_UPDATED", entityType: "finance_project", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.json(row);
  } catch (err) {
    if (err.code === "23505") return res.status(409).json({ error: "Die Projekt-Nr. ist bereits vergeben." });
    next(err);
  }
});

router.delete("/projects/:id", requireMfa, requirePermission("finance.delete", ["admin", "chair"]), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const result = await db.query(
        `UPDATE finance_project
            SET deleted_at = NOW(), updated_by = $2, updated_at = NOW()
          WHERE id = $1::bigint AND deleted_at IS NULL
          RETURNING id, name`,
        [req.params.id, req.user.sub]
      );
      return result.rows[0] || null;
    });
    if (!row) return res.status(404).json({ error: "Finanzprojekt nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_PROJECT_DELETED", entityType: "finance_project", entityId: row.id, metadata: row, ipAddress: req.ip });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});

router.get("/expenses", async (req, res, next) => {
  try {
    const year = req.query.year ? normalizeYear(req.query.year) : null;
    const projectId = req.query.project_id ? Number(req.query.project_id) : null;
    const rows = await withDbSession(req.user, (db) => getExpenseRows(db, { year, projectId }));
    res.json(rows);
  } catch (err) {
    next(err);
  }
});

router.post("/expenses", requireMfa, requirePermission("finance.write", ["admin", "chair"]), validate(expenseSchema), async (req, res, next) => {
  try {
    const budgetYear = yearFromDate(req.body.expense_date);

    const row = await withDbSession(req.user, async (db) => {
      const { lines, totals } = await normalizeExpenseLines(db, req.body);
      const primaryLine = lines[0];
      const result = await db.query(
        `INSERT INTO finance_expense(
           project_id, expense_date, budget_year, title, vendor, cost_category,
           net_amount, tax_rate_percent, tax_amount, gross_amount, currency, payment_status, notes,
           created_by, updated_by
         ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$14)
         RETURNING *`,
        [
          req.body.project_id,
          req.body.expense_date,
          budgetYear,
          req.body.title,
          normalizeNullableText(req.body.vendor),
          normalizeNullableText(req.body.cost_category) || primaryLine?.tax_code_key || primaryLine?.description || null,
          totals.net,
          primaryLine?.tax_rate_percent || 0,
          totals.tax,
          totals.gross,
          (req.body.currency || "EUR").toUpperCase(),
          req.body.payment_status,
          normalizeNullableText(req.body.notes),
          req.user.sub,
        ]
      );
      await replaceExpenseLines(db, result.rows[0].id, lines);
      return result.rows[0];
    });

    await writeAudit({ actorId: req.user.sub, action: "FINANCE_EXPENSE_CREATED", entityType: "finance_expense", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.status(201).json(row);
  } catch (err) {
    next(err);
  }
});

router.put("/expenses/:id", requireMfa, requirePermission("finance.write", ["admin", "chair"]), validate(expenseSchema.partial()), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const current = await db.query(`SELECT * FROM finance_expense WHERE id = $1::bigint AND deleted_at IS NULL`, [req.params.id]);
      const old = current.rows[0];
      if (!old) return null;

      const nextBody = {
        project_id: req.body.project_id ?? old.project_id,
        expense_date: req.body.expense_date || normalizeDateString(old.expense_date),
        title: req.body.title ?? old.title,
        vendor: req.body.vendor !== undefined ? req.body.vendor : old.vendor,
        cost_category: req.body.cost_category !== undefined ? req.body.cost_category : old.cost_category,
        net_amount: req.body.net_amount !== undefined ? req.body.net_amount : old.net_amount,
        tax_rate_percent: req.body.tax_rate_percent !== undefined ? req.body.tax_rate_percent : old.tax_rate_percent,
        currency: req.body.currency || old.currency,
        payment_status: req.body.payment_status || old.payment_status,
        notes: req.body.notes !== undefined ? req.body.notes : old.notes,
        lines: req.body.lines,
      };
      const { lines, totals } = await normalizeExpenseLines(db, nextBody, old);
      const primaryLine = lines[0];
      const budgetYear = yearFromDate(nextBody.expense_date);

      const result = await db.query(
        `UPDATE finance_expense
            SET project_id = $2,
                expense_date = $3,
                budget_year = $4,
                title = $5,
                vendor = $6,
                cost_category = $7,
                net_amount = $8,
                tax_rate_percent = $9,
                tax_amount = $10,
                gross_amount = $11,
                currency = $12,
                payment_status = $13,
                notes = $14,
                updated_by = $15,
                updated_at = NOW()
          WHERE id = $1::bigint
          RETURNING *`,
        [
          req.params.id,
          nextBody.project_id,
          nextBody.expense_date,
          budgetYear,
          nextBody.title,
          normalizeNullableText(nextBody.vendor),
          normalizeNullableText(nextBody.cost_category) || primaryLine?.tax_code_key || primaryLine?.description || null,
          totals.net,
          primaryLine?.tax_rate_percent || 0,
          totals.tax,
          totals.gross,
          nextBody.currency.toUpperCase(),
          nextBody.payment_status,
          normalizeNullableText(nextBody.notes),
          req.user.sub,
        ]
      );
      await replaceExpenseLines(db, req.params.id, lines);
      return result.rows[0];
    });

    if (!row) return res.status(404).json({ error: "Ausgabe nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_EXPENSE_UPDATED", entityType: "finance_expense", entityId: row.id, metadata: req.body, ipAddress: req.ip });
    res.json(row);
  } catch (err) {
    next(err);
  }
});

router.delete("/expenses/:id", requireMfa, requirePermission("finance.delete", ["admin", "chair"]), async (req, res, next) => {
  try {
    const row = await withDbSession(req.user, async (db) => {
      const result = await db.query(
        `UPDATE finance_expense
            SET deleted_at = NOW(), updated_by = $2, updated_at = NOW()
          WHERE id = $1::bigint AND deleted_at IS NULL
          RETURNING id, title`,
        [req.params.id, req.user.sub]
      );
      return result.rows[0] || null;
    });
    if (!row) return res.status(404).json({ error: "Ausgabe nicht gefunden" });
    await writeAudit({ actorId: req.user.sub, action: "FINANCE_EXPENSE_DELETED", entityType: "finance_expense", entityId: row.id, metadata: row, ipAddress: req.ip });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});

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

      const scan = await scanBuffer(req.file.buffer);
      if (!scan.clean) {
        await writeAudit({ actorId: req.user.sub, action: "FINANCE_DOCUMENT_REJECTED", entityType: "finance_expense", entityId: Number(req.params.id), metadata: { scan: scan.raw, file_name: req.file.originalname }, ipAddress: req.ip });
        return res.status(422).json({ error: "Datei durch Virenscan abgelehnt", scan_result: scan.raw });
      }

      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 storageKey = `finance/${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 document = await withDbSession(req.user, async (db) => {
        const expense = await db.query(
          `SELECT e.id, e.title, e.project_id, p.name AS project_name
             FROM finance_expense e
             JOIN finance_project p ON p.id = e.project_id
            WHERE e.id = $1::bigint
              AND e.deleted_at IS NULL`,
          [req.params.id]
        );
        if (!expense.rows[0]) return null;

        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, uploaded_by, scan_status, scan_engine, scan_result, uploaded_at
           ) VALUES ($1,$2,'finance_receipt',$3,'v1',$4,$5,$6,$7,$8,$9,'intern',$10,'clean','clamav',$11,NOW())
           RETURNING id, document_number, title, file_name, mime_type, size_bytes, scan_status, uploaded_at`,
          [
            generateDocumentNumber(),
            normalizeNullableText(req.body.title) || `Beleg: ${expense.rows[0].title}`,
            normalizeNullableText(req.body.description) || `Finanzbeleg zu Ausgabe ${expense.rows[0].title} im Projekt ${expense.rows[0].project_name}`,
            req.file.originalname,
            docMeta.storage_path,
            docMeta.storage_provider,
            detectedMime,
            req.file.size,
            sha256,
            req.user.sub,
            scan.raw,
          ]
        );

        const row = insert.rows[0];
        await db.query(
          `UPDATE document_item
              SET version_number = 1,
                  version_group_id = id
            WHERE id = $1::bigint`,
          [row.id]
        );
        await db.query(
          `INSERT INTO finance_expense_document(expense_id, document_id, created_by)
           VALUES ($1,$2,$3)
           ON CONFLICT DO NOTHING`,
          [req.params.id, row.id, req.user.sub]
        );
        await db.query(
          `INSERT INTO document_scan_event(document_id, scanned_by_user_id, status, engine, result_text, scanned_at)
           VALUES ($1,$2,'clean','clamav',$3,NOW())`,
          [row.id, req.user.sub, scan.raw]
        );
        return row;
      });

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

      await writeAudit({
        actorId: req.user.sub,
        action: "FINANCE_DOCUMENT_UPLOADED",
        entityType: "document",
        entityId: document.id,
        metadata: { expense_id: Number(req.params.id), sha256, storage_provider: docMeta.storage_provider },
        ipAddress: req.ip,
      });

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

async function getFinanceDocument(user, documentId) {
  return withDbSession(user, async (db) => {
    const result = await db.query(
      `SELECT d.id, d.title, d.file_name, d.storage_path, d.mime_type, d.scan_status
         FROM document_item d
         JOIN finance_expense_document fed ON fed.document_id = d.id
         JOIN finance_expense fe ON fe.id = fed.expense_id AND fe.deleted_at IS NULL
        WHERE d.id = $1::bigint
          AND d.deleted_at IS NULL
        LIMIT 1`,
      [documentId]
    );
    return result.rows[0] || null;
  });
}

async function streamFinanceDocument(req, res, next, disposition = "attachment") {
  try {
    const row = await getFinanceDocument(req.user, req.params.id);
    if (!row) return res.status(404).json({ error: "Beleg nicht gefunden" });
    if (row.scan_status !== "clean") return res.status(409).json({ error: "Beleg nicht freigegeben" });

    const { stream, contentType } = await getObjectStream(row.storage_path);
    const safeFilename = encodeURIComponent(row.file_name || row.title || `beleg-${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");

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

    stream.pipe(res);
  } catch (err) {
    next(err);
  }
}

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

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

export default router;
