from __future__ import annotations

import tkinter as tk
from collections.abc import Callable
from decimal import Decimal
from pathlib import Path
from tkinter import filedialog, messagebox, ttk

from ccma.config import AppConfig
from ccma.domain.contributions import money_text
from ccma.domain.dates import (
    DateValidationError,
    date_input_hint,
    format_date_for_display,
    normalize_date_input,
)
from ccma.services.gnucash_import import (
    GnuCashAccount,
    GnuCashImportError,
    GnuCashTransaction,
    list_accounts,
    list_transactions,
)
from ccma.storage.repository import MemberRepository, RepositoryError
from ccma.ui.monitors import centered_geometry, preferred_monitor


class GnuCashImportDialog(tk.Toplevel):
    """Lets the board point at a GnuCash file, pick one account, narrow its bookings
    down with filters, and select (multi-select: click, Ctrl+click, Shift+click for
    ranges) which ones to import as payments for this member.

    Only bookings with a positive amount on the selected account are offered (money
    coming in). A booking whose date + amount already match an existing payment for
    this member is flagged, but can still be selected -- at import time, the board is
    asked whether to skip those or instead adopt the booking's description onto the
    already-recorded payment, rather than silently blocking them."""

    def __init__(
        self,
        master: tk.Misc,
        repository: MemberRepository,
        member_id: str,
        config: AppConfig,
        on_imported: Callable[[], None],
    ):
        super().__init__(master)
        self.repository = repository
        self.member_id = member_id
        self.config = config
        self.on_imported = on_imported

        self.accounts: list[GnuCashAccount] = []
        self.account_by_label: dict[str, GnuCashAccount] = {}
        self.transactions: list[GnuCashTransaction] = []
        self.existing_payment_by_key: dict[tuple[str, str], str] = {}

        self.title("Zahlungen aus GnuCash importieren")
        self.transient(master.winfo_toplevel())
        monitor = preferred_monitor(master.winfo_toplevel())
        self.geometry(centered_geometry(round(monitor.width * 0.8), round(monitor.height * 0.8), monitor))
        self.minsize(780, 480)
        self.protocol("WM_DELETE_WINDOW", self.destroy)
        self.bind("<Escape>", lambda _event: self.destroy())
        self._build_ui()
        self._load_existing_payment_keys()
        self.after_idle(self._activate)
        if self.file_var.get().strip():
            self._load_file()

    def _activate(self) -> None:
        try:
            self.deiconify()
            self.lift()
            self.focus_force()
            self.grab_set()
        except tk.TclError:
            return

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)

        file_frame = ttk.Frame(self, padding=(16, 16, 16, 8))
        file_frame.grid(row=0, column=0, sticky="ew")
        file_frame.columnconfigure(1, weight=1)
        self.file_var = tk.StringVar(value=self.config.gnucash_path)
        ttk.Label(file_frame, text="GnuCash-Datei").grid(row=0, column=0, sticky="w", padx=(0, 12))
        ttk.Entry(file_frame, textvariable=self.file_var).grid(row=0, column=1, sticky="ew")
        ttk.Button(file_frame, text="Durchsuchen…", command=self._browse_file).grid(
            row=0, column=2, padx=(8, 0)
        )
        ttk.Label(file_frame, text="Konto").grid(row=1, column=0, sticky="w", padx=(0, 12), pady=(8, 0))
        self.account_var = tk.StringVar()
        self.account_combo = ttk.Combobox(
            file_frame, textvariable=self.account_var, state="readonly"
        )
        self.account_combo.grid(row=1, column=1, sticky="ew", pady=(8, 0))
        self.account_combo.bind("<<ComboboxSelected>>", lambda _event: self._account_selected())
        ttk.Button(file_frame, text="Datei laden", command=self._load_file).grid(
            row=1, column=2, padx=(8, 0), pady=(8, 0)
        )

        filter_frame = ttk.Frame(self, padding=(16, 0, 16, 8))
        filter_frame.grid(row=1, column=0, sticky="ew")
        ttk.Label(filter_frame, text="Beschreibung enthält").pack(side="left")
        self.description_filter_var = tk.StringVar()
        ttk.Entry(filter_frame, textvariable=self.description_filter_var, width=26).pack(
            side="left", padx=(8, 16)
        )
        self.description_filter_var.trace_add("write", lambda *_args: self._render_transactions())
        ttk.Label(filter_frame, text=f"Von ({date_input_hint()})").pack(side="left")
        self.date_from_var = tk.StringVar()
        ttk.Entry(filter_frame, textvariable=self.date_from_var, width=12).pack(side="left", padx=(8, 16))
        self.date_from_var.trace_add("write", lambda *_args: self._render_transactions())
        ttk.Label(filter_frame, text=f"Bis ({date_input_hint()})").pack(side="left")
        self.date_to_var = tk.StringVar()
        ttk.Entry(filter_frame, textvariable=self.date_to_var, width=12).pack(side="left", padx=(8, 0))
        self.date_to_var.trace_add("write", lambda *_args: self._render_transactions())

        tree_frame = ttk.Frame(self, padding=(16, 0, 16, 8))
        tree_frame.grid(row=2, column=0, sticky="nsew")
        tree_frame.columnconfigure(0, weight=1)
        tree_frame.rowconfigure(0, weight=1)
        self.tree = ttk.Treeview(
            tree_frame,
            columns=("date", "description", "memo", "amount", "status"),
            show="headings",
            selectmode="extended",
        )
        for key, title, width in (
            ("date", "Datum", 100),
            ("description", "Beschreibung", 280),
            ("memo", "Memo", 180),
            ("amount", "Betrag", 100),
            ("status", "Status", 220),
        ):
            self.tree.heading(key, text=title)
            self.tree.column(key, width=width, anchor="w", stretch=key in {"description", "memo"})
        self.tree.grid(row=0, column=0, sticky="nsew")
        scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.tree.configure(yscrollcommand=scrollbar.set)
        self.tree.tag_configure("duplicate", background="#7a2323", foreground="#ffffff")
        self.tree.bind("<<TreeviewSelect>>", lambda _event: self._update_summary())

        ttk.Label(
            self,
            text=(
                "Es werden nur Buchungen mit positivem Betrag auf dem gewählten Konto angezeigt "
                "(eingehende Zahlungen). Mehrfachauswahl per Klick, Strg+Klick oder Umschalt+Klick. "
                "Rot markierte Buchungen haben Datum und Betrag einer bereits vorhandenen Zahlung "
                "dieses Mitglieds; beim Import kann gewählt werden, ob diese übersprungen werden "
                "oder ob die Beschreibung der vorhandenen Zahlung ersetzt wird."
            ),
            style="Mono.TLabel",
            wraplength=900,
        ).grid(row=3, column=0, sticky="w", padx=16)

        summary_frame = ttk.Frame(self, padding=(16, 8, 16, 8))
        summary_frame.grid(row=4, column=0, sticky="ew")
        self.summary_var = tk.StringVar(value="Keine Datei geladen.")
        ttk.Label(summary_frame, textvariable=self.summary_var, style="Mono.TLabel").pack(side="left")

        buttons = ttk.Frame(self, padding=(16, 0, 16, 16))
        buttons.grid(row=5, column=0, sticky="e")
        ttk.Button(buttons, text="Schließen", command=self.destroy).pack(side="left", padx=(0, 8))
        self.import_button = ttk.Button(
            buttons,
            text="Ausgewählte importieren",
            style="Accent.TButton",
            command=self._import_selected,
        )
        self.import_button.pack(side="left")

    def _load_existing_payment_keys(self) -> None:
        data = self.repository.get_contributions(self.member_id)
        self.existing_payment_by_key = {}
        for payment in data.payments:
            key = (str(payment.get("date", "")), money_text(payment.get("amount", "0")))
            self.existing_payment_by_key.setdefault(key, str(payment.get("payment_id", "")))

    def _browse_file(self) -> None:
        current_text = self.file_var.get().strip()
        current = Path(current_text).expanduser() if current_text else Path.home()
        initial_dir = current.parent if current.is_file() else current
        selected = filedialog.askopenfilename(
            parent=self,
            title="GnuCash-Datei auswählen",
            initialdir=str(initial_dir) if initial_dir.is_dir() else str(Path.home()),
            filetypes=[("GnuCash-Dateien", "*.gnucash *.xac"), ("Alle Dateien", "*.*")],
        )
        if selected:
            self.file_var.set(selected)
            self._load_file()

    def _load_file(self) -> None:
        path_text = self.file_var.get().strip()
        if not path_text:
            messagebox.showinfo("GnuCash-Datei", "Bitte zuerst eine GnuCash-Datei auswählen.", parent=self)
            return
        path = Path(path_text).expanduser()
        if not path.is_file():
            messagebox.showerror("GnuCash-Datei", "Die ausgewählte Datei existiert nicht.", parent=self)
            return
        try:
            self.accounts = list_accounts(path)
        except GnuCashImportError as exc:
            messagebox.showerror("GnuCash-Datei konnte nicht gelesen werden", str(exc), parent=self)
            return
        self.account_by_label = {
            f"{account.full_name}  ({account.account_type})": account for account in self.accounts
        }
        self.account_combo.configure(values=list(self.account_by_label))
        if self.account_by_label and self.account_var.get() not in self.account_by_label:
            self.account_var.set(self._preferred_account_label() or next(iter(self.account_by_label)))
        if self.account_var.get():
            self._account_selected()
        else:
            self.transactions = []
            self._render_transactions()
        if path_text != self.config.gnucash_path:
            self.config.gnucash_path = path_text
            self.config.save()

    def _current_file_key(self) -> str | None:
        path_text = self.file_var.get().strip()
        if not path_text:
            return None
        path = Path(path_text).expanduser()
        if not path.is_file():
            return None
        return str(path.resolve())

    def _preferred_account_label(self) -> str | None:
        file_key = self._current_file_key()
        if file_key is None:
            return None
        last_guid = self.config.gnucash_last_accounts.get(file_key)
        if not last_guid:
            return None
        return next(
            (label for label, account in self.account_by_label.items() if account.guid == last_guid),
            None,
        )

    def _account_selected(self) -> None:
        account = self.account_by_label.get(self.account_var.get())
        if account is None:
            return
        path = Path(self.file_var.get().strip()).expanduser()
        try:
            all_transactions = list_transactions(path, account.guid)
        except GnuCashImportError as exc:
            messagebox.showerror("GnuCash-Datei konnte nicht gelesen werden", str(exc), parent=self)
            self.transactions = []
            self._render_transactions()
            return
        self.transactions = [item for item in all_transactions if item.amount > 0]
        self._render_transactions()
        file_key = self._current_file_key()
        if file_key is not None and self.config.gnucash_last_accounts.get(file_key) != account.guid:
            self.config.gnucash_last_accounts[file_key] = account.guid
            self.config.save()

    def _filtered_transactions(self) -> list[GnuCashTransaction]:
        needle = self.description_filter_var.get().strip().casefold()
        try:
            date_from = normalize_date_input(self.date_from_var.get(), "Von") or None
        except DateValidationError:
            date_from = None
        try:
            date_to = normalize_date_input(self.date_to_var.get(), "Bis") or None
        except DateValidationError:
            date_to = None
        result = []
        for item in self.transactions:
            if needle and needle not in item.description.casefold() and needle not in item.memo.casefold():
                continue
            iso = item.date.isoformat()
            if date_from and iso < date_from:
                continue
            if date_to and iso > date_to:
                continue
            result.append(item)
        return result

    def _duplicate_payment_id(self, item: GnuCashTransaction) -> str | None:
        return self.existing_payment_by_key.get((item.date.isoformat(), money_text(item.amount)))

    def _is_duplicate(self, item: GnuCashTransaction) -> bool:
        return self._duplicate_payment_id(item) is not None

    def _render_transactions(self) -> None:
        previously_selected = set(self.tree.selection())
        self.tree.delete(*self.tree.get_children())
        for item in self._filtered_transactions():
            duplicate = self._is_duplicate(item)
            self.tree.insert(
                "",
                "end",
                iid=item.guid,
                values=(
                    format_date_for_display(item.date.isoformat()),
                    item.description,
                    item.memo,
                    f"{money_text(item.amount)} EUR",
                    "Bereits vorhanden" if duplicate else "",
                ),
                tags=("duplicate",) if duplicate else (),
            )
        still_present = [guid for guid in previously_selected if self.tree.exists(guid)]
        if still_present:
            self.tree.selection_set(still_present)
        self._update_summary()

    def _update_summary(self) -> None:
        filtered = self._filtered_transactions()
        selected_guids = set(self.tree.selection())
        selected_items = [item for item in filtered if item.guid in selected_guids]
        total = sum((item.amount for item in selected_items), Decimal("0"))
        self.summary_var.set(
            f"{len(filtered)} Buchungen gefunden · {len(selected_items)} ausgewählt · "
            f"{money_text(total)} EUR"
        )

    def _import_selected(self) -> None:
        selected_guids = set(self.tree.selection())
        selected_items = [item for item in self.transactions if item.guid in selected_guids]
        if not selected_items:
            messagebox.showinfo("Import", "Bitte mindestens eine Buchung auswählen.", parent=self)
            return

        new_items = [item for item in selected_items if not self._is_duplicate(item)]
        duplicate_items = [item for item in selected_items if self._is_duplicate(item)]

        replace_description = False
        if duplicate_items:
            response = messagebox.askyesnocancel(
                "Bereits vorhandene Buchungen ausgewählt",
                (
                    f"{len(duplicate_items)} der ausgewählten Buchungen stimmen in Datum und Betrag "
                    "mit bereits vorhandenen Zahlungen dieses Mitglieds überein.\n\n"
                    "Ja: Diese werden beim Import übersprungen.\n"
                    "Nein: Bei diesen wird stattdessen die Beschreibung der vorhandenen Zahlung "
                    "durch die Beschreibung aus GnuCash ersetzt.\n"
                    "Abbrechen: Es wird nichts importiert."
                ),
                parent=self,
            )
            if response is None:
                return
            replace_description = not response

        imported = 0
        updated = 0
        processed_guids: set[str] = set()
        errors: list[str] = []
        for item in new_items:
            try:
                self.repository.create_payment(
                    self.member_id,
                    payment_date=item.date.isoformat(),
                    amount=money_text(item.amount),
                    reference=item.description,
                    gnucash_transaction_id=item.guid,
                )
                imported += 1
                processed_guids.add(item.guid)
            except RepositoryError as exc:
                errors.append(
                    f"{format_date_for_display(item.date.isoformat())} · {money_text(item.amount)} EUR: {exc}"
                )
        if replace_description:
            for item in duplicate_items:
                payment_id = self._duplicate_payment_id(item)
                if not payment_id:
                    continue
                try:
                    self.repository.update_payment_reference(
                        self.member_id,
                        payment_id,
                        reference=item.description,
                        gnucash_transaction_id=item.guid,
                    )
                    updated += 1
                    processed_guids.add(item.guid)
                except RepositoryError as exc:
                    errors.append(
                        f"{format_date_for_display(item.date.isoformat())} · "
                        f"{money_text(item.amount)} EUR: {exc}"
                    )

        if imported or updated:
            self._load_existing_payment_keys()
            self._render_transactions()
            still_selected = set(self.tree.selection()) - processed_guids
            self.tree.selection_set(list(still_selected))

        summary_parts = []
        if imported:
            summary_parts.append(f"{imported} Zahlung(en) importiert")
        if updated:
            summary_parts.append(f"{updated} Beschreibung(en) aktualisiert")
        skipped = len(duplicate_items) if not replace_description else 0
        if skipped:
            summary_parts.append(f"{skipped} übersprungen (bereits vorhanden)")
        summary = ", ".join(summary_parts) or "Keine Buchung verarbeitet."

        if errors:
            messagebox.showwarning(
                "Import teilweise fehlgeschlagen",
                f"{summary}.\n\nFehler:\n" + "\n".join(errors),
                parent=self,
            )
        else:
            messagebox.showinfo("Import abgeschlossen", f"{summary}.", parent=self)
        if imported or updated:
            self.on_imported()
