from __future__ import annotations

import hashlib
import os
import re
from contextlib import nullcontext
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

from ccma.domain.contributions import claim_balance, claim_total, money_text, reminder_items_total
from ccma.domain.dates import format_date_for_display
from ccma.domain.models import Member
from ccma.services.mail_composition import (
    compose_mail,
    payment_instructions,
    render_template,
    template_values,
)
from ccma.services.mail_delivery import (
    append_message,
    ensure_imap_folder,
    imap_session,
    send_via_smtp,
    smtp_session,
)
from ccma.storage.repository import MemberRepository, RepositoryError


@dataclass(frozen=True, slots=True)
class GeneratedReminderMail:
    # None for "send"/"drafts" delivery -- there is no local file, the mail was
    # handed straight to the mail server. Always set for "local" delivery.
    export_path: Path | None
    archive_path: Path
    sha256: str


def _safe_filename(value: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip()).strip("-._")
    return cleaned or "Mahnung"


def _available_path(directory: Path, filename: str) -> Path:
    candidate = directory / filename
    if not candidate.exists():
        return candidate
    stem, suffix = Path(filename).stem, Path(filename).suffix
    index = 2
    while (directory / f"{stem}-{index}{suffix}").exists():
        index += 1
    return directory / f"{stem}-{index}{suffix}"


def reminder_mail_bytes(
    repository: MemberRepository,
    *,
    member: Member,
    reminder: dict,
    claim: dict,
    balance: str,
    organization: dict,
    sender_name: str,
    sender_email: str,
    signature: str,
    created_at: datetime | None = None,
) -> bytes:
    values = template_values(member, organization, signature=signature, created_at=created_at)
    detail = str(reminder.get("detail", "")).strip()
    fee_items = reminder.get("items") or []
    due_date = format_date_for_display(str(claim.get("due_date", "")))
    payment_deadline = format_date_for_display(str(reminder.get("payment_deadline", "")))
    reference = " – ".join(
        part
        for part in (
            f"Mitgliedsnummer {member.member_number}" if member.member_number else "",
            str(claim.get("title", "")).strip(),
        )
        if part
    )
    values.update(
        {
            "claim.title": str(claim.get("title", "")),
            "claim.description": str(claim.get("description", "") or claim.get("title", "")),
            "claim.due_date": due_date,
            "claim.amount": money_text(claim_total(claim)),
            "claim.balance": balance,
            "reminder.name": str(reminder.get("name", "Zahlungserinnerung")),
            "reminder.level": str(reminder.get("level", "")),
            "reminder.payment_deadline": payment_deadline,
            "reminder.detail": detail,
            "reminder.hint": f"Hinweis: {detail}" if detail else "",
            "reminder.items": "\n".join(
                f"{item.get('description', 'Gebühr')}: {item.get('amount', '0.00')} Euro"
                for item in fee_items
            ),
            "reminder.fee_total": money_text(reminder_items_total(reminder)),
            "payment.reference": reference,
            "payment.instructions": payment_instructions(
                member, organization, due_date=payment_deadline, reference=reference
            ),
        }
    )
    rendered = render_template(
        repository,
        "reminder",
        values,
        {
            "reminder.items": [
                {
                    "item.description": str(item.get("description", "")),
                    "item.amount": str(item.get("amount", "0.00")),
                }
                for item in fee_items
            ]
        },
    )
    return compose_mail(
        recipient=member.email,
        subject=rendered.subject,
        body=rendered.body,
        sender_name=sender_name,
        sender_email=sender_email,
        created_at=created_at,
    )


def generate_and_send_reminder_mail(
    repository: MemberRepository,
    member_id: str,
    claim_id: str,
    reminder_id: str,
    *,
    delivery_mode: str,
    output_path: Path | str | None = None,
    sender_name: str,
    sender_email: str,
    signature: str,
) -> GeneratedReminderMail:
    if delivery_mode not in {"local", "send", "drafts"}:
        raise RepositoryError(f"Unbekannter Versandmodus: {delivery_mode}")
    repository.assert_writable()
    member = repository.get_member(member_id)
    data, claim = repository.get_claim(member_id, claim_id)
    reminder = repository._find_reminder(data, claim_id, reminder_id)
    if str(reminder.get("status", "")) not in {"draft", "generated"}:
        raise RepositoryError("Nur ein Mahnungsentwurf kann als E-Mail erzeugt werden.")
    if not member.email.strip():
        raise RepositoryError("Für das Mitglied ist keine E-Mail-Adresse hinterlegt.")
    if not sender_email.strip() or "@" not in sender_email:
        raise RepositoryError("Für die Mahnung ist eine gültige Absenderadresse erforderlich.")
    target: Path | None = None
    if delivery_mode == "local":
        if not output_path:
            raise RepositoryError("Kein Zielpfad für die lokale Ablage angegeben.")
        target = Path(output_path)
        target.parent.mkdir(parents=True, exist_ok=True)
        if target.exists():
            raise RepositoryError(f"Die Ausgabedatei existiert bereits: {target}")

    sent_reminder = repository.mark_reminder_sent(member_id, claim_id, reminder_id)
    export_path: Path | None = None
    # Once the mail has actually left the building -- landed on the SMTP server, been
    # filed into an IMAP folder, or been written to the local export file -- it must
    # not be un-sent again: a later failure (Sent-folder copy, moving the archive file
    # into place) can no longer roll the reminder back to "draft", or a retry could
    # send/file the same Mahnung a second time and double-book its fee. Everything
    # before that point -- including preparing the archive directory/path -- still
    # rolls back on failure, since nothing has actually gone out yet.
    delivered = False
    archive_failure: Exception | None = None
    archive_temp: Path | None = None
    try:
        updated_data, updated_claim = repository.get_claim(member_id, claim_id)
        organization = repository.get_configuration().get("organization") or {}
        content = reminder_mail_bytes(
            repository,
            member=member,
            reminder=sent_reminder,
            claim=updated_claim,
            balance=f"{claim_balance(updated_data, updated_claim):.2f}",
            organization=organization,
            sender_name=sender_name,
            sender_email=sender_email,
            signature=signature,
        )
        archive_dir = repository.members_root / member_id / "files" / "documents" / "Mahnungen"
        archive_dir.mkdir(parents=True, exist_ok=True)
        filename = (
            f"{datetime.now().date().isoformat()}-"
            f"{_safe_filename(str(sent_reminder.get('name', 'Mahnung')))}.eml"
        )
        archive_path = _available_path(archive_dir, filename)
        archive_temp = archive_path.with_name(f".{archive_path.name}.tmp")
        archive_temp.write_bytes(content)
        if delivery_mode == "local":
            export_temp = target.with_name(f".{target.name}.tmp")
            try:
                export_temp.write_bytes(content)
                os.replace(export_temp, target)
            finally:
                export_temp.unlink(missing_ok=True)
            export_path = target
            delivered = True
        elif delivery_mode == "send":
            settings = repository.get_email_settings()
            sent_copy = imap_session(settings) if settings["imap_sent_enabled"] else nullcontext(None)
            with smtp_session(settings) as smtp_client, sent_copy as imap_client:
                send_via_smtp(smtp_client, content)
                delivered = True
                if imap_client is not None:
                    ensure_imap_folder(imap_client, settings["imap_sent_folder"])
                    append_message(
                        imap_client, content, folder=settings["imap_sent_folder"], flags=r"(\Seen)"
                    )
        else:
            settings = repository.get_email_settings()
            with imap_session(settings) as client:
                ensure_imap_folder(client, settings["imap_drafts_folder"])
                append_message(client, content, folder=settings["imap_drafts_folder"], flags=r"(\Draft)")
                delivered = True
        os.replace(archive_temp, archive_path)
    except Exception as exc:
        if archive_temp is not None:
            archive_temp.unlink(missing_ok=True)
        if not delivered:
            repository.revert_reminder_sent(member_id, claim_id, reminder_id)
            raise
        archive_failure = exc

    digest = hashlib.sha256(content).hexdigest()
    references = {"claim_id": claim_id, "reminder_id": reminder_id}
    if archive_failure is None:
        references["document"] = archive_path.relative_to(
            repository.members_root / member_id / "files"
        ).as_posix()
    data = {
        "recipient": member.email,
        "sha256": digest,
        "payment_deadline": str(sent_reminder.get("payment_deadline", "")),
        "delivery_mode": delivery_mode,
    }
    if archive_failure is not None:
        data["archive_error"] = str(archive_failure)
    # Log the event even when archiving failed -- the mail was already delivered/filed,
    # so this must stay a traceable "sent" record instead of silently vanishing.
    repository.append_event(
        member_id,
        event_type="reminder_email_sent",
        summary=f"Mahnung per E-Mail versandt: {sent_reminder.get('name', '')}",
        actor_type="user",
        actor_name="Vorstand",
        references=references,
        data=data,
    )
    if archive_failure is not None:
        raise RepositoryError(
            "Die Mahnung wurde zugestellt bzw. abgelegt, konnte aber nicht vollständig "
            f"archiviert werden: {archive_failure}. Die Mahnung bleibt als versandt gebucht "
            "-- bitte die Mitgliedsakte manuell prüfen."
        ) from archive_failure
    return GeneratedReminderMail(export_path, archive_path, digest)
