from __future__ import annotations

import hashlib
import re
from contextlib import nullcontext
from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path

from ccma.domain.models import Member
from ccma.services.mail_composition import compose_mail, render_template, template_values
from ccma.services.mail_delivery import (
    append_message,
    ensure_imap_folder,
    imap_session,
    send_via_smtp,
    smtp_session,
)
from ccma.services.sepa import DirectDebit
from ccma.storage.repository import MemberRepository, RepositoryError


@dataclass(frozen=True, slots=True)
class GeneratedDebitMail:
    member_id: str
    recipient: str
    # None for "send"/"drafts" delivery -- see reminder_mail.GeneratedReminderMail.
    export_path: Path | None
    archive_path: Path


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


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 debit_mail_bytes(
    repository: MemberRepository,
    *,
    member: Member,
    debit: DirectDebit,
    collection_date: date,
    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)
    values.update(
        {
            "debit.amount": f"{debit.amount:.2f}",
            "debit.collection_date": collection_date.strftime("%d.%m.%Y"),
            "debit.purpose": debit.purpose,
            "debit.mandate_reference": debit.mandate_reference,
        }
    )
    rendered = render_template(repository, "sepa", values)
    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_debit_mails(
    repository: MemberRepository,
    debits: list[DirectDebit],
    *,
    collection_date: date,
    delivery_mode: str,
    output_directory: Path | str | None = None,
    sender_name: str,
    sender_email: str,
    signature: str,
) -> tuple[list[GeneratedDebitMail], list[str]]:
    repository.assert_writable()
    output: Path | None = None
    email_settings: dict | None = None
    smtp_ctx = nullcontext(None)
    imap_ctx = nullcontext(None)
    if delivery_mode == "local":
        if not output_directory:
            raise RepositoryError("Kein Zielordner für die lokale Ablage angegeben.")
        output = Path(output_directory)
        output.mkdir(parents=True, exist_ok=True)
    elif delivery_mode == "send":
        email_settings = repository.get_email_settings()
        smtp_ctx = smtp_session(email_settings)
        if email_settings["imap_sent_enabled"]:
            imap_ctx = imap_session(email_settings)
    elif delivery_mode == "drafts":
        email_settings = repository.get_email_settings()
        imap_ctx = imap_session(email_settings)
    else:
        raise RepositoryError(f"Unbekannter Versandmodus: {delivery_mode}")

    organization = repository.get_configuration().get("organization") or {}
    creditor_id = str(organization.get("creditor_id", "")).strip()
    if not creditor_id:
        raise RepositoryError("In den Optionen fehlt die Gläubiger-ID.")
    generated: list[GeneratedDebitMail] = []
    warnings: list[str] = []
    # One connection (or two, for "send" with a Sent-folder copy) is opened for the
    # whole batch -- a run can cover dozens of members -- instead of reconnecting/
    # re-authenticating per recipient.
    with smtp_ctx as smtp_client, imap_ctx as imap_client:
        if delivery_mode == "drafts":
            ensure_imap_folder(imap_client, email_settings["imap_drafts_folder"])
        elif delivery_mode == "send" and imap_client is not None:
            ensure_imap_folder(imap_client, email_settings["imap_sent_folder"])
        for debit in debits:
            member = repository.get_member(debit.member_id)
            if not member.email.strip():
                warnings.append(f"{member.member_number or member.display_name}: E-Mail-Adresse fehlt.")
                continue
            export_path: Path | None = None
            archive_path: Path | None = None
            # Once the mail has actually left the building for this debit -- SMTP
            # accepted it, it's filed in the IMAP folder, or the local export file was
            # written -- a later archiving failure must not abort the whole batch and
            # lose track of the fact that this one was already delivered; nor may it
            # abort earlier/later debits that have nothing to do with this failure.
            delivered = False
            archive_failure: Exception | None = None
            try:
                content = debit_mail_bytes(
                    repository,
                    member=member,
                    debit=debit,
                    collection_date=collection_date,
                    organization=organization,
                    sender_name=sender_name,
                    sender_email=sender_email,
                    signature=signature,
                )
                filename = (
                    f"SEPA-Info-{collection_date.isoformat()}-"
                    f"{_safe_filename(member.member_number or member.display_name)}.eml"
                )
                archive_dir = (
                    repository.members_root / member.member_id / "files" / "documents" / "SEPA"
                )
                archive_dir.mkdir(parents=True, exist_ok=True)
                archive_path = _available_path(archive_dir, filename)
                if delivery_mode == "local":
                    export_path = _available_path(output, filename)
                    export_path.write_bytes(content)
                    delivered = True
                elif delivery_mode == "send":
                    send_via_smtp(smtp_client, content)
                    delivered = True
                    if imap_client is not None:
                        append_message(
                            imap_client, content, folder=email_settings["imap_sent_folder"], flags=r"(\Seen)"
                        )
                else:
                    append_message(
                        imap_client, content, folder=email_settings["imap_drafts_folder"], flags=r"(\Draft)"
                    )
                    delivered = True
                archive_path.write_bytes(content)
            except (OSError, RepositoryError) as exc:
                if not delivered:
                    if export_path is not None:
                        export_path.unlink(missing_ok=True)
                    warnings.append(f"{member.member_number or member.display_name}: {exc}")
                    continue
                warnings.append(
                    f"{member.member_number or member.display_name}: E-Mail wurde versandt/abgelegt, "
                    f"konnte aber nicht archiviert werden ({exc}); bitte manuell prüfen."
                )
                archive_failure = exc

            digest = hashlib.sha256(content).hexdigest()
            references = {}
            if archive_failure is None:
                references["document"] = archive_path.relative_to(
                    repository.members_root / member.member_id / "files"
                ).as_posix()
            data = {
                "sha256": digest,
                "recipient": member.email,
                "collection_date": collection_date.isoformat(),
                "amount": f"{debit.amount:.2f}",
                "delivery_mode": delivery_mode,
            }
            if archive_failure is not None:
                data["archive_error"] = str(archive_failure)
            repository.append_event(
                member.member_id,
                event_type="sepa_notification_generated",
                summary=f"SEPA-Info-Mail erzeugt: {archive_path.name}",
                actor_type="user",
                actor_name="Vorstand",
                references=references,
                data=data,
            )
            generated.append(
                GeneratedDebitMail(member.member_id, member.email, export_path, archive_path)
            )
    return generated, warnings
