"""
Celery-Tasks fuer die automatische Verarbeitung eingehender E-Mails.

Workflow:
  1. `poll_emails` laeuft alle 15 Minuten (Celery Beat)
  2. Er liest ungelesene E-Mails aus dem IMAP-Postfach
  3. Fuer jede E-Mail:
     a) Absender wird mit Destinataer-Datenbank abgeglichen (E-Mail-Feld)
     b) Betreff/Body wird auf Rechnungs-Keywords geprueft
     c) Ein EmailEingang-Datensatz wird angelegt (mit Kategorie)
     d) Alle Anhaenge werden als DokumentDatei im Django-DMS gespeichert
  4. Nicht erkannte Absender werden als "unbekannt" markiert (manuelle Nachbearbeitung)

Konfiguration (Umgebungsvariablen in .env / compose.yml):
  IMAP_HOST       — IMAP-Server-Adresse (z. B. mail.vhtv-stiftung.de)
  IMAP_PORT       — Port (Standard: 993 fuer SSL)
  IMAP_USER       — Benutzername
  IMAP_PASSWORD   — Passwort
  IMAP_FOLDER     — Ordner (Standard: INBOX)
"""

import email
import email.utils
import imaplib
import logging
import mimetypes
import re
from datetime import datetime, timezone as dt_timezone
from email.header import decode_header, make_header

from celery import shared_task
from django.utils import timezone

logger = logging.getLogger(__name__)


# Patterns fuer Rechnungserkennung im Betreff/Body
RECHNUNG_PATTERNS = [
    re.compile(r"\brechnung\b", re.IGNORECASE),
    re.compile(r"\binvoice\b", re.IGNORECASE),
    re.compile(r"\brechnungs[-\s]?nr\.?\s*[:\s]?\s*\d+", re.IGNORECASE),
    re.compile(r"\bRE[-/]\d{4,}", re.IGNORECASE),  # RE-2024001, RE/20240315
]

GESCHICHTE_PATTERNS = [
    re.compile(r"\bstiftungsgeschichte\b", re.IGNORECASE),
    re.compile(r"\bahnenforschung\b", re.IGNORECASE),
    re.compile(r"\bgenealogie\b", re.IGNORECASE),
    re.compile(r"\bstammbaum\b", re.IGNORECASE),
    re.compile(r"\bhistorisch", re.IGNORECASE),
    re.compile(r"\bchronik\b", re.IGNORECASE),
    re.compile(r"\barchiv\b", re.IGNORECASE),
    re.compile(r"\bfamiliengeschichte\b", re.IGNORECASE),
    re.compile(r"\burkunde\b", re.IGNORECASE),
]


# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------


def _decode_header_value(raw_value: str) -> str:
    """Dekodiert kodierte E-Mail-Header (z. B. UTF-8 oder Latin-1)."""
    if not raw_value:
        return ""
    try:
        return str(make_header(decode_header(raw_value)))
    except Exception:
        return raw_value


def _parse_email_date(date_str: str) -> datetime:
    """Parst das E-Mail-Datum und gibt ein timezone-aware datetime zurueck."""
    try:
        parsed = email.utils.parsedate_to_datetime(date_str)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=dt_timezone.utc)
        return parsed
    except Exception:
        return timezone.now()


def _get_email_body(msg) -> str:
    """Extrahiert den Text-Body aus einer E-Mail (bevorzugt plain text)."""
    body_parts = []
    if msg.is_multipart():
        for part in msg.walk():
            ctype = part.get_content_type()
            disposition = str(part.get_content_disposition() or "")
            if ctype == "text/plain" and "attachment" not in disposition:
                charset = part.get_content_charset() or "utf-8"
                try:
                    body_parts.append(part.get_payload(decode=True).decode(charset, errors="replace"))
                except Exception:
                    pass
    else:
        charset = msg.get_content_charset() or "utf-8"
        try:
            body_parts.append(msg.get_payload(decode=True).decode(charset, errors="replace"))
        except Exception:
            pass
    return "\n".join(body_parts).strip()


def _detect_kategorie(betreff: str, email_text: str, has_destinataer: bool) -> str:
    """
    Erkennt die Kategorie einer Email anhand von Betreff und Body.
    Gibt 'destinataer', 'rechnung', 'stiftungsgeschichte', oder 'allgemein' zurueck.
    """
    if has_destinataer:
        return "destinataer"

    text_to_check = f"{betreff}\n{email_text[:2000]}"

    # Rechnungserkennung via Patterns
    for pattern in RECHNUNG_PATTERNS:
        if pattern.search(text_to_check):
            return "rechnung"

    # Stiftungsgeschichte-Erkennung
    for pattern in GESCHICHTE_PATTERNS:
        if pattern.search(text_to_check):
            return "stiftungsgeschichte"

    return "allgemein"


def _save_to_dms(content: bytes, filename: str, destinataer=None, betreff: str = "", kontext: str = "korrespondenz"):
    """
    Speichert einen E-Mail-Anhang direkt als DokumentDatei im Django-DMS.

    Gibt das DokumentDatei-Objekt zurueck, oder None bei Fehler.
    """
    from stiftung.models import DokumentDatei
    from django.core.files.base import ContentFile

    safe_filename = filename or "anhang.bin"
    mime_type, _ = mimetypes.guess_type(safe_filename)
    mime_type = mime_type or "application/octet-stream"

    titel = f"{betreff[:100]} – {safe_filename}" if betreff else safe_filename
    beschreibung = ""
    if destinataer:
        beschreibung = (
            f"Automatisch importiert aus E-Mail-Eingang.\n"
            f"Absender: {destinataer.vorname} {destinataer.nachname} <{destinataer.email}>"
        )

    try:
        doc = DokumentDatei(
            titel=titel[:255],
            beschreibung=beschreibung,
            kontext=kontext,
            dateiname_original=safe_filename,
            dateityp=mime_type,
            dateigroesse=len(content),
            destinataer=destinataer,
        )
        doc.datei.save(safe_filename, ContentFile(content), save=False)
        doc.save()
        logger.info("Anhang '%s' als DokumentDatei gespeichert (ID: %s).", safe_filename, doc.pk)
        return doc
    except Exception as exc:
        logger.error("Fehler beim Speichern von '%s' im DMS: %s", safe_filename, exc)
        return None



# ---------------------------------------------------------------------------
# Haupttask
# ---------------------------------------------------------------------------


@shared_task(bind=True, max_retries=3, default_retry_delay=300, name="stiftung.tasks.poll_emails")
def poll_emails(self, search_all_recent_days=0):
    """
    Liest E-Mails aus dem IMAP-Postfach und verarbeitet sie.

    Wird durch Celery Beat alle 15 Minuten ausgefuehrt.
    Erkennt automatisch Destinataer-Emails, Rechnungen und allgemeine Post.

    Args:
        search_all_recent_days: Wenn > 0, werden alle E-Mails der letzten N Tage
            durchsucht (nicht nur ungelesene). Nuetzlich fuer manuellen Abruf.
    """
    from stiftung.models import Destinataer, EmailEingang

    # IMAP-Konfiguration: DB (AppConfiguration) mit Fallback auf env/settings
    from stiftung.utils.config import get_config

    imap_host = get_config("imap_host")
    imap_port = int(get_config("imap_port", 993))
    imap_user = get_config("imap_user")
    imap_password = get_config("imap_password")
    imap_folder = get_config("imap_folder", "INBOX")
    imap_use_ssl = get_config("imap_use_ssl", True)

    if not all([imap_host, imap_user, imap_password]):
        logger.warning(
            "IMAP nicht konfiguriert (IMAP_HOST, IMAP_USER, IMAP_PASSWORD fehlen). "
            "Task wird uebersprungen."
        )
        return {"status": "skipped", "reason": "IMAP not configured"}

    # Vorab: Destinataer-E-Mail-Index fuer schnelle Zuordnung
    # Nur aktive Destinataere mit gesetzter E-Mail-Adresse
    destinataer_by_email = {
        d.email.lower(): d
        for d in Destinataer.objects.filter(aktiv=True, email__isnull=False).exclude(email="")
    }

    processed = 0
    errors = 0

    try:
        # IMAP-Verbindung aufbauen (mit Socket-Timeout fuer grosse E-Mails)
        imap_timeout = 120  # Sekunden – genug fuer grosse Anhaenge
        if imap_use_ssl:
            mail = imaplib.IMAP4_SSL(imap_host, imap_port, timeout=imap_timeout)
        else:
            mail = imaplib.IMAP4(imap_host, imap_port, timeout=imap_timeout)

        mail.login(imap_user, imap_password)
        mail.select(imap_folder)

        # Nachrichten suchen
        if search_all_recent_days and search_all_recent_days > 0:
            from datetime import timedelta
            since_date = (datetime.now(dt_timezone.utc) - timedelta(days=search_all_recent_days)).strftime("%d-%b-%Y")
            _, message_ids_raw = mail.search(None, "SINCE", since_date)
            search_mode = f"ALL seit {since_date}"
        else:
            _, message_ids_raw = mail.search(None, "UNSEEN")
            search_mode = "UNSEEN"
        message_ids = message_ids_raw[0].split()

        logger.info("Postfach '%s' (%s): %d Nachricht(en) gefunden.", imap_folder, search_mode, len(message_ids))

        for msg_id in message_ids:
            try:
                _, msg_data = mail.fetch(msg_id, "(RFC822)")
                raw_email = msg_data[0][1]
                msg = email.message_from_bytes(raw_email)

                # Absender ermitteln
                from_raw = msg.get("From", "")
                absender_name_raw, absender_email_raw = email.utils.parseaddr(from_raw)
                absender_email_addr = absender_email_raw.lower().strip()
                absender_name = _decode_header_value(absender_name_raw)

                # Betreff
                betreff = _decode_header_value(msg.get("Subject", ""))

                # Eingangsdatum
                eingangsdatum = _parse_email_date(msg.get("Date", ""))

                # E-Mail-Text
                email_text = _get_email_body(msg)

                # Destinataer zuordnen
                destinataer = destinataer_by_email.get(absender_email_addr)

                # Kategorie erkennen
                kategorie = _detect_kategorie(betreff, email_text, has_destinataer=bool(destinataer))

                # Status basierend auf Kategorie
                if destinataer:
                    status = "zugewiesen"
                elif kategorie == "rechnung":
                    status = "neu"  # Muss manuell als Rechnung erfasst werden
                else:
                    status = "unbekannt"

                # DMS-Kontext fuer Anhaenge basierend auf Kategorie
                dms_kontext_map = {
                    "rechnung": "rechnung",
                    "stiftungsgeschichte": "stiftungsgeschichte",
                }
                dms_kontext = dms_kontext_map.get(kategorie, "korrespondenz")

                # Pruefen ob diese E-Mail bereits verarbeitet wurde (Duplikat-Check via
                # Datum + Absender + Betreff)
                already_exists = EmailEingang.objects.filter(
                    absender_email=absender_email_addr,
                    eingangsdatum=eingangsdatum,
                    betreff=betreff[:500],
                ).exists()
                if already_exists:
                    logger.debug(
                        "E-Mail von %s am %s bereits vorhanden – wird uebersprungen.",
                        absender_email_addr, eingangsdatum,
                    )
                    # Als gelesen markieren
                    mail.store(msg_id, "+FLAGS", "\\Seen")
                    continue

                # Datensatz anlegen
                eingang = EmailEingang(
                    kategorie=kategorie,
                    destinataer=destinataer,
                    absender_email=absender_email_addr,
                    absender_name=absender_name,
                    betreff=betreff[:500],
                    eingangsdatum=eingangsdatum,
                    email_text=email_text,
                    status=status,
                )

                # Anhaenge verarbeiten und als DokumentDatei im DMS speichern
                dms_dokumente = []
                if msg.is_multipart():
                    for part in msg.walk():
                        disposition = str(part.get_content_disposition() or "")
                        if "attachment" in disposition:
                            filename = _decode_header_value(part.get_filename() or "")
                            content = part.get_payload(decode=True)
                            if not content:
                                logger.warning(
                                    "Anhang '%s' hat keinen Inhalt – wird uebersprungen.",
                                    filename,
                                )
                                continue

                            doc = _save_to_dms(
                                content=content,
                                filename=filename,
                                destinataer=destinataer,
                                betreff=betreff,
                                kontext=dms_kontext,
                            )
                            if doc:
                                dms_dokumente.append(doc)

                # Cover-Email als eigenes DMS-Dokument speichern
                email_body_doc = None
                if email_text.strip():
                    email_filename = f"Email_{eingangsdatum.strftime('%Y%m%d_%H%M')}_{betreff[:50]}.txt"
                    # Bereinige Dateinamen
                    email_filename = re.sub(r'[^\w\s\-._]', '', email_filename)
                    anhang_count = len(dms_dokumente)
                    anhang_hinweis = (
                        f"\n\n--- Anhänge: {anhang_count} ---\n"
                        + "\n".join(f"  • {d.dateiname_original or d.titel}" for d in dms_dokumente)
                        if dms_dokumente else ""
                    )
                    email_body_content = (
                        f"Von: {absender_name} <{absender_email_addr}>\n"
                        f"Datum: {eingangsdatum.strftime('%d.%m.%Y %H:%M')}\n"
                        f"Betreff: {betreff}\n"
                        f"{'=' * 60}\n\n"
                        f"{email_text}"
                        f"{anhang_hinweis}"
                    )
                    email_body_doc = _save_to_dms(
                        content=email_body_content.encode("utf-8"),
                        filename=email_filename,
                        destinataer=destinataer,
                        betreff=betreff,
                        kontext="email",
                    )
                    if email_body_doc:
                        # Beschreibung mit Anhang-Verweis ergaenzen
                        if dms_dokumente:
                            email_body_doc.beschreibung = (
                                f"E-Mail-Nachricht mit {anhang_count} Anhang/Anhängen.\n"
                                f"Absender: {absender_name} <{absender_email_addr}>"
                            )
                        else:
                            email_body_doc.beschreibung = (
                                f"E-Mail-Nachricht (ohne Anhänge).\n"
                                f"Absender: {absender_name} <{absender_email_addr}>"
                            )
                        email_body_doc.save(update_fields=["beschreibung"])

                # Alle DMS-Dokumente (Email-Body + Anhaenge) verknuepfen
                alle_dms_dokumente = []
                if email_body_doc:
                    alle_dms_dokumente.append(email_body_doc)
                alle_dms_dokumente.extend(dms_dokumente)

                if dms_dokumente:
                    eingang.status = "verarbeitet" if destinataer else status
                eingang.save()
                if alle_dms_dokumente:
                    eingang.dokument_dateien.set(alle_dms_dokumente)

                # Als gelesen markieren
                mail.store(msg_id, "+FLAGS", "\\Seen")
                processed += 1
                logger.info(
                    "E-Mail verarbeitet: von=%s, Kategorie=%s, Destinataer=%s, Anhaenge=%d",
                    absender_email_addr,
                    kategorie,
                    str(destinataer) if destinataer else "–",
                    len(dms_dokumente),
                )

            except Exception as exc:
                errors += 1
                logger.exception("Fehler bei Verarbeitung von Nachricht %s: %s", msg_id, exc)
                # Nicht als gelesen markieren – wird beim naechsten Lauf erneut versucht

        mail.close()
        mail.logout()

    except imaplib.IMAP4.error as exc:
        logger.error("IMAP-Fehler: %s", exc)
        raise self.retry(exc=exc)
    except Exception as exc:
        logger.exception("Unerwarteter Fehler im poll_emails-Task: %s", exc)
        raise self.retry(exc=exc)

    result = {"status": "done", "processed": processed, "errors": errors}
    logger.info("poll_emails abgeschlossen: %s", result)
    return result


# Backward-compatible alias for existing Celery Beat schedules
poll_destinataer_emails = poll_emails


# =============================================================================
# SMTP-Ausgangs-Tasks: Nachweis-Aufforderungen und Token-Erinnerungen
# =============================================================================

import secrets  # noqa: E402 (wird hier benötigt)
from datetime import timedelta  # noqa: E402


def _get_smtp_connection():
    """
    Erstellt eine Django-E-Mail-Verbindung mit SMTP-Einstellungen aus der DB.
    """
    from django.core.mail import get_connection
    from stiftung.utils.config import get_config

    return get_connection(
        backend="django.core.mail.backends.smtp.EmailBackend",
        host=get_config("smtp_host", "smtp.ionos.de"),
        port=int(get_config("smtp_port", 465)),
        username=get_config("smtp_user", ""),
        password=get_config("smtp_password", ""),
        use_ssl=bool(get_config("smtp_use_ssl", True)),
        use_tls=False,
        fail_silently=False,
    )


def _get_smtp_from_email():
    """Gibt die konfigurierte Absenderadresse zurück."""
    from stiftung.utils.config import get_config
    return get_config("smtp_from_email", "buero@vhtv-stiftung.de")


@shared_task(bind=True, max_retries=3, default_retry_delay=300)
def send_nachweis_aufforderung(self, destinataer_id, nachweis_id, base_url=None):
    """
    Erstellt einen UploadToken und sendet eine Nachweis-Aufforderungs-E-Mail
    mit Einmallink und QR-Code an den Destinatär.

    Args:
        destinataer_id: UUID des Destinatärs
        nachweis_id: UUID des VierteljahresNachweises
        base_url: Basis-URL der Anwendung (z.B. 'https://vhtv-stiftung.de')
    """
    from django.conf import settings
    from django.core.mail import EmailMultiAlternatives
    from django.template.loader import render_to_string
    from django.utils import timezone
    import io
    try:
        import qrcode
        from PIL import Image
        import base64
        qr_available = True
    except ImportError:
        qr_available = False

    from stiftung.models import Destinataer, VierteljahresNachweis, UploadToken

    try:
        destinataer = Destinataer.objects.get(id=destinataer_id)
        nachweis = VierteljahresNachweis.objects.get(id=nachweis_id)
    except (Destinataer.DoesNotExist, VierteljahresNachweis.DoesNotExist) as exc:
        logger.error("send_nachweis_aufforderung: Objekt nicht gefunden: %s", exc)
        return {"status": "error", "message": str(exc)}

    if not destinataer.email:
        logger.warning(
            "send_nachweis_aufforderung: Destinatär %s hat keine E-Mail-Adresse",
            destinataer_id,
        )
        return {"status": "skipped", "reason": "no_email"}

    # Bestehende aktive Tokens für diesen Nachweis deaktivieren
    UploadToken.objects.filter(
        destinataer=destinataer,
        nachweis=nachweis,
        ist_aktiv=True,
    ).update(ist_aktiv=False)

    # Neuen Token erstellen
    token_str = secrets.token_urlsafe(48)
    gueltig_bis = timezone.now() + timedelta(days=30)
    upload_token = UploadToken.objects.create(
        token=token_str,
        destinataer=destinataer,
        nachweis=nachweis,
        gueltig_bis=gueltig_bis,
    )

    if base_url is None:
        base_url = getattr(settings, "SITE_URL", "https://vhtv-stiftung.de")

    upload_url = f"{base_url}/portal/upload/{token_str}/"

    # QR-Code generieren
    qr_code_base64 = None
    if qr_available:
        try:
            qr = qrcode.QRCode(
                version=1,
                error_correction=qrcode.constants.ERROR_CORRECT_M,
                box_size=6,
                border=4,
            )
            qr.add_data(upload_url)
            qr.make(fit=True)
            img = qr.make_image(fill_color="black", back_color="white")
            buffer = io.BytesIO()
            img.save(buffer, format="PNG")
            qr_code_base64 = base64.b64encode(buffer.getvalue()).decode()
        except Exception as qr_exc:
            logger.warning("QR-Code-Generierung fehlgeschlagen: %s", qr_exc)

    # Halbjahr bestimmen (Q1+Q2 = 1. Halbjahr, Q3+Q4 = 2. Halbjahr)
    halbjahr = 1 if nachweis.quartal in [1, 2] else 2
    halbjahr_label = f"{halbjahr}. Halbjahr {nachweis.jahr}"
    quartal_label = f"Q{nachweis.quartal} {nachweis.jahr}"

    context = {
        "destinataer": destinataer,
        "nachweis": nachweis,
        "upload_url": upload_url,
        "qr_code_base64": qr_code_base64,
        "gueltig_bis": gueltig_bis,
        "halbjahr_label": halbjahr_label,
        "quartal_label": quartal_label,
        "datenschutz_url": f"{base_url}/portal/datenschutz/",
    }

    subject = f"Nachweis-Aufforderung: {quartal_label} ({halbjahr_label}) – vHTV-Stiftung"
    from_email = _get_smtp_from_email()
    to_email = destinataer.email

    from stiftung.utils.vorlagen import render_vorlage
    text_body = render_vorlage("email/nachweis_aufforderung.txt", context)
    html_body = render_vorlage("email/nachweis_aufforderung.html", context)

    try:
        connection = _get_smtp_connection()
        msg = EmailMultiAlternatives(subject, text_body, from_email, [to_email], connection=connection)
        msg.attach_alternative(html_body, "text/html")
        msg.send()
        logger.info(
            "Nachweis-Aufforderung gesendet an %s (Token %s)",
            to_email,
            upload_token.id,
        )
        return {
            "status": "sent",
            "destinataer_id": str(destinataer_id),
            "nachweis_id": str(nachweis_id),
            "token_id": str(upload_token.id),
        }
    except Exception as exc:
        logger.exception("E-Mail-Versand fehlgeschlagen für %s: %s", to_email, exc)
        raise self.retry(exc=exc)


@shared_task(bind=True, max_retries=3, default_retry_delay=300)
def send_nachweis_erinnerung(self, token_id, base_url=None):
    """
    Sendet eine Erinnerungs-E-Mail für einen bald ablaufenden Upload-Token.
    Wird durch Celery Beat ausgelöst (7 Tage vor Ablauf).
    """
    from django.conf import settings
    from django.core.mail import EmailMultiAlternatives
    from django.template.loader import render_to_string

    from stiftung.models import UploadToken

    try:
        upload_token = UploadToken.objects.select_related(
            "destinataer", "nachweis"
        ).get(id=token_id, ist_aktiv=True)
    except UploadToken.DoesNotExist:
        return {"status": "skipped", "reason": "token_not_found_or_inactive"}

    if not upload_token.ist_gueltig():
        return {"status": "skipped", "reason": "token_invalid"}

    if not upload_token.destinataer.email:
        return {"status": "skipped", "reason": "no_email"}

    if base_url is None:
        base_url = getattr(settings, "SITE_URL", "https://vhtv-stiftung.de")

    upload_url = f"{base_url}/portal/upload/{upload_token.token}/"
    nachweis = upload_token.nachweis
    halbjahr = 1 if nachweis.quartal in [1, 2] else 2
    halbjahr_label = f"{halbjahr}. Halbjahr {nachweis.jahr}"

    context = {
        "destinataer": upload_token.destinataer,
        "nachweis": nachweis,
        "upload_url": upload_url,
        "gueltig_bis": upload_token.gueltig_bis,
        "halbjahr_label": halbjahr_label,
        "ist_erinnerung": True,
        "datenschutz_url": f"{base_url}/portal/datenschutz/",
    }

    subject = f"Erinnerung: Nachweis-Upload noch ausstehend – {halbjahr_label}"
    from_email = _get_smtp_from_email()
    to_email = upload_token.destinataer.email

    from stiftung.utils.vorlagen import render_vorlage
    text_body = render_vorlage("email/nachweis_aufforderung.txt", context)
    html_body = render_vorlage("email/nachweis_aufforderung.html", context)

    try:
        connection = _get_smtp_connection()
        msg = EmailMultiAlternatives(subject, text_body, from_email, [to_email], connection=connection)
        msg.attach_alternative(html_body, "text/html")
        msg.send()
        upload_token.erinnerung_gesendet = True
        upload_token.save(update_fields=["erinnerung_gesendet"])
        logger.info("Erinnerung gesendet an %s (Token %s)", to_email, token_id)
        return {"status": "sent", "token_id": str(token_id)}
    except Exception as exc:
        logger.exception("Erinnerungs-E-Mail fehlgeschlagen für %s: %s", to_email, exc)
        raise self.retry(exc=exc)


@shared_task(bind=True, max_retries=3, default_retry_delay=300)
def send_onboarding_einladung(self, einladung_id, base_url=None):
    """
    Sendet eine Onboarding-Einladungs-E-Mail an eine neue potenzielle Destinatärin/
    einen neuen potenziellen Destinatär.

    Args:
        einladung_id: UUID der OnboardingEinladung
        base_url: Basis-URL der Anwendung
    """
    from django.conf import settings
    from django.core.mail import EmailMultiAlternatives
    from django.template.loader import render_to_string

    from stiftung.models import OnboardingEinladung

    try:
        einladung = OnboardingEinladung.objects.get(id=einladung_id)
    except OnboardingEinladung.DoesNotExist as exc:
        logger.error("send_onboarding_einladung: Einladung %s nicht gefunden", einladung_id)
        return {"status": "error", "message": str(exc)}

    if not einladung.ist_gueltig():
        return {"status": "skipped", "reason": "einladung_ungueltig"}

    if base_url is None:
        base_url = getattr(settings, "SITE_URL", "https://vhtv-stiftung.de")

    onboarding_url = f"{base_url}/portal/onboarding/{einladung.token}/"

    context = {
        "einladung": einladung,
        "onboarding_url": onboarding_url,
        "gueltig_bis": einladung.gueltig_bis,
    }

    subject = "Einladung zum Onboarding – van Hees-Theyssen-Vogel'sche Stiftung"
    from_email = _get_smtp_from_email()
    to_email = einladung.email

    from stiftung.utils.vorlagen import render_vorlage
    text_body = render_vorlage("email/onboarding_einladung.txt", context)
    html_body = render_vorlage("email/onboarding_einladung.html", context)

    try:
        connection = _get_smtp_connection()
        msg = EmailMultiAlternatives(subject, text_body, from_email, [to_email], connection=connection)
        msg.attach_alternative(html_body, "text/html")
        msg.send()
        logger.info(
            "Onboarding-Einladung gesendet an %s (Einladung %s)",
            to_email,
            einladung_id,
        )
        return {"status": "sent", "einladung_id": str(einladung_id), "email": to_email}
    except Exception as exc:
        logger.exception("Onboarding-E-Mail-Versand fehlgeschlagen für %s: %s", to_email, exc)
        raise self.retry(exc=exc)


def _send_bestaetigung_sync(destinataer_id):
    """
    Generiert ein Bestätigungsschreiben (PDF) für einen Destinatär und sendet es
    per E-Mail. Das PDF wird zusätzlich im DMS unter Kontext "korrespondenz" abgelegt.

    Kann direkt (synchron) oder via Celery-Task aufgerufen werden.
    Bei Fehlern wird eine Exception geworfen (kein stilles Verschlucken).
    """
    from decimal import Decimal
    from django.core.files.base import ContentFile
    from django.core.mail import EmailMultiAlternatives
    from django.utils import timezone

    from stiftung.models import Destinataer, DestinataerUnterstuetzung, DokumentDatei

    try:
        destinataer = Destinataer.objects.get(id=destinataer_id)
    except Destinataer.DoesNotExist as exc:
        logger.error("send_bestaetigung: Destinatär %s nicht gefunden", destinataer_id)
        return {"status": "error", "message": str(exc)}

    if not destinataer.email:
        logger.warning("send_bestaetigung: Destinatär %s hat keine E-Mail-Adresse", destinataer_id)
        return {"status": "skipped", "reason": "no_email"}

    # Alle abgeschlossenen Unterstützungen laden
    unterstuetzungen = list(DestinataerUnterstuetzung.objects.filter(
        destinataer=destinataer,
        status__in=["ausgezahlt", "abgeschlossen"],
    ).order_by("faellig_am"))

    gesamtbetrag = sum(u.betrag for u in unterstuetzungen) if unterstuetzungen else Decimal("0")

    zeitraum = None
    if unterstuetzungen:
        erste = unterstuetzungen[0].faellig_am
        letzte = unterstuetzungen[-1].faellig_am
        if erste == letzte:
            zeitraum = erste.strftime("%d.%m.%Y")
        else:
            zeitraum = f"{erste.strftime('%d.%m.%Y')} – {letzte.strftime('%d.%m.%Y')}"

    betrag_quartal = destinataer.vierteljaehrlicher_betrag
    betrag_jaehrlich = (betrag_quartal * 4) if betrag_quartal else None
    zweck = destinataer.ausbildungsstand or (destinataer.get_berufsgruppe_display() if destinataer.berufsgruppe else None)

    datum = timezone.now().date()
    context = {
        "destinataer": destinataer,
        "unterstuetzungen": unterstuetzungen,
        "gesamtbetrag": gesamtbetrag,
        "datum": datum,
        "zeitraum": zeitraum,
        "betrag_quartal": betrag_quartal,
        "betrag_jaehrlich": betrag_jaehrlich,
        "zweck": zweck,
    }

    # PDF generieren via WeasyPrint
    pdf_bytes = None
    try:
        from weasyprint import HTML
        from stiftung.utils.vorlagen import render_vorlage
        html_content = render_vorlage("pdf/bestaetigung.html", context)
        pdf_bytes = HTML(string=html_content).write_pdf()
    except Exception as exc:
        logger.error("send_bestaetigung: PDF-Generierung fehlgeschlagen: %s", exc)
        raise

    # PDF im DMS ablegen
    filename = (
        f"bestaetigung_{destinataer.nachname}_{destinataer.vorname}"
        f"_{datum.strftime('%Y%m%d')}.pdf"
    )
    try:
        doc = DokumentDatei(
            titel=f"Bestätigungsschreiben {datum.strftime('%d.%m.%Y')} – {destinataer.get_full_name()}",
            beschreibung="Automatisch generiertes Bestätigungsschreiben über Förderleistungen.",
            kontext="korrespondenz",
            dateiname_original=filename,
            dateityp="application/pdf",
            dateigroesse=len(pdf_bytes),
            destinataer=destinataer,
        )
        doc.datei.save(filename, ContentFile(pdf_bytes), save=False)
        doc.save()
        logger.info("Bestätigung im DMS gespeichert (ID: %s).", doc.pk)
    except Exception as exc:
        logger.error("send_bestaetigung: DMS-Speicherung fehlgeschlagen: %s", exc)
        # Weiter mit E-Mail-Versand auch wenn DMS-Speicherung schlägt fehl

    # E-Mail senden
    from stiftung.utils.vorlagen import render_vorlage
    html_body = render_vorlage("email/bestaetigung.html", context)
    subject = "Bestätigung Ihrer Stiftungsförderung – van Hees-Theyssen-Vogel'sche Stiftung"
    from_email = _get_smtp_from_email()
    to_email = destinataer.email

    connection = _get_smtp_connection()
    msg = EmailMultiAlternatives(subject, "", from_email, [to_email], connection=connection)
    msg.attach_alternative(html_body, "text/html")
    if pdf_bytes:
        msg.attach(filename, pdf_bytes, "application/pdf")
    msg.send()
    logger.info("Bestätigung gesendet an %s (Destinatär %s)", to_email, destinataer_id)
    return {"status": "sent", "destinataer_id": str(destinataer_id), "email": to_email}


@shared_task(bind=True, max_retries=3, default_retry_delay=300)
def send_bestaetigung(self, destinataer_id, base_url=None):
    """Celery-Wrapper für _send_bestaetigung_sync (für asynchronen Aufruf)."""
    try:
        return _send_bestaetigung_sync(destinataer_id)
    except Exception as exc:
        logger.exception("send_bestaetigung task fehlgeschlagen: %s", exc)
        raise self.retry(exc=exc)


@shared_task
def check_ablaufende_tokens():
    """
    Prüft täglich Upload-Tokens, die in 7 Tagen ablaufen,
    und sendet Erinnerungs-E-Mails (falls noch nicht gesendet).
    Wird durch Celery Beat aufgerufen.
    """
    from django.utils import timezone

    from stiftung.models import UploadToken

    grenze = timezone.now() + timedelta(days=7)
    tokens = UploadToken.objects.filter(
        ist_aktiv=True,
        eingeloest_am__isnull=True,
        erinnerung_gesendet=False,
        gueltig_bis__lte=grenze,
        gueltig_bis__gt=timezone.now(),
    )
    count = 0
    for token in tokens:
        send_nachweis_erinnerung.delay(str(token.id))
        count += 1
    logger.info("check_ablaufende_tokens: %d Erinnerungen angestoßen", count)
    return {"triggered": count}
