// ==================== CACHED EMAIL CONTROLLER ====================

import { Request, Response } from 'express';
import * as cachedEmailService from '../services/cachedEmail.service.js';
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
import * as invoiceService from '../services/invoice.service.js';
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js';
import { fetchAttachment, appendToSent, ImapCredentials, humanizeImapError, findJunkFolderPath } from '../services/imapService.js';
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
import { decrypt } from '../utils/encryption.js';
import { sanitizeNotes, stripHtml, validateContractDocumentType, validateOptionalIsoDate, assertSafePdf } from '../utils/sanitize.js';
import { ApiError } from '../utils/apiError.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js';
import { generateEmailPdf } from '../services/pdfService.js';
import { maybeActivateOnDeliveryConfirmation, withContractDocumentLock } from '../services/contractStatusScheduler.service.js';
import { DocumentType } from '@prisma/client';
import prisma from '../lib/prisma.js';
import path from 'path';
import fs from 'fs';
import {
  canAccessCustomer,
  canAccessContract,
  canAccessCachedEmail,
  canAccessStressfreiEmail,
} from '../utils/accessControl.js';

// ==================== E-MAIL LIST ====================

// Hilfsfunktion: Query-Param zu boolean parsen ('true' / 'false' / fehlt).
function parseBoolParam(v: unknown): boolean | undefined {
  if (v === 'true') return true;
  if (v === 'false') return false;
  return undefined;
}

function parseDateParam(v: unknown): Date | undefined {
  if (typeof v !== 'string' || !v.trim()) return undefined;
  const d = new Date(v);
  return isNaN(d.getTime()) ? undefined : d;
}

// Pentest 91.1 (LOW, 2026-06-21): `accountId=abc` → `parseInt` = `NaN`
// → der Ternary gab `NaN` an den Service. `if (NaN)` ist falsy → der
// Postfach-Filter fiel weg, und der Vertrag zeigte Mails aus ALLEN
// Postfächern.
//
// Pentest 92 (LOW, 2026-06-21): Bei `accountId=abc` auf Vertrags-
// Endpunkten reichte das silent-undefined nicht – die Mailbox-Isolation
// brach (man sah Mails aus allen Postfächern statt 0). Strict-400, weil
// Verträge per Design IMMER ein bestimmtes Postfach meinen.
//
// Helper hat zwei Modi:
//   - default (optional):  fehlend/leer  → undefined (kein Filter)
//                          invalid       → 400
//   - { required: true }:  fehlend/leer  → 400
//                          invalid       → 400
// Bei 400 schreibt der Helper direkt die Response und gibt `null`
// zurück; der Caller bricht dann mit `return` ab.
function parsePositiveIntQuery(
  v: unknown,
  fieldLabel: string,
  res: Response,
  options?: { required?: boolean },
): number | undefined | null {
  // Pentest 93.1 (INFO, 2026-06-21): `?accountId=` (explizit-leer) wurde
  // wie `?accountId` weggelassen behandelt → 200 statt 400 auf optionalen
  // Endpunkten. Spec sagt aber: leerer String ist KEINE gültige Zahl.
  // Trennung jetzt strikt:
  //   - Param fehlt komplett (`undefined`) → "absent"
  //   - Param da, aber Wert leer/Whitespace/keine Zahl → invalid → 400
  if (v === undefined) {
    if (options?.required) {
      res.status(400).json({
        success: false,
        error: `${fieldLabel} ist erforderlich (positive Ganzzahl).`,
      } as ApiResponse);
      return null;
    }
    return undefined;
  }
  if (typeof v !== 'string') {
    res.status(400).json({
      success: false,
      error: `${fieldLabel} muss als Zahl übergeben werden.`,
    } as ApiResponse);
    return null;
  }
  const trimmed = v.trim();
  if (trimmed === '') {
    res.status(400).json({
      success: false,
      error: `${fieldLabel} darf nicht leer sein – bitte weglassen oder positive Ganzzahl angeben.`,
    } as ApiResponse);
    return null;
  }
  const n = parseInt(trimmed, 10);
  if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
    res.status(400).json({
      success: false,
      error: `${fieldLabel} muss eine positive Ganzzahl sein.`,
    } as ApiResponse);
    return null;
  }
  return n;
}

// E-Mails für einen Kunden abrufen
export async function getEmailsForCustomer(req: AuthRequest, res: Response): Promise<void> {
  try {
    const customerId = parseInt(req.params.customerId);
    if (!(await canAccessCustomer(req, res, customerId))) return;
    // Customer-Inbox: accountId ist legitim optional (cross-mailbox-Ansicht
    // ist erwünscht), aber invalide Werte → 400.
    const stressfreiEmailId = parsePositiveIntQuery(req.query.accountId, 'accountId', res);
    if (stressfreiEmailId === null) return;
    const folder = req.query.folder as string | undefined; // INBOX oder SENT
    const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
    const offset = req.query.offset ? parseInt(req.query.offset as string) : 0;

    const emails = await cachedEmailService.getCachedEmails({
      customerId,
      stressfreiEmailId,
      folder,
      limit,
      offset,
      includeBody: false,
      search: typeof req.query.search === 'string' ? req.query.search : undefined,
      fromFilter: typeof req.query.fromFilter === 'string' ? req.query.fromFilter : undefined,
      toFilter: typeof req.query.toFilter === 'string' ? req.query.toFilter : undefined,
      subjectFilter: typeof req.query.subjectFilter === 'string' ? req.query.subjectFilter : undefined,
      bodyFilter: typeof req.query.bodyFilter === 'string' ? req.query.bodyFilter : undefined,
      attachmentNameFilter: typeof req.query.attachmentNameFilter === 'string' ? req.query.attachmentNameFilter : undefined,
      hasAttachments: parseBoolParam(req.query.hasAttachments),
      isRead: parseBoolParam(req.query.isRead),
      isStarred: parseBoolParam(req.query.isStarred),
      receivedFrom: parseDateParam(req.query.receivedFrom),
      receivedTo: parseDateParam(req.query.receivedTo),
    });

    res.json({ success: true, data: emails } as ApiResponse);
  } catch (error) {
    console.error('getEmailsForCustomer error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der E-Mails',
    } as ApiResponse);
  }
}

// E-Mails für einen Vertrag abrufen.
// `accountId` (optional) schränkt zusätzlich auf ein bestimmtes Postfach
// ein – ohne, sieht man im Vertrags-Tab Mails aus ALLEN Postfächern des
// Kunden, die dem Vertrag zugeordnet sind (User-Bug 2026-06-21).
export async function getEmailsForContract(req: AuthRequest, res: Response): Promise<void> {
  try {
    const contractId = parseInt(req.params.contractId);
    if (!(await canAccessContract(req, res, contractId))) return;
    const folder = req.query.folder as string | undefined; // INBOX oder SENT
    // Vertrags-Endpunkte sind per Design IMMER pro Postfach – fehlt
    // accountId, ist die Abfrage semantisch ungültig. Strict-400.
    // Frontend hat eh ein `enabled: !!selectedAccountId`-Guard.
    const stressfreiEmailId = parsePositiveIntQuery(req.query.accountId, 'accountId', res, { required: true });
    if (stressfreiEmailId === null || stressfreiEmailId === undefined) return;
    const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
    const offset = req.query.offset ? parseInt(req.query.offset as string) : 0;

    const emails = await cachedEmailService.getCachedEmails({
      contractId,
      stressfreiEmailId,
      folder,
      limit,
      offset,
      includeBody: false,
    });

    res.json({ success: true, data: emails } as ApiResponse);
  } catch (error) {
    console.error('getEmailsForContract error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Vertrags-E-Mails',
    } as ApiResponse);
  }
}

// ==================== SINGLE EMAIL ====================

// Einzelne E-Mail abrufen (mit Body)
export async function getEmail(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;

    const email = await cachedEmailService.getCachedEmailById(id);

    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Als gelesen markieren
    await cachedEmailService.markEmailAsRead(id);

    res.json({ success: true, data: { ...email, isRead: true } } as ApiResponse);
  } catch (error) {
    console.error('getEmail error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der E-Mail',
    } as ApiResponse);
  }
}

// E-Mail als gelesen/ungelesen markieren
export async function markAsRead(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;
    const { isRead } = req.body;

    if (isRead) {
      await cachedEmailService.markEmailAsRead(id);
    } else {
      await cachedEmailService.markEmailAsUnread(id);
    }

    res.json({ success: true } as ApiResponse);
  } catch (error) {
    console.error('markAsRead error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Markieren der E-Mail',
    } as ApiResponse);
  }
}

// E-Mail Stern umschalten
export async function toggleStar(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;
    const isStarred = await cachedEmailService.toggleEmailStar(id);

    res.json({ success: true, data: { isStarred } } as ApiResponse);
  } catch (error) {
    console.error('toggleStar error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Ändern des Sterns',
    } as ApiResponse);
  }
}

// ==================== CONTRACT ASSIGNMENT ====================

// E-Mail einem Vertrag zuordnen
export async function assignToContract(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const { contractId } = req.body;
    if (!(await canAccessContract(req, res, contractId))) return;
    const userId = (req as any).userId; // Falls Auth-Middleware userId setzt

    const email = await cachedEmailService.assignEmailToContract(emailId, contractId, userId);

    res.json({ success: true, data: email } as ApiResponse);
  } catch (error) {
    console.error('assignToContract error:', error);
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Zuordnen der E-Mail',
    } as ApiResponse);
  }
}

// Vertragszuordnung aufheben
export async function unassignFromContract(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;

    const email = await cachedEmailService.unassignEmailFromContract(emailId);

    res.json({ success: true, data: email } as ApiResponse);
  } catch (error) {
    console.error('unassignFromContract error:', error);
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Aufheben der Zuordnung',
    } as ApiResponse);
  }
}

// E-Mail-Anzahl pro Ordner für ein Konto
export async function getFolderCounts(req: AuthRequest, res: Response): Promise<void> {
  try {
    const stressfreiEmailId = parseInt(req.params.id);
    if (!(await canAccessStressfreiEmail(req, res, stressfreiEmailId))) return;

    const counts = await cachedEmailService.getFolderCountsForAccount(stressfreiEmailId);

    res.json({ success: true, data: counts } as ApiResponse);
  } catch (error) {
    console.error('getFolderCounts error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Ordner-Anzahlen',
    } as ApiResponse);
  }
}

// E-Mail-Anzahl pro Ordner für einen Vertrag (optional pro Postfach)
export async function getContractFolderCounts(req: AuthRequest, res: Response): Promise<void> {
  try {
    const contractId = parseInt(req.params.contractId);
    if (!(await canAccessContract(req, res, contractId))) return;
    // Wie getEmailsForContract: Postfach ist required (sonst zeigt der
    // Badge eine andere Zahl als die Liste).
    const stressfreiEmailId = parsePositiveIntQuery(req.query.accountId, 'accountId', res, { required: true });
    if (stressfreiEmailId === null || stressfreiEmailId === undefined) return;

    const counts = await cachedEmailService.getFolderCountsForContract(contractId, stressfreiEmailId);

    res.json({ success: true, data: counts } as ApiResponse);
  } catch (error) {
    console.error('getContractFolderCounts error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Ordner-Anzahlen',
    } as ApiResponse);
  }
}

// ==================== SYNC & SEND ====================

// E-Mails für ein Konto synchronisieren (INBOX + SENT)
export async function syncAccount(req: AuthRequest, res: Response): Promise<void> {
  try {
    const stressfreiEmailId = parseInt(req.params.id);
    // Mitarbeiter brauchen customers:update (wie früher), Portal-Kunden
    // brauchen keine Perm – nur Eigentum am Konto (Owner-Check unten).
    // Trennung der Threat-Modelle: Portal-User dürfen IHR eigenes
    // Postfach syncen, sollen aber nicht Mitarbeiter-Updates triggern.
    const isPortal = !!req.user?.isCustomerPortal;
    const hasUpdatePerm = req.user?.permissions?.includes('customers:update') ?? false;
    if (!isPortal && !hasUpdatePerm) {
      res.status(403).json({ success: false, error: 'Keine Berechtigung' } as ApiResponse);
      return;
    }
    if (!(await canAccessStressfreiEmail(req, res, stressfreiEmailId))) return;
    const fullSync = req.query.full === 'true';

    // Synchronisiert sowohl INBOX als auch SENT
    const result = await cachedEmailService.syncAllFoldersForAccount(stressfreiEmailId, { fullSync });

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({
      success: true,
      data: {
        newEmails: result.newEmails,
        totalEmails: result.totalEmails,
      },
    } as ApiResponse);
  } catch (error) {
    console.error('syncAccount error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Synchronisieren der E-Mails',
    } as ApiResponse);
  }
}

// Security: verhindert Header-Injection via CRLF in E-Mail-Feldern.
// nodemailer prüft das zwar auch selbst, aber besser vor dem Versand
// einen sauberen 400er zurückgeben als einen unklaren SMTP-Fehler.
function hasCRLF(value: unknown): boolean {
  if (typeof value === 'string') return /[\r\n]/.test(value);
  if (Array.isArray(value)) return value.some(hasCRLF);
  return false;
}

// Pentest 97.1 + 97.2 (LOW/INFO, 2026-06-21): Attachment-Validierung
// vor `Buffer.from(...)` damit malformed Content (null, true, leerer
// String, falsches Base64) nicht erst tief im SMTP-Service kracht und
// dort die rohe Node.js-Fehlermeldung in der Response landet. Außerdem
// explizite App-Level-Caps (10 MB pro Datei, 25 MB total, 25 Anhänge),
// damit die Frontend-Doku auch ohne bodyParser-Heroics gilt.
const MAX_ATTACHMENT_COUNT = 25;
const MAX_PER_FILE_BYTES = 10 * 1024 * 1024;
const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
// Standard-Base64-Alphabet inkl. optionalem `=`-Padding (kein URL-safe).
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;

interface AttachmentValidationError {
  status: 400;
  error: string;
}

function validateAttachments(
  attachments: unknown,
): { ok: true } | AttachmentValidationError {
  if (attachments === undefined) return { ok: true };
  if (!Array.isArray(attachments)) {
    return { status: 400, error: 'attachments muss ein Array sein.' };
  }
  if (attachments.length > MAX_ATTACHMENT_COUNT) {
    return {
      status: 400,
      error: `Maximal ${MAX_ATTACHMENT_COUNT} Anhänge pro E-Mail erlaubt (${attachments.length} übermittelt).`,
    };
  }
  let totalBytes = 0;
  for (let i = 0; i < attachments.length; i++) {
    const a = attachments[i];
    const label = `Anhang ${i + 1}`;
    if (!a || typeof a !== 'object') {
      return { status: 400, error: `${label} hat das falsche Format.` };
    }
    const filename = (a as Record<string, unknown>).filename;
    const content = (a as Record<string, unknown>).content;
    const contentType = (a as Record<string, unknown>).contentType;
    if (typeof filename !== 'string' || filename.trim() === '') {
      return { status: 400, error: `${label} hat keinen Dateinamen.` };
    }
    if (typeof content !== 'string' || content.length === 0) {
      return {
        status: 400,
        error: `Anhang "${filename}" hat keinen Inhalt (content muss ein nicht-leerer Base64-String sein).`,
      };
    }
    if (!BASE64_RE.test(content)) {
      return {
        status: 400,
        error: `Anhang "${filename}" ist kein gültiger Base64-String.`,
      };
    }
    if (contentType !== undefined && typeof contentType !== 'string') {
      return {
        status: 400,
        error: `Anhang "${filename}" hat ein ungültiges contentType-Feld.`,
      };
    }
    // Größen-Abschätzung anhand der Base64-Länge: jedes 4-Zeichen-Quartett
    // dekodiert zu max 3 Bytes. So vermeiden wir Buffer.from() schon hier.
    const approxBytes = Math.ceil(content.length * 0.75);
    if (approxBytes > MAX_PER_FILE_BYTES) {
      return {
        status: 400,
        error: `Anhang "${filename}" überschreitet die Maximalgröße von ${MAX_PER_FILE_BYTES / 1024 / 1024} MB.`,
      };
    }
    totalBytes += approxBytes;
    if (totalBytes > MAX_TOTAL_BYTES) {
      return {
        status: 400,
        error: `Gesamtgröße aller Anhänge überschreitet ${MAX_TOTAL_BYTES / 1024 / 1024} MB.`,
      };
    }
  }
  return { ok: true };
}

// E-Mail senden
export async function sendEmailFromAccount(req: AuthRequest, res: Response): Promise<void> {
  try {
    const stressfreiEmailId = parseInt(req.params.id);
    if (!(await canAccessStressfreiEmail(req, res, stressfreiEmailId))) return;
    const { to, cc, subject, text, html, inReplyTo, references, attachments, contractId } = req.body;

    // Header-Injection (CRLF) in Empfänger/Betreff ablehnen
    if (hasCRLF(to) || hasCRLF(cc) || hasCRLF(subject) || hasCRLF(inReplyTo) || hasCRLF(references)) {
      res.status(400).json({
        success: false,
        error: 'Ungültige Zeichen in E-Mail-Feldern (Zeilenumbrüche nicht erlaubt)',
      } as ApiResponse);
      return;
    }

    // Pentest 97.1 + 97.2: Attachment-Validierung vor Buffer.from()
    // (Format, Größe, Anzahl) – sonst leakte der rohe Node.js-Fehler
    // in die Response und Limits waren nur Frontend-Doku.
    const attachmentCheck = validateAttachments(attachments);
    if (!('ok' in attachmentCheck)) {
      res.status(attachmentCheck.status).json({
        success: false,
        error: attachmentCheck.error,
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(stressfreiEmailId);

    if (!stressfreiEmail) {
      res.status(404).json({
        success: false,
        error: 'E-Mail-Konto nicht gefunden',
      } as ApiResponse);
      return;
    }

    if (!stressfreiEmail.hasMailbox) {
      res.status(400).json({
        success: false,
        error: 'Dieses Konto hat keine Mailbox für den Versand',
      } as ApiResponse);
      return;
    }

    // Passwort entschlüsseln
    const password = await stressfreiEmailService.getDecryptedPassword(stressfreiEmailId);

    if (!password) {
      res.status(400).json({
        success: false,
        error: 'Passwort für E-Mail-Versand nicht verfügbar',
      } as ApiResponse);
      return;
    }

    // SMTP-Einstellungen vom Provider
    const settings = await getImapSmtpSettings();

    if (!settings) {
      res.status(400).json({
        success: false,
        error: 'Keine SMTP-Einstellungen konfiguriert',
      } as ApiResponse);
      return;
    }

    // SMTP-Credentials
    const credentials: SmtpCredentials = {
      host: settings.smtpServer,
      port: settings.smtpPort,
      user: stressfreiEmail.email,
      password,
      encryption: settings.smtpEncryption,
      allowSelfSignedCerts: settings.allowSelfSignedCerts,
    };

    // E-Mail-Parameter
    const emailParams: SendEmailParams = {
      to,
      cc,
      subject,
      text,
      html,
      inReplyTo,
      references,
      attachments: attachments as EmailAttachment[] | undefined,
    };

    // E-Mail senden
    const authReq = req as any;
    const result = await sendEmail(credentials, stressfreiEmail.email, emailParams, {
      context: 'customer-email',
      customerId: stressfreiEmail.customerId,
      triggeredBy: authReq.user?.email,
    });

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    // Gesendete E-Mail im IMAP Sent-Ordner speichern (für Attachment-Download)
    let sentUid: number | undefined;
    if (result.rawEmail) {
      try {
        // IMAP-Credentials für Sent-Ordner
        const imapCredentials: ImapCredentials = {
          host: settings.imapServer,
          port: settings.imapPort,
          user: stressfreiEmail.email,
          password,
          encryption: settings.imapEncryption,
          allowSelfSignedCerts: settings.allowSelfSignedCerts,
        };

        const appendResult = await appendToSent(imapCredentials, {
          rawEmail: result.rawEmail,
        });

        if (appendResult.success && appendResult.uid) {
          sentUid = appendResult.uid;
          console.log(`[SMTP] Email stored in Sent folder with UID ${sentUid}`);
        }
      } catch (appendError) {
        // Nicht kritisch - E-Mail wurde trotzdem gesendet
        console.error('Error appending to IMAP Sent folder:', appendError);
      }
    }

    // Gesendete E-Mail im Cache speichern
    try {
      // Anhangsnamen extrahieren falls vorhanden
      const attachmentNames = attachments?.map((a: EmailAttachment) => a.filename) || [];

      await cachedEmailService.createSentEmail(stressfreiEmailId, {
        to,
        cc,
        subject,
        text,
        html,
        messageId: result.messageId || `sent-${Date.now()}@opencrm.local`,
        contractId: contractId ? parseInt(contractId) : undefined,
        attachmentNames: attachmentNames.length > 0 ? attachmentNames : undefined,
        uid: sentUid, // UID vom IMAP Sent-Ordner für Attachment-Download
      });
    } catch (saveError) {
      // Fehler beim Speichern nicht kritisch - E-Mail wurde trotzdem gesendet
      console.error('Error saving sent email to cache:', saveError);
    }

    res.json({
      success: true,
      data: { messageId: result.messageId },
    } as ApiResponse);
  } catch (error) {
    console.error('sendEmailFromAccount error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Senden der E-Mail',
    } as ApiResponse);
  }
}

// ==================== ATTACHMENTS ====================

// Anhang-Liste einer E-Mail abrufen
export async function getAttachments(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.emailId);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;

    // E-Mail aus Cache laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Anhänge aus attachmentNames parsen (JSON Array)
    const attachmentNames: string[] = email.attachmentNames
      ? JSON.parse(email.attachmentNames)
      : [];

    res.json({
      success: true,
      data: attachmentNames.map((name) => ({ filename: name })),
    } as ApiResponse);
  } catch (error) {
    console.error('getAttachments error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Anhänge',
    } as ApiResponse);
  }
}

// Einzelnen Anhang herunterladen
export async function downloadAttachment(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.emailId);
    const filename = decodeURIComponent(req.params.filename);

    // Portal-Isolation: nur eigene/vertretene Emails
    if (!(await canAccessCachedEmail(req, res, emailId))) return;

    // E-Mail aus Cache laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Für gesendete E-Mails: Prüfen ob UID vorhanden (im IMAP Sent gespeichert)
    if (email.folder === 'SENT' && email.uid === 0) {
      res.status(400).json({
        success: false,
        error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden um Zugangsdaten zu bekommen
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
    if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
      res.status(400).json({
        success: false,
        error: 'Keine Mailbox-Zugangsdaten verfügbar',
      } as ApiResponse);
      return;
    }

    // IMAP-Einstellungen laden
    const settings = await getImapSmtpSettings();
    if (!settings) {
      res.status(400).json({
        success: false,
        error: 'Keine E-Mail-Provider-Einstellungen gefunden',
      } as ApiResponse);
      return;
    }

    // Passwort entschlüsseln
    const password = decrypt(stressfreiEmail.emailPasswordEncrypted);

    // IMAP-Credentials
    const credentials: ImapCredentials = {
      host: settings.imapServer,
      port: settings.imapPort,
      user: stressfreiEmail.email,
      password,
      encryption: settings.imapEncryption,
      allowSelfSignedCerts: settings.allowSelfSignedCerts,
    };

    // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent)
    // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs
    // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt
    // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder,
    // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie
    // cachedEmail.service.ts::moveEmailToTrash.
    let imapFolder: string;
    if (email.folder === 'SENT') {
      imapFolder = 'Sent';
    } else if (email.folder === 'SPAM') {
      // Kein stiller Fallback: findet sich der Junk-Ordner nicht (mehr),
      // lieber ein klarer Fehler als aus dem falschen Ordner (INBOX/'Junk')
      // die falsche UID zu ziehen.
      const junkPath = await findJunkFolderPath(credentials);
      if (!junkPath) {
        res.status(404).json({
          success: false,
          error: 'Postfach hat keinen Spam-/Junk-Ordner (mehr) – Anhang nicht abrufbar',
        } as ApiResponse);
        return;
      }
      imapFolder = junkPath;
    } else {
      imapFolder = 'INBOX';
    }

    // Anhang per IMAP abrufen
    const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);

    if (!attachment) {
      res.status(404).json({
        success: false,
        error: 'Anhang nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Security: Content-Type aus IMAP kommt vom Absender und kann `text/html`
    // o.ä. sein. Für inline-Preview verlässt sich der Server nicht auf den
    // gemeldeten Type, sondern prüft die Magic-Bytes des Buffer-Inhalts.
    // Real-world-Problem (intern gemeldet 2026-05-30): manche Mail-Clients
    // setzen für PDF-Anhänge `application/octet-stream` → unser alter
    // Whitelist-Check fiel auf attachment zurück, der Browser öffnete
    // trotz target="_blank" keinen neuen Tab. Mit Magic-Byte-Detection
    // wird der echte Typ erkannt und inline-Preview klappt zuverlässig.
    const buf: Buffer = attachment.content;
    let detectedType: string | null = null;
    if (buf.length >= 5 && buf.subarray(0, 5).toString('latin1') === '%PDF-') {
      detectedType = 'application/pdf';
    } else if (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
      detectedType = 'image/png';
    } else if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
      detectedType = 'image/jpeg';
    } else if (buf.length >= 6 && (buf.subarray(0, 6).toString('latin1') === 'GIF87a' || buf.subarray(0, 6).toString('latin1') === 'GIF89a')) {
      detectedType = 'image/gif';
    } else if (buf.length >= 12 && buf.subarray(0, 4).toString('latin1') === 'RIFF' && buf.subarray(8, 12).toString('latin1') === 'WEBP') {
      detectedType = 'image/webp';
    } else if (buf.length >= 1 && (attachment.contentType || '').toLowerCase().startsWith('text/plain')) {
      // text/plain hat keine eindeutige Magic-Byte – akzeptieren wenn
      // der IMAP-Header das so meldet und Inhalt nur druckbare ASCII/UTF-8 ist.
      // Konservative Prüfung: keine HTML-Tag-Anfänge.
      const sample = buf.subarray(0, Math.min(buf.length, 256)).toString('utf8');
      if (!/<[a-z!\/?]/i.test(sample)) {
        detectedType = 'text/plain; charset=utf-8';
      }
    }
    const isSafeInline = detectedType !== null;
    const requestedDisposition = req.query.view === 'true' ? 'inline' : 'attachment';
    const disposition = requestedDisposition === 'inline' && isSafeInline ? 'inline' : 'attachment';
    // Filename: Steuerzeichen entfernen (CRLF-Injection in Header)
    const safeFilename = (attachment.filename || 'attachment').replace(/[\r\n"\\]/g, '_');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    // Bei sicherem Inline-Typ: erkannten Type setzen (überschreibt
    // eventuell falsches application/octet-stream aus IMAP). Sonst
    // octet-stream erzwingen, damit der Browser nichts erraten kann.
    res.setHeader('Content-Type', isSafeInline ? detectedType! : 'application/octet-stream');
    res.setHeader('Content-Disposition', `${disposition}; filename="${encodeURIComponent(safeFilename)}"`);
    res.setHeader('Content-Length', attachment.size);
    res.send(attachment.content);
  } catch (error) {
    console.error('downloadAttachment error:', error);
    res.status(500).json({
      success: false,
      error: `Fehler beim Herunterladen des Anhangs: ${humanizeImapError(error)}`,
    } as ApiResponse);
  }
}

// ==================== MAILBOX ACCOUNTS ====================

// Mailbox-Konten eines Kunden abrufen
export async function getMailboxAccounts(req: AuthRequest, res: Response): Promise<void> {
  try {
    const customerId = parseInt(req.params.customerId);
    if (!(await canAccessCustomer(req, res, customerId))) return;

    const accounts = await cachedEmailService.getMailboxAccountsForCustomer(customerId);

    res.json({ success: true, data: accounts } as ApiResponse);
  } catch (error) {
    console.error('getMailboxAccounts error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der E-Mail-Konten',
    } as ApiResponse);
  }
}

// Mailbox nachträglich aktivieren
export async function enableMailbox(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessStressfreiEmail(req, res, id))) return;

    const result = await stressfreiEmailService.enableMailbox(id);

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({ success: true } as ApiResponse);
  } catch (error) {
    console.error('enableMailbox error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Aktivieren der Mailbox',
    } as ApiResponse);
  }
}

// Mailbox-Status mit Provider synchronisieren
export async function syncMailboxStatus(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessStressfreiEmail(req, res, id))) return;

    const result = await stressfreiEmailService.syncMailboxStatus(id);

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({
      success: true,
      data: {
        hasMailbox: result.hasMailbox,
        wasUpdated: result.wasUpdated,
      },
    } as ApiResponse);
  } catch (error) {
    console.error('syncMailboxStatus error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Synchronisieren des Mailbox-Status',
    } as ApiResponse);
  }
}

// E-Mail-Thread abrufen
export async function getThread(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;

    const thread = await cachedEmailService.getEmailThread(id);

    res.json({ success: true, data: thread } as ApiResponse);
  } catch (error) {
    console.error('getThread error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden des E-Mail-Threads',
    } as ApiResponse);
  }
}

// Mailbox-Zugangsdaten abrufen (IMAP/SMTP)
export async function getMailboxCredentials(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    // Ownership-Check: ohne diesen Check konnte ein Portal-Kunde mit
    // bekannter Stressfrei-Email-ID die kompletten IMAP/SMTP-Credentials
    // eines anderen Kunden abrufen (IDOR). Pentest-Finding 2026-05-XX.
    if (!(await canAccessStressfreiEmail(req, res, id))) return;

    // StressfreiEmail laden
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(id);

    if (!stressfreiEmail) {
      res.status(404).json({
        success: false,
        error: 'E-Mail-Konto nicht gefunden',
      } as ApiResponse);
      return;
    }

    if (!stressfreiEmail.hasMailbox) {
      res.status(400).json({
        success: false,
        error: 'Keine Mailbox für diese E-Mail-Adresse',
      } as ApiResponse);
      return;
    }

    // Passwort entschlüsseln
    const password = await stressfreiEmailService.getDecryptedPassword(id);

    if (!password) {
      res.status(500).json({
        success: false,
        error: 'Passwort konnte nicht entschlüsselt werden',
      } as ApiResponse);
      return;
    }

    // IMAP/SMTP-Einstellungen laden
    const settings = await getImapSmtpSettings();

    // Klartext-Mailbox-Passwort-Read auditieren (CRITICAL)
    await logChange({
      req,
      action: 'READ',
      resourceType: 'MailboxCredentials',
      resourceId: id.toString(),
      label: `Klartext-Mailbox-Zugangsdaten von ${stressfreiEmail.email} entschlüsselt`,
    });

    res.json({
      success: true,
      data: {
        email: stressfreiEmail.email,
        password,
        imap: settings ? {
          server: settings.imapServer,
          port: settings.imapPort,
          encryption: settings.imapEncryption,
        } : null,
        smtp: settings ? {
          server: settings.smtpServer,
          port: settings.smtpPort,
          encryption: settings.smtpEncryption,
        } : null,
      },
    } as ApiResponse);
  } catch (error) {
    console.error('getMailboxCredentials error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Mailbox-Zugangsdaten',
    } as ApiResponse);
  }
}

// Ungelesene E-Mails zählen
export async function getUnreadCount(req: AuthRequest, res: Response): Promise<void> {
  try {
    const customerId = req.query.customerId ? parseInt(req.query.customerId as string) : undefined;
    const contractId = req.query.contractId ? parseInt(req.query.contractId as string) : undefined;

    let count = 0;

    if (customerId) {
      if (!(await canAccessCustomer(req, res, customerId))) return;
      count = await cachedEmailService.getUnreadCountForCustomer(customerId);
    } else if (contractId) {
      if (!(await canAccessContract(req, res, contractId))) return;
      count = await cachedEmailService.getUnreadCountForContract(contractId);
    }

    res.json({ success: true, data: { count } } as ApiResponse);
  } catch (error) {
    console.error('getUnreadCount error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Zählen der ungelesenen E-Mails',
    } as ApiResponse);
  }
}

// E-Mail in Papierkorb verschieben (nur Admin)
export async function deleteEmail(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;

    // Prüfen ob E-Mail existiert
    const email = await cachedEmailService.getCachedEmailById(id);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    const result = await cachedEmailService.moveEmailToTrash(id);

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({ success: true, message: 'E-Mail in Papierkorb verschoben' } as ApiResponse);
  } catch (error) {
    console.error('deleteEmail error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Löschen der E-Mail',
    } as ApiResponse);
  }
}

// ==================== TRASH OPERATIONS ====================

// Papierkorb-E-Mails für einen Kunden abrufen.
// Optional `accountId` (Postfach-Filter) und `contractId` (Vertrags-Filter)
// – beide aus User-Bug 2026-06-21. Wenn beide leer sind, Verhalten wie
// vorher: alle gelöschten E-Mails des Kunden.
export async function getTrashEmails(req: AuthRequest, res: Response): Promise<void> {
  try {
    const customerId = parseInt(req.params.customerId);
    if (!(await canAccessCustomer(req, res, customerId))) return;
    // Trash auf Kundenebene: Filter sind optional (Cross-Mailbox-Trash-
    // View ist legitim), invalide Werte → 400.
    const stressfreiEmailId = parsePositiveIntQuery(req.query.accountId, 'accountId', res);
    if (stressfreiEmailId === null) return;
    const contractId = parsePositiveIntQuery(req.query.contractId, 'contractId', res);
    if (contractId === null) return;

    const emails = await cachedEmailService.getTrashEmails(customerId, {
      stressfreiEmailId,
      contractId,
    });

    res.json({ success: true, data: emails } as ApiResponse);
  } catch (error) {
    console.error('getTrashEmails error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Papierkorb-E-Mails',
    } as ApiResponse);
  }
}

// Papierkorb-Anzahl für einen Kunden (gleiche Filter wie getTrashEmails)
export async function getTrashCount(req: AuthRequest, res: Response): Promise<void> {
  try {
    const customerId = parseInt(req.params.customerId);
    if (!(await canAccessCustomer(req, res, customerId))) return;
    const stressfreiEmailId = parsePositiveIntQuery(req.query.accountId, 'accountId', res);
    if (stressfreiEmailId === null) return;
    const contractId = parsePositiveIntQuery(req.query.contractId, 'contractId', res);
    if (contractId === null) return;

    const count = await cachedEmailService.getTrashCount(customerId, {
      stressfreiEmailId,
      contractId,
    });

    res.json({ success: true, data: { count } } as ApiResponse);
  } catch (error) {
    console.error('getTrashCount error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Zählen der Papierkorb-E-Mails',
    } as ApiResponse);
  }
}

// E-Mail aus Papierkorb wiederherstellen
export async function restoreEmail(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;

    const result = await cachedEmailService.restoreEmailFromTrash(id);

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({ success: true, message: 'E-Mail wiederhergestellt' } as ApiResponse);
  } catch (error) {
    console.error('restoreEmail error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Wiederherstellen der E-Mail',
    } as ApiResponse);
  }
}

// E-Mail endgültig löschen (aus Papierkorb)
export async function permanentDeleteEmail(req: AuthRequest, res: Response): Promise<void> {
  try {
    const id = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, id))) return;

    const result = await cachedEmailService.permanentDeleteEmail(id);

    if (!result.success) {
      res.status(400).json({
        success: false,
        error: result.error,
      } as ApiResponse);
      return;
    }

    res.json({ success: true, message: 'E-Mail endgültig gelöscht' } as ApiResponse);
  } catch (error) {
    console.error('permanentDeleteEmail error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim endgültigen Löschen der E-Mail',
    } as ApiResponse);
  }
}

// ==================== ATTACHMENT TARGETS ====================

// Verfügbare Dokumenten-Ziele für E-Mail-Anhänge abrufen
export async function getAttachmentTargets(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;

    // E-Mail mit StressfreiEmail laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden um an den Kunden zu kommen
    const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
      where: { id: email.stressfreiEmailId },
      include: {
        customer: {
          include: {
            identityDocuments: {
              where: { isActive: true },
              select: { id: true, type: true, documentNumber: true, documentPath: true },
            },
            bankCards: {
              where: { isActive: true },
              select: { id: true, iban: true, bankName: true, documentPath: true },
            },
          },
        },
      },
    });

    if (!stressfreiEmail) {
      res.status(404).json({
        success: false,
        error: 'E-Mail-Konto nicht gefunden',
      } as ApiResponse);
      return;
    }

    const customer = stressfreiEmail.customer;
    const customerType = customer.type as 'PRIVATE' | 'BUSINESS';

    // Ergebnis-Struktur
    interface TargetSlot {
      key: string;
      label: string;
      field: string;
      hasDocument: boolean;
      currentPath?: string;
    }

    interface EntityWithSlots {
      id: number;
      label: string;
      slots: TargetSlot[];
    }

    interface AttachmentTargetsResponse {
      customer: {
        id: number;
        name: string;
        type: string;
        slots: TargetSlot[];
      };
      identityDocuments: EntityWithSlots[];
      bankCards: EntityWithSlots[];
      contract?: {
        id: number;
        contractNumber: string;
        type: string;
        energyDetailsId?: number;
        slots: TargetSlot[];
      };
    }

    // Kunden-Dokumentziele (gefiltert nach Kundentyp)
    const customerTargets = getCustomerTargets(customerType);
    const customerSlots: TargetSlot[] = customerTargets.map(target => ({
      key: target.key,
      label: target.label,
      field: target.field,
      hasDocument: !!(customer as any)[target.field],
      currentPath: (customer as any)[target.field] || undefined,
    }));

    // Ausweis-Dokumentziele
    const identityTargets = getIdentityDocumentTargets();
    const identityDocuments: EntityWithSlots[] = customer.identityDocuments.map((doc: { id: number; type: DocumentType; documentNumber: string; documentPath: string | null }) => {
      const docTypeLabels: Record<string, string> = {
        ID_CARD: 'Personalausweis',
        PASSPORT: 'Reisepass',
        DRIVERS_LICENSE: 'Führerschein',
        OTHER: 'Sonstiges',
      };
      return {
        id: doc.id,
        label: `${docTypeLabels[doc.type] || doc.type}: ${doc.documentNumber}`,
        slots: identityTargets.map(target => ({
          key: target.key,
          label: target.label,
          field: target.field,
          hasDocument: !!(doc as Record<string, unknown>)[target.field],
          currentPath: ((doc as Record<string, unknown>)[target.field] as string | undefined) || undefined,
        })),
      };
    });

    // Bankkarten-Dokumentziele
    const bankCardTargets = getBankCardTargets();
    const bankCards: EntityWithSlots[] = customer.bankCards.map((card: { id: number; iban: string; bankName: string | null; documentPath: string | null }) => ({
      id: card.id,
      label: `${card.bankName || 'Bank'}: ${card.iban.slice(-4)}`,
      slots: bankCardTargets.map(target => ({
        key: target.key,
        label: target.label,
        field: target.field,
        hasDocument: !!(card as Record<string, unknown>)[target.field],
        currentPath: ((card as Record<string, unknown>)[target.field] as string | undefined) || undefined,
      })),
    }));

    // Basis-Antwort
    const response: AttachmentTargetsResponse = {
      customer: {
        id: customer.id,
        name: customer.companyName || `${customer.firstName} ${customer.lastName}`,
        type: customerType,
        slots: customerSlots,
      },
      identityDocuments,
      bankCards,
    };

    // Vertrag hinzufügen, falls E-Mail einem Vertrag zugeordnet ist
    if (email.contractId) {
      const contract = await prisma.contract.findUnique({
        where: { id: email.contractId },
        select: {
          id: true,
          contractNumber: true,
          type: true,
          cancellationLetterPath: true,
          cancellationConfirmationPath: true,
          cancellationLetterOptionsPath: true,
          cancellationConfirmationOptionsPath: true,
          energyDetails: {
            select: { id: true },
          },
        },
      });

      if (contract) {
        const contractTargets = getContractTargets();
        response.contract = {
          id: contract.id,
          contractNumber: contract.contractNumber,
          type: contract.type,
          energyDetailsId: contract.energyDetails?.id,
          slots: contractTargets.map(target => ({
            key: target.key,
            label: target.label,
            field: target.field,
            hasDocument: !!(contract as any)[target.field],
            currentPath: (contract as any)[target.field] || undefined,
          })),
        };
      }
    }

    res.json({ success: true, data: response } as ApiResponse);
  } catch (error) {
    console.error('getAttachmentTargets error:', error);
    res.status(500).json({
      success: false,
      error: 'Fehler beim Laden der Dokumenten-Ziele',
    } as ApiResponse);
  }
}

// E-Mail-Anhang in ein Dokumentenfeld speichern
export async function saveAttachmentTo(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const filename = decodeURIComponent(req.params.filename);
    const { entityType, entityId, targetKey } = req.body;

    console.log('[saveAttachmentTo] Request:', { emailId, filename, entityType, entityId, targetKey });

    // Validierung
    if (!entityType || !targetKey) {
      res.status(400).json({
        success: false,
        error: 'entityType und targetKey sind erforderlich',
      } as ApiResponse);
      return;
    }

    // E-Mail aus Cache laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Für gesendete E-Mails: Prüfen ob UID vorhanden (im IMAP Sent gespeichert)
    if (email.folder === 'SENT' && email.uid === 0) {
      res.status(400).json({
        success: false,
        error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden um Zugangsdaten zu bekommen
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
    if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
      res.status(400).json({
        success: false,
        error: 'Keine Mailbox-Zugangsdaten verfügbar',
      } as ApiResponse);
      return;
    }

    // IMAP-Einstellungen laden
    const settings = await getImapSmtpSettings();
    if (!settings) {
      res.status(400).json({
        success: false,
        error: 'Keine E-Mail-Provider-Einstellungen gefunden',
      } as ApiResponse);
      return;
    }

    // Passwort entschlüsseln
    const password = decrypt(stressfreiEmail.emailPasswordEncrypted);

    // IMAP-Credentials
    const credentials: ImapCredentials = {
      host: settings.imapServer,
      port: settings.imapPort,
      user: stressfreiEmail.email,
      password,
      encryption: settings.imapEncryption,
      allowSelfSignedCerts: settings.allowSelfSignedCerts,
    };

    // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent)
    // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs
    // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt
    // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder,
    // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie
    // cachedEmail.service.ts::moveEmailToTrash.
    let imapFolder: string;
    if (email.folder === 'SENT') {
      imapFolder = 'Sent';
    } else if (email.folder === 'SPAM') {
      // Kein stiller Fallback: findet sich der Junk-Ordner nicht (mehr),
      // lieber ein klarer Fehler als aus dem falschen Ordner (INBOX/'Junk')
      // die falsche UID zu ziehen.
      const junkPath = await findJunkFolderPath(credentials);
      if (!junkPath) {
        res.status(404).json({
          success: false,
          error: 'Postfach hat keinen Spam-/Junk-Ordner (mehr) – Anhang nicht abrufbar',
        } as ApiResponse);
        return;
      }
      imapFolder = junkPath;
    } else {
      imapFolder = 'INBOX';
    }

    // Anhang per IMAP abrufen
    const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);

    if (!attachment) {
      res.status(404).json({
        success: false,
        error: 'Anhang nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Ziel-Konfiguration finden
    let targetConfig;
    let targetDir: string;
    let targetField: string;

    console.log('[saveAttachmentTo] Looking for target config:', { entityType, targetKey });

    if (entityType === 'customer') {
      targetConfig = documentTargets.customer.find(t => t.key === targetKey);
    } else if (entityType === 'identityDocument') {
      targetConfig = documentTargets.identityDocument.find(t => t.key === targetKey);
    } else if (entityType === 'bankCard') {
      targetConfig = documentTargets.bankCard.find(t => t.key === targetKey);
    } else if (entityType === 'contract') {
      targetConfig = documentTargets.contract.find(t => t.key === targetKey);
    }

    console.log('[saveAttachmentTo] Found targetConfig:', targetConfig);

    if (!targetConfig) {
      res.status(400).json({
        success: false,
        error: `Unbekanntes Dokumentziel: ${entityType}/${targetKey}`,
      } as ApiResponse);
      return;
    }

    targetDir = targetConfig.directory;
    targetField = targetConfig.field;

    // Uploads-Verzeichnis erstellen
    const uploadsDir = path.join(process.cwd(), 'uploads', targetDir);
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir, { recursive: true });
    }

    // Eindeutigen Dateinamen generieren
    const ext = path.extname(filename) || '.pdf';
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const newFilename = `${uniqueSuffix}${ext}`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/${targetDir}/${newFilename}`;

    // Pentest 68.1: PDF-Anhänge auf aktive Inhalte scannen (JS/Launch/Embed).
    assertSafePdf(attachment.content);

    // Datei speichern
    fs.writeFileSync(filePath, attachment.content);

    // Alte Datei löschen und DB aktualisieren
    if (entityType === 'customer') {
      // Customer ID aus StressfreiEmail laden
      console.log('[saveAttachmentTo] Looking up customer for stressfreiEmail.id:', stressfreiEmail.id);
      const customer = await prisma.customer.findFirst({
        where: {
          stressfreiEmails: { some: { id: stressfreiEmail.id } },
        },
      });
      console.log('[saveAttachmentTo] Found customer:', customer?.id);

      if (!customer) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Kunde nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (customer as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.customer.update({
        where: { id: customer.id },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'identityDocument') {
      if (!entityId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'entityId ist für identityDocument erforderlich',
        } as ApiResponse);
        return;
      }

      const doc = await prisma.identityDocument.findUnique({ where: { id: entityId } });
      if (!doc) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Ausweis nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (doc as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.identityDocument.update({
        where: { id: entityId },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'bankCard') {
      if (!entityId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'entityId ist für bankCard erforderlich',
        } as ApiResponse);
        return;
      }

      const card = await prisma.bankCard.findUnique({ where: { id: entityId } });
      if (!card) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Bankkarte nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (card as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.bankCard.update({
        where: { id: entityId },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'contract') {
      // Contract-ID kommt aus der E-Mail-Zuordnung
      if (!email.contractId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'E-Mail ist keinem Vertrag zugeordnet',
        } as ApiResponse);
        return;
      }

      const contract = await prisma.contract.findUnique({ where: { id: email.contractId } });
      if (!contract) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Vertrag nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (contract as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.contract.update({
        where: { id: email.contractId },
        data: { [targetField]: relativePath },
      });
    }

    res.json({
      success: true,
      data: {
        path: relativePath,
        filename: newFilename,
        originalName: filename,
        size: attachment.size,
      },
    } as ApiResponse);
  } catch (error) {
    console.error('saveAttachmentTo error:', error);
    // Detailliertere Fehlermeldung für Debugging
    const status = error instanceof ApiError ? error.statusCode : 500;
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(status).json({
      success: false,
      error: `Fehler beim Speichern des Anhangs: ${errorMessage}`,
    } as ApiResponse);
  }
}

// ==================== SAVE EMAIL AS PDF ====================

// E-Mail als PDF exportieren und in Dokumentenfeld speichern
export async function saveEmailAsPdf(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const { entityType, entityId, targetKey } = req.body;

    console.log('[saveEmailAsPdf] Request:', { emailId, entityType, entityId, targetKey });

    // Validierung
    if (!entityType || !targetKey) {
      res.status(400).json({
        success: false,
        error: 'entityType und targetKey sind erforderlich',
      } as ApiResponse);
      return;
    }

    // E-Mail aus Cache laden (mit Body)
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden um an den Kunden zu kommen
    const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
      where: { id: email.stressfreiEmailId },
      include: { customer: true },
    });

    if (!stressfreiEmail) {
      res.status(404).json({
        success: false,
        error: 'E-Mail-Konto nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Empfänger-Adressen parsen (JSON Array)
    let toAddresses: string[] = [];
    let ccAddresses: string[] = [];
    try {
      toAddresses = JSON.parse(email.toAddresses);
    } catch { toAddresses = [email.toAddresses]; }
    try {
      if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses);
    } catch { /* ignore */ }

    // PDF generieren
    const pdfBuffer = await generateEmailPdf({
      from: email.fromAddress,
      to: toAddresses.join(', '),
      cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
      subject: email.subject || '(Kein Betreff)',
      date: email.receivedAt,
      bodyText: email.textBody || undefined,
      bodyHtml: email.htmlBody || undefined,
    });

    // Ziel-Konfiguration finden
    let targetConfig;
    let targetDir: string;
    let targetField: string;

    console.log('[saveEmailAsPdf] Looking for target config:', { entityType, targetKey });

    if (entityType === 'customer') {
      targetConfig = documentTargets.customer.find(t => t.key === targetKey);
    } else if (entityType === 'identityDocument') {
      targetConfig = documentTargets.identityDocument.find(t => t.key === targetKey);
    } else if (entityType === 'bankCard') {
      targetConfig = documentTargets.bankCard.find(t => t.key === targetKey);
    } else if (entityType === 'contract') {
      targetConfig = documentTargets.contract.find(t => t.key === targetKey);
    }

    console.log('[saveEmailAsPdf] Found targetConfig:', targetConfig);

    if (!targetConfig) {
      res.status(400).json({
        success: false,
        error: `Unbekanntes Dokumentziel: ${entityType}/${targetKey}`,
      } as ApiResponse);
      return;
    }

    targetDir = targetConfig.directory;
    targetField = targetConfig.field;

    // Uploads-Verzeichnis erstellen
    const uploadsDir = path.join(process.cwd(), 'uploads', targetDir);
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir, { recursive: true });
    }

    // Eindeutigen Dateinamen generieren
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const newFilename = `email-${uniqueSuffix}.pdf`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/${targetDir}/${newFilename}`;

    // PDF speichern
    fs.writeFileSync(filePath, pdfBuffer);

    // Alte Datei löschen und DB aktualisieren
    if (entityType === 'customer') {
      const customer = stressfreiEmail.customer;

      // Alte Datei löschen
      const oldPath = (customer as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.customer.update({
        where: { id: customer.id },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'identityDocument') {
      if (!entityId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'entityId ist für identityDocument erforderlich',
        } as ApiResponse);
        return;
      }

      const doc = await prisma.identityDocument.findUnique({ where: { id: entityId } });
      if (!doc) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Ausweis nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (doc as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.identityDocument.update({
        where: { id: entityId },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'bankCard') {
      if (!entityId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'entityId ist für bankCard erforderlich',
        } as ApiResponse);
        return;
      }

      const card = await prisma.bankCard.findUnique({ where: { id: entityId } });
      if (!card) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Bankkarte nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (card as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.bankCard.update({
        where: { id: entityId },
        data: { [targetField]: relativePath },
      });
    } else if (entityType === 'contract') {
      // Contract-ID kommt aus der E-Mail-Zuordnung oder direkt
      const contractId = email.contractId;
      if (!contractId) {
        fs.unlinkSync(filePath);
        res.status(400).json({
          success: false,
          error: 'E-Mail ist keinem Vertrag zugeordnet',
        } as ApiResponse);
        return;
      }

      const contract = await prisma.contract.findUnique({ where: { id: contractId } });
      if (!contract) {
        fs.unlinkSync(filePath);
        res.status(404).json({
          success: false,
          error: 'Vertrag nicht gefunden',
        } as ApiResponse);
        return;
      }

      // Alte Datei löschen
      const oldPath = (contract as any)[targetField];
      if (oldPath) {
        const oldFullPath = path.join(process.cwd(), oldPath);
        if (fs.existsSync(oldFullPath)) {
          fs.unlinkSync(oldFullPath);
        }
      }

      await prisma.contract.update({
        where: { id: contractId },
        data: { [targetField]: relativePath },
      });
    }

    res.json({
      success: true,
      data: {
        path: relativePath,
        filename: newFilename,
        size: pdfBuffer.length,
      },
    } as ApiResponse);
  } catch (error) {
    console.error('saveEmailAsPdf error:', error);
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(500).json({
      success: false,
      error: `Fehler beim Erstellen der PDF: ${errorMessage}`,
    } as ApiResponse);
  }
}

// ==================== SAVE EMAIL AS INVOICE ====================

// E-Mail als PDF exportieren und als Rechnung speichern
export async function saveEmailAsInvoice(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const { invoiceDate, invoiceType, notes } = req.body;

    console.log('[saveEmailAsInvoice] Request:', { emailId, invoiceDate, invoiceType, notes });

    // Validierung
    if (!invoiceDate || !invoiceType) {
      res.status(400).json({
        success: false,
        error: 'invoiceDate und invoiceType sind erforderlich',
      } as ApiResponse);
      return;
    }

    // Validiere invoiceType
    if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
      res.status(400).json({
        success: false,
        error: 'Ungültiger Rechnungstyp',
      } as ApiResponse);
      return;
    }

    // E-Mail aus Cache laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Prüfen ob E-Mail einem Vertrag zugeordnet ist
    if (!email.contractId) {
      res.status(400).json({
        success: false,
        error: 'E-Mail ist keinem Vertrag zugeordnet',
      } as ApiResponse);
      return;
    }

    // Vertrag laden
    const contract = await prisma.contract.findUnique({
      where: { id: email.contractId },
      include: { energyDetails: true },
    });

    if (!contract) {
      res.status(404).json({
        success: false,
        error: 'Vertrag nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Empfänger-Adressen parsen (JSON Array)
    let toAddresses: string[] = [];
    let ccAddresses: string[] = [];
    try {
      toAddresses = JSON.parse(email.toAddresses);
    } catch { toAddresses = [email.toAddresses]; }
    try {
      if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses);
    } catch { /* ignore */ }

    // PDF generieren
    const pdfBuffer = await generateEmailPdf({
      from: email.fromAddress,
      to: toAddresses.join(', '),
      cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
      subject: email.subject || '(Kein Betreff)',
      date: email.receivedAt,
      bodyText: email.textBody || undefined,
      bodyHtml: email.htmlBody || undefined,
    });

    // Uploads-Verzeichnis erstellen
    const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir, { recursive: true });
    }

    // Eindeutigen Dateinamen generieren
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const newFilename = `invoice-email-${uniqueSuffix}.pdf`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/invoices/${newFilename}`;

    // PDF speichern
    fs.writeFileSync(filePath, pdfBuffer);

    // Invoice in DB erstellen (für alle Vertragstypen)
    const invoice = contract.energyDetails
      ? await invoiceService.addInvoice(contract.energyDetails.id, {
          invoiceDate: new Date(invoiceDate),
          invoiceType,
          documentPath: relativePath,
          notes: notes || undefined,
        })
      : await invoiceService.addInvoiceByContract(contract.id, {
          invoiceDate: new Date(invoiceDate),
          invoiceType,
          documentPath: relativePath,
          notes: notes || undefined,
        });

    res.json({
      success: true,
      data: invoice,
    } as ApiResponse);
  } catch (error) {
    console.error('saveEmailAsInvoice error:', error);
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(500).json({
      success: false,
      error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
    } as ApiResponse);
  }
}

// ==================== SAVE EMAIL AS CONTRACT DOCUMENT ====================

// E-Mail als PDF exportieren und als Vertragsdokument hinterlegen.
// Parallel zu saveAttachmentAsContractDocument (Anhang-Variante) – damit
// auch reine Mail-Bestätigungen ohne Anhang als Auftragsformular/
// Lieferbestätigung etc. an einem Vertrag landen können.
export async function saveEmailAsContractDocument(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const { documentType, notes } = req.body;

    // Pentest 58.1: Whitelist-Validierung des documentType.
    let validatedType: string;
    try {
      validatedType = validateContractDocumentType(documentType);
    } catch (err) {
      res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiger documentType' } as ApiResponse);
      return;
    }

    // Pentest 62.7: deliveryDate früh validieren, bevor wir Dateien schreiben.
    let deliveryDate: string | null;
    try {
      deliveryDate = validateOptionalIsoDate(req.body?.deliveryDate, 'deliveryDate');
    } catch (err) {
      res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Lieferdatum' } as ApiResponse);
      return;
    }

    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse);
      return;
    }
    if (!email.contractId) {
      res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet' } as ApiResponse);
      return;
    }

    const contract = await prisma.contract.findUnique({
      where: { id: email.contractId },
      select: { id: true, contractNumber: true, customerId: true },
    });
    if (!contract) {
      res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' } as ApiResponse);
      return;
    }
    if (!(await canAccessContract(req as AuthRequest, res, contract.id))) return;

    // Empfänger-Adressen parsen
    let toAddresses: string[] = [];
    let ccAddresses: string[] = [];
    try { toAddresses = JSON.parse(email.toAddresses); } catch { toAddresses = [email.toAddresses]; }
    try { if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses); } catch { /* ignore */ }

    const pdfBuffer = await generateEmailPdf({
      from: email.fromAddress,
      to: toAddresses.join(', '),
      cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
      subject: email.subject || '(Kein Betreff)',
      date: email.receivedAt,
      bodyText: email.textBody || undefined,
      bodyHtml: email.htmlBody || undefined,
    });

    const uploadsDir = path.join(process.cwd(), 'uploads', 'contract-documents');
    if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });

    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const safeType = validatedType.toLowerCase().replace(/[^a-z0-9]/g, '-');
    const newFilename = `${safeType}-email-${uniqueSuffix}.pdf`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/contract-documents/${newFilename}`;

    // Pentest 55.4: Lock vor Schreiben, damit parallele Requests blockieren.
    // PDF erst INNERHALB des Locks auf Disk schreiben → kein verwaister
    // Datei-Müll bei Race-Reject.
    const doc = await withContractDocumentLock(contract.id, validatedType, async () => {
      fs.writeFileSync(filePath, pdfBuffer);
      return prisma.contractDocument.create({
        data: {
          contractId: contract.id,
          documentType: validatedType,
          documentPath: relativePath,
          originalName: `${email.subject || 'email'}.pdf`,
          notes: sanitizeNotes(notes),
          uploadedBy: (req as any).user?.email || 'email-import',
        },
      });
    });

    // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer.
    // deliveryDate wurde oben schon validiert (Pentest 62.7).
    await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate);

    res.json({ success: true, data: doc } as ApiResponse);
  } catch (error) {
    console.error('saveEmailAsContractDocument error:', error);
    // Pentest 64.1: ApiError mit eigenem statusCode (z.B. 400 vom Race-
    // Lock) statt pauschal 500.
    const status = error instanceof ApiError ? error.statusCode : 500;
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(status).json({ success: false, error: `Fehler beim Speichern: ${errorMessage}` } as ApiResponse);
  }
}

// ==================== SAVE ATTACHMENT AS INVOICE ====================

// E-Mail-Anhang als Rechnung speichern
export async function saveAttachmentAsInvoice(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const filename = decodeURIComponent(req.params.filename);
    const { invoiceDate, invoiceType, notes } = req.body;

    console.log('[saveAttachmentAsInvoice] Request:', { emailId, filename, invoiceDate, invoiceType, notes });

    // Validierung
    if (!invoiceDate || !invoiceType) {
      res.status(400).json({
        success: false,
        error: 'invoiceDate und invoiceType sind erforderlich',
      } as ApiResponse);
      return;
    }

    // Validiere invoiceType
    if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
      res.status(400).json({
        success: false,
        error: 'Ungültiger Rechnungstyp',
      } as ApiResponse);
      return;
    }

    // E-Mail aus Cache laden
    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({
        success: false,
        error: 'E-Mail nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Prüfen ob E-Mail einem Vertrag zugeordnet ist
    if (!email.contractId) {
      res.status(400).json({
        success: false,
        error: 'E-Mail ist keinem Vertrag zugeordnet',
      } as ApiResponse);
      return;
    }

    // Vertrag laden
    const contract = await prisma.contract.findUnique({
      where: { id: email.contractId },
      include: { energyDetails: true },
    });

    if (!contract) {
      res.status(404).json({
        success: false,
        error: 'Vertrag nicht gefunden',
      } as ApiResponse);
      return;
    }

    // Für gesendete E-Mails: Prüfen ob UID vorhanden
    if (email.folder === 'SENT' && email.uid === 0) {
      res.status(400).json({
        success: false,
        error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail laden um Zugangsdaten zu bekommen
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
    if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
      res.status(400).json({
        success: false,
        error: 'Keine Mailbox-Zugangsdaten verfügbar',
      } as ApiResponse);
      return;
    }

    // IMAP-Einstellungen laden
    const settings = await getImapSmtpSettings();
    if (!settings) {
      res.status(400).json({
        success: false,
        error: 'Keine E-Mail-Provider-Einstellungen gefunden',
      } as ApiResponse);
      return;
    }

    // Passwort entschlüsseln
    const password = decrypt(stressfreiEmail.emailPasswordEncrypted);

    // IMAP-Credentials zusammenstellen
    const credentials: ImapCredentials = {
      host: settings.imapServer,
      port: settings.imapPort,
      user: stressfreiEmail.email,
      password,
      encryption: settings.imapEncryption,
      allowSelfSignedCerts: settings.allowSelfSignedCerts,
    };

    // IMAP-Ordner bestimmen
    // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs
    // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt
    // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder,
    // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie
    // cachedEmail.service.ts::moveEmailToTrash.
    let imapFolder: string;
    if (email.folder === 'SENT') {
      imapFolder = 'Sent';
    } else if (email.folder === 'SPAM') {
      // Kein stiller Fallback: findet sich der Junk-Ordner nicht (mehr),
      // lieber ein klarer Fehler als aus dem falschen Ordner (INBOX/'Junk')
      // die falsche UID zu ziehen.
      const junkPath = await findJunkFolderPath(credentials);
      if (!junkPath) {
        res.status(404).json({
          success: false,
          error: 'Postfach hat keinen Spam-/Junk-Ordner (mehr) – Anhang nicht abrufbar',
        } as ApiResponse);
        return;
      }
      imapFolder = junkPath;
    } else {
      imapFolder = 'INBOX';
    }

    // Anhang vom IMAP-Server laden
    const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
    if (!attachment) {
      res.status(404).json({
        success: false,
        error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
      } as ApiResponse);
      return;
    }

    // Uploads-Verzeichnis erstellen
    const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir, { recursive: true });
    }

    // Dateiendung extrahieren
    const ext = path.extname(filename) || '.pdf';
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const newFilename = `invoice-attachment-${uniqueSuffix}${ext}`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/invoices/${newFilename}`;

    // Pentest 68.1: PDF-Anhänge auf aktive Inhalte scannen.
    assertSafePdf(attachment.content);

    // Datei speichern
    fs.writeFileSync(filePath, attachment.content);

    // Invoice in DB erstellen (für alle Vertragstypen)
    const invoice = contract.energyDetails
      ? await invoiceService.addInvoice(contract.energyDetails.id, {
          invoiceDate: new Date(invoiceDate),
          invoiceType,
          documentPath: relativePath,
          notes: notes || undefined,
        })
      : await invoiceService.addInvoiceByContract(contract.id, {
          invoiceDate: new Date(invoiceDate),
          invoiceType,
          documentPath: relativePath,
          notes: notes || undefined,
        });

    res.json({
      success: true,
      data: invoice,
    } as ApiResponse);
  } catch (error) {
    console.error('saveAttachmentAsInvoice error:', error);
    const status = error instanceof ApiError ? error.statusCode : 500;
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(status).json({
      success: false,
      error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
    } as ApiResponse);
  }
}

/**
 * Anhang einer E-Mail als Vertragsdokument (ContractDocument) speichern.
 * Nutzt die flexible ContractDocument-Tabelle mit documentType (Auftragsformular, Lieferbestätigung, etc.)
 */
export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Response): Promise<void> {
  try {
    const emailId = parseInt(req.params.id);
    if (!(await canAccessCachedEmail(req, res, emailId))) return;
    const filename = decodeURIComponent(req.params.filename);
    const { documentType, notes } = req.body;

    // Pentest 58.1: Whitelist-Validierung des documentType.
    let validatedType: string;
    try {
      validatedType = validateContractDocumentType(documentType);
    } catch (err) {
      res.status(400).json({
        success: false,
        error: err instanceof Error ? err.message : 'Ungültiger documentType',
      } as ApiResponse);
      return;
    }

    // Pentest 62.7: deliveryDate früh validieren, bevor wir Dateien schreiben.
    let deliveryDate: string | null;
    try {
      deliveryDate = validateOptionalIsoDate(req.body?.deliveryDate, 'deliveryDate');
    } catch (err) {
      res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Lieferdatum' } as ApiResponse);
      return;
    }

    const email = await cachedEmailService.getCachedEmailById(emailId);
    if (!email) {
      res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse);
      return;
    }

    if (!email.contractId) {
      res.status(400).json({
        success: false,
        error: 'E-Mail ist keinem Vertrag zugeordnet',
      } as ApiResponse);
      return;
    }

    const contract = await prisma.contract.findUnique({
      where: { id: email.contractId },
      select: { id: true, contractNumber: true, customerId: true },
    });

    if (!contract) {
      res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' } as ApiResponse);
      return;
    }

    // Ownership-Check (Portal-Kunde darf nur auf eigenen/vertretenen Vertrag)
    if (!(await canAccessContract(req as AuthRequest, res, contract.id))) return;

    // Für gesendete E-Mails: Prüfen ob UID vorhanden
    if (email.folder === 'SENT' && email.uid === 0) {
      res.status(400).json({
        success: false,
        error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
      } as ApiResponse);
      return;
    }

    // StressfreiEmail für IMAP-Zugangsdaten
    const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
    if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
      res.status(400).json({
        success: false,
        error: 'Keine Mailbox-Zugangsdaten verfügbar',
      } as ApiResponse);
      return;
    }

    const settings = await getImapSmtpSettings();
    if (!settings) {
      res.status(400).json({
        success: false,
        error: 'Keine E-Mail-Provider-Einstellungen gefunden',
      } as ApiResponse);
      return;
    }

    const password = decrypt(stressfreiEmail.emailPasswordEncrypted);

    const credentials: ImapCredentials = {
      host: settings.imapServer,
      port: settings.imapPort,
      user: stressfreiEmail.email,
      password,
      encryption: settings.imapEncryption,
      allowSelfSignedCerts: settings.allowSelfSignedCerts,
    };

    // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs
    // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt
    // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder,
    // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie
    // cachedEmail.service.ts::moveEmailToTrash.
    let imapFolder: string;
    if (email.folder === 'SENT') {
      imapFolder = 'Sent';
    } else if (email.folder === 'SPAM') {
      // Kein stiller Fallback: findet sich der Junk-Ordner nicht (mehr),
      // lieber ein klarer Fehler als aus dem falschen Ordner (INBOX/'Junk')
      // die falsche UID zu ziehen.
      const junkPath = await findJunkFolderPath(credentials);
      if (!junkPath) {
        res.status(404).json({
          success: false,
          error: 'Postfach hat keinen Spam-/Junk-Ordner (mehr) – Anhang nicht abrufbar',
        } as ApiResponse);
        return;
      }
      imapFolder = junkPath;
    } else {
      imapFolder = 'INBOX';
    }

    const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
    if (!attachment) {
      res.status(404).json({
        success: false,
        error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
      } as ApiResponse);
      return;
    }

    // Uploads-Verzeichnis
    const uploadsDir = path.join(process.cwd(), 'uploads', 'contract-documents');
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir, { recursive: true });
    }

    const ext = path.extname(filename) || '.pdf';
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const safeType = validatedType.toLowerCase().replace(/[^a-z0-9]/g, '-');
    const newFilename = `${safeType}-${uniqueSuffix}${ext}`;
    const filePath = path.join(uploadsDir, newFilename);
    const relativePath = `/uploads/contract-documents/${newFilename}`;

    // Pentest 68.1: PDF-Anhänge auf aktive Inhalte scannen.
    assertSafePdf(attachment.content);

    // Pentest 55.4: Lock vor Schreiben (siehe saveEmailAsContractDocument).
    const doc = await withContractDocumentLock(contract.id, validatedType, async () => {
      fs.writeFileSync(filePath, attachment.content);
      return prisma.contractDocument.create({
        data: {
          contractId: contract.id,
          documentType: validatedType,
          documentPath: relativePath,
          originalName: filename,
          notes: sanitizeNotes(notes),
          uploadedBy: (req as any).user?.email || 'email-import',
        },
      });
    });

    // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer.
    // deliveryDate wurde oben schon validiert (Pentest 62.7).
    await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate);

    res.json({ success: true, data: doc } as ApiResponse);
  } catch (error) {
    console.error('saveAttachmentAsContractDocument error:', error);
    // Pentest 64.1: ApiError mit eigenem statusCode statt pauschal 500.
    const status = error instanceof ApiError ? error.statusCode : 500;
    const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
    res.status(status).json({
      success: false,
      error: `Fehler beim Speichern: ${errorMessage}`,
    } as ApiResponse);
  }
}
