from __future__ import annotations

import tkinter as tk
from datetime import date, timedelta
from decimal import Decimal
from pathlib import Path
from tkinter import filedialog, messagebox, ttk

from ccma.domain.dates import DateValidationError, normalize_date_input
from ccma.services.sepa import pending_direct_debits, write_csv, write_pain008
from ccma.services.sepa_mail import generate_debit_mails
from ccma.storage.repository import MemberRepository, RepositoryError
from ccma.ui.email_delivery_dialog import resolve_delivery_mode


def _next_weekday(selected: date) -> date:
    while selected.weekday() >= 5:
        selected += timedelta(days=1)
    return selected


class SepaExportDialog(tk.Toplevel):
    def __init__(self, master: tk.Misc, repository: MemberRepository):
        super().__init__(master)
        self.repository = repository
        self.debits = []
        self.warnings: list[str] = []
        self.title("SEPA-Lastschriften")
        self.transient(master.winfo_toplevel())
        self.geometry("1000x560")
        self.minsize(760, 420)
        self.from_var = tk.StringVar(value=date(date.today().year, 1, 1).isoformat())
        self.due_var = tk.StringVar(value=date.today().isoformat())
        self.collection_var = tk.StringVar(
            value=_next_weekday(date.today() + timedelta(days=2)).isoformat()
        )
        self.summary_var = tk.StringVar()
        self.warning_var = tk.StringVar()
        self._build_ui()
        self.refresh()

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(3, weight=1)
        filters = ttk.Frame(self, padding=12)
        filters.grid(row=0, column=0, sticky="ew")
        ttk.Label(filters, text="Fällig ab").grid(row=0, column=0)
        ttk.Entry(filters, textvariable=self.from_var, width=14).grid(
            row=0, column=1, padx=(8, 20)
        )
        ttk.Label(filters, text="Fällig bis").grid(row=0, column=2)
        ttk.Entry(filters, textvariable=self.due_var, width=14).grid(
            row=0, column=3, padx=(8, 20)
        )
        ttk.Label(filters, text="Einzugsdatum").grid(row=0, column=4)
        ttk.Entry(filters, textvariable=self.collection_var, width=14).grid(
            row=0, column=5, padx=(8, 20)
        )
        ttk.Button(filters, text="Liste aktualisieren", command=self.refresh).grid(row=0, column=6)
        ttk.Label(self, textvariable=self.summary_var, style="Mono.TLabel").grid(
            row=1, column=0, sticky="w", padx=12, pady=(0, 2)
        )
        ttk.Label(
            self,
            text=(
                "Standardmäßig sind alle Lastschriften ausgewählt -- einzelne mit Strg+Klick "
                "abwählen (z. B. während eine Rücklastschrift geklärt wird), um sie erst bei "
                "einem späteren Einzug zu berücksichtigen."
            ),
            style="Muted.TLabel",
            wraplength=960,
        ).grid(row=2, column=0, sticky="w", padx=12, pady=(0, 8))
        self.table = ttk.Treeview(
            self,
            columns=("number", "name", "iban", "mandate", "purpose", "amount"),
            show="headings",
            selectmode="extended",
        )
        for key, label, width, anchor in (
            ("number", "Mitglied", 100, "w"), ("name", "Kontoinhaber", 170, "w"),
            ("iban", "IBAN", 190, "w"), ("mandate", "Mandat", 130, "w"),
            ("purpose", "Verwendungszweck", 280, "w"), ("amount", "Betrag", 90, "e"),
        ):
            self.table.heading(key, text=label)
            self.table.column(key, width=width, anchor=anchor)
        self.table.grid(row=3, column=0, sticky="nsew", padx=12)
        self.table.bind("<<TreeviewSelect>>", lambda _event: self._update_summary())
        footer = ttk.Frame(self, padding=12)
        footer.grid(row=4, column=0, sticky="ew")
        footer.columnconfigure(0, weight=1)
        ttk.Label(footer, textvariable=self.warning_var).grid(row=0, column=0, sticky="w")
        ttk.Button(footer, text="CSV-Liste speichern", command=self._save_csv).grid(
            row=0, column=1, padx=(8, 0)
        )
        ttk.Button(footer, text="Info-Mails erzeugen", command=self._open_mail_export).grid(
            row=0, column=2, padx=(8, 0)
        )
        ttk.Button(
            footer, text="SEPA-XML speichern", style="Accent.TButton", command=self._save_xml
        ).grid(row=0, column=3, padx=(8, 0))
        ttk.Button(footer, text="Schließen", command=self.destroy).grid(
            row=0, column=4, padx=(8, 0)
        )

    def _dates(self) -> tuple[date, date, date]:
        try:
            due_from = date.fromisoformat(
                normalize_date_input(self.from_var.get(), "Fällig ab")
            )
            due = date.fromisoformat(normalize_date_input(self.due_var.get(), "Fällig bis"))
            collection = date.fromisoformat(
                normalize_date_input(self.collection_var.get(), "Einzugsdatum")
            )
        except (DateValidationError, ValueError) as exc:
            raise RepositoryError(str(exc)) from exc
        if due_from > due:
            raise RepositoryError("„Fällig ab“ darf nicht nach „Fällig bis“ liegen.")
        if collection.weekday() >= 5:
            raise RepositoryError(
                "Das Einzugsdatum muss ein Bankarbeitstag von Montag bis Freitag sein."
            )
        return due_from, due, collection

    def refresh(self) -> None:
        # Preserve which members were deliberately deselected across a refresh (e.g.
        # _prepare() re-querying right before an export) by identity, not by row index
        # -- a debit newly appearing in the requeried list still defaults to selected.
        previously_deselected = {
            self.debits[int(iid)].member_id
            for iid in self.table.get_children()
            if iid not in self.table.selection()
        }
        try:
            due_from, due, _collection = self._dates()
            self.debits, self.warnings = pending_direct_debits(
                self.repository, due_from=due_from, due_until=due
            )
        except RepositoryError as exc:
            messagebox.showerror("SEPA-Liste konnte nicht erstellt werden", str(exc), parent=self)
            return
        self.table.delete(*self.table.get_children())
        for index, debit in enumerate(self.debits):
            self.table.insert(
                "", "end", iid=str(index),
                values=(debit.member_number, debit.account_holder, debit.iban,
                        debit.mandate_reference, debit.purpose, f"{debit.amount:.2f} EUR"),
            )
        self.table.selection_set(
            [
                str(index)
                for index, debit in enumerate(self.debits)
                if debit.member_id not in previously_deselected
            ]
        )
        self._update_summary()
        self.warning_var.set(
            f"{len(self.warnings)} unvollständige(s) Mandat(e) übersprungen." if self.warnings else ""
        )

    def _selected_debits(self) -> list:
        return [self.debits[int(iid)] for iid in sorted(self.table.selection(), key=int)]

    def _update_summary(self) -> None:
        selected = self._selected_debits()
        total = sum((item.amount for item in self.debits), Decimal("0"))
        if len(selected) == len(self.debits):
            self.summary_var.set(f"{len(self.debits)} Lastschrift(en) · Summe {total:.2f} EUR")
            return
        selected_total = sum((item.amount for item in selected), Decimal("0"))
        self.summary_var.set(
            f"{len(selected)} von {len(self.debits)} Lastschrift(en) ausgewählt · "
            f"Summe {selected_total:.2f} EUR (gesamt {total:.2f} EUR)"
        )

    def _prepare(self) -> tuple[date, date, date] | None:
        self.refresh()
        if not self.debits:
            messagebox.showinfo(
                "Keine Lastschriften", "Für den Stichtag ist nichts einzuziehen.", parent=self
            )
            return None
        if not self._selected_debits():
            messagebox.showinfo(
                "Keine Auswahl", "Bitte mindestens eine Lastschrift auswählen.", parent=self
            )
            return None
        if self.warnings and not messagebox.askyesno(
            "Unvollständige Mandate",
            "\n".join(self.warnings[:10]) + "\n\nMit den übrigen Lastschriften fortfahren?",
            parent=self,
        ):
            return None
        return self._dates()

    def _target(self, extension: str, title: str, collection: date) -> Path | None:
        selected = filedialog.asksaveasfilename(
            parent=self, title=title, defaultextension=extension,
            initialfile=f"SEPA-Lastschriften-{collection.isoformat()}{extension}",
            filetypes=[(title, f"*{extension}"), ("Alle Dateien", "*.*")],
        )
        return Path(selected) if selected else None

    def _save_csv(self) -> None:
        dates = self._prepare()
        if not dates:
            return
        target = self._target(".csv", "CSV-Datei", dates[2])
        if target:
            write_csv(target, self._selected_debits(), collection_date=dates[2])
            messagebox.showinfo("Export gespeichert", f"CSV gespeichert:\n{target}", parent=self)

    def _save_xml(self) -> None:
        dates = self._prepare()
        if not dates:
            return
        target = self._target(".xml", "SEPA-XML", dates[2])
        if not target:
            return
        try:
            write_pain008(
                target, self._selected_debits(), collection_date=dates[2],
                organization=self.repository.get_configuration().get("organization") or {},
            )
        except RepositoryError as exc:
            messagebox.showerror("SEPA-XML konnte nicht erstellt werden", str(exc), parent=self)
            return
        messagebox.showinfo("Export gespeichert", f"SEPA-XML gespeichert:\n{target}", parent=self)

    def _open_mail_export(self) -> None:
        dates = self._prepare()
        if dates:
            SepaMailExportDialog(self, self.repository, self._selected_debits(), dates[2])


class SepaMailExportDialog(tk.Toplevel):
    def __init__(
        self,
        master: tk.Misc,
        repository: MemberRepository,
        debits: list,
        collection_date: date,
    ):
        super().__init__(master)
        self.repository = repository
        self.debits = debits
        self.collection_date = collection_date
        organization = repository.get_configuration().get("organization") or {}
        self.sender_name_var = tk.StringVar(
            value=str(organization.get("name") or "Verwaltung C3MA")
        )
        self.sender_email_var = tk.StringVar(value=str(organization.get("email") or ""))
        self.title("SEPA-Info-Mails erzeugen")
        self.transient(master)
        self.resizable(True, False)
        self.columnconfigure(1, weight=1)
        ttk.Label(
            self,
            text=(
                f"Für {len(debits)} Lastschrift(en) werden Thunderbird-kompatible "
                ".eml-Entwürfe erzeugt."
            ),
            wraplength=620,
        ).grid(row=0, column=0, columnspan=2, sticky="w", padx=14, pady=(14, 10))
        ttk.Label(self, text="Absendername").grid(row=1, column=0, sticky="w", padx=14, pady=5)
        ttk.Entry(self, textvariable=self.sender_name_var, width=55).grid(
            row=1, column=1, sticky="ew", padx=(0, 14), pady=5
        )
        ttk.Label(self, text="Absenderadresse").grid(row=2, column=0, sticky="w", padx=14, pady=5)
        ttk.Entry(self, textvariable=self.sender_email_var, width=55).grid(
            row=2, column=1, sticky="ew", padx=(0, 14), pady=5
        )
        ttk.Label(self, text="Signatur").grid(row=3, column=0, sticky="nw", padx=14, pady=5)
        self.signature = tk.Text(self, width=55, height=5, wrap="word")
        self.signature.grid(row=3, column=1, sticky="ew", padx=(0, 14), pady=5)
        self.signature.insert("1.0", "Der Vorstand\nChaos Computer Club Mannheim e.V.")
        buttons = ttk.Frame(self, padding=14)
        buttons.grid(row=4, column=0, columnspan=2, sticky="e")
        ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left")
        ttk.Button(
            buttons,
            text="Info-Mails erzeugen",
            style="Accent.TButton",
            command=self._generate,
        ).pack(side="left", padx=(8, 0))
        self.grab_set()

    def _generate(self) -> None:
        delivery_mode = resolve_delivery_mode(self, self.repository)
        if delivery_mode is None:
            return
        output_directory = None
        if delivery_mode == "local":
            selected = filedialog.askdirectory(
                parent=self,
                title="Ausgabeordner für Thunderbird-Entwürfe auswählen",
                mustexist=True,
            )
            if not selected:
                return
            output_directory = selected
        try:
            generated, warnings = generate_debit_mails(
                self.repository,
                self.debits,
                collection_date=self.collection_date,
                delivery_mode=delivery_mode,
                output_directory=output_directory,
                sender_name=self.sender_name_var.get(),
                sender_email=self.sender_email_var.get(),
                signature=self.signature.get("1.0", "end-1c"),
            )
        except (OSError, RepositoryError) as exc:
            messagebox.showerror("Info-Mails konnten nicht erzeugt werden", str(exc), parent=self)
            return
        location = {
            "local": f"in\n{output_directory}\nerzeugt",
            "send": "direkt versandt",
            "drafts": "als Entwurf im Postfach abgelegt",
        }[delivery_mode]
        detail = f"{len(generated)} Info-Mail(s) wurden {location} und in den Mitgliederakten archiviert."
        if warnings:
            detail += "\n\nÜbersprungen:\n" + "\n".join(warnings[:12])
        messagebox.showinfo("Info-Mails erzeugt", detail, parent=self)
        self.destroy()
