from __future__ import annotations

import tkinter as tk
from collections.abc import Callable
from decimal import Decimal, InvalidOperation
from tkinter import messagebox, ttk

from ccma.domain.contributions import (
    claim_balance,
    claim_status,
    decimal_value,
    money_text,
)
from ccma.domain.dates import date_input_hint, format_date_for_display
from ccma.storage.repository import MemberRepository, RepositoryError


class PaymentEditDialog(tk.Toplevel):
    def __init__(
        self,
        master: tk.Misc,
        repository: MemberRepository,
        member_id: str,
        payment_id: str,
        on_saved: Callable[[], None],
    ):
        super().__init__(master)
        self.repository = repository
        self.member_id = member_id
        self.payment_id = payment_id
        self.on_saved = on_saved
        self.data = repository.get_contributions(member_id)
        self.payment = next(
            (
                payment
                for payment in self.data.payments
                if str(payment.get("payment_id", "")) == payment_id
            ),
            None,
        )
        if self.payment is None:
            raise RepositoryError("Zahlung nicht gefunden.")
        self.allocations = self._current_allocations()
        self.claims_by_id = {
            str(claim.get("claim_id", "")): claim
            for claim in self.data.claims
            if str(claim.get("claim_id", ""))
            and (
                claim_status(self.data, claim) != "cancelled"
                or str(claim.get("claim_id", "")) in self.allocations
            )
        }
        self.capacities = self._claim_capacities()

        self.title("Zahlung bearbeiten")
        self.transient(master.winfo_toplevel())
        self.geometry("880x560")
        self.minsize(720, 460)
        self.protocol("WM_DELETE_WINDOW", self.destroy)
        self.bind("<Escape>", lambda _event: self.destroy())
        self._build_ui()
        self._refresh_claims()
        self.after_idle(self._activate)

    def _current_allocations(self) -> dict[str, str]:
        totals: dict[str, Decimal] = {}
        for allocation in self.data.allocations:
            if str(allocation.get("payment_id", "")) != self.payment_id:
                continue
            claim_id = str(allocation.get("claim_id", ""))
            totals[claim_id] = totals.get(claim_id, Decimal("0")) + decimal_value(
                allocation.get("amount", "0")
            )
        return {claim_id: money_text(amount) for claim_id, amount in totals.items() if claim_id}

    def _claim_capacities(self) -> dict[str, Decimal]:
        capacities = {}
        for claim_id, claim in self.claims_by_id.items():
            current = decimal_value(self.allocations.get(claim_id, "0"))
            capacities[claim_id] = max(claim_balance(self.data, claim) + current, Decimal("0"))
        return capacities

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        form = ttk.Frame(self, padding=16)
        form.grid(row=0, column=0, sticky="ew")
        form.columnconfigure(1, weight=1)
        self.variables = {
            "date": tk.StringVar(
                value=format_date_for_display(str(self.payment.get("date", "")))
            ),
            "amount": tk.StringVar(value=str(self.payment.get("amount", ""))),
            "gnucash": tk.StringVar(value=str(self.payment.get("gnucash_transaction_id", ""))),
            "reference": tk.StringVar(value=str(self.payment.get("reference", ""))),
        }
        fields = (
            (f"Zahlungsdatum ({date_input_hint()})", "date"),
            ("Zahlungsbetrag", "amount"),
            ("GnuCash-ID", "gnucash"),
            ("Referenz", "reference"),
        )
        for row, (label, key) in enumerate(fields):
            ttk.Label(form, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=4)
            ttk.Entry(form, textvariable=self.variables[key], width=70).grid(
                row=row, column=1, sticky="ew", pady=4
            )
        self.variables["amount"].trace_add("write", lambda *_args: self._refresh_totals())

        allocation_frame = ttk.LabelFrame(self, text="Aufteilung auf Forderungen", padding=12)
        allocation_frame.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 12))
        allocation_frame.columnconfigure(0, weight=1)
        allocation_frame.rowconfigure(0, weight=1)
        self.claims = ttk.Treeview(
            allocation_frame,
            columns=("title", "due", "available", "allocated"),
            show="headings",
            selectmode="browse",
        )
        for key, title, width in (
            ("title", "Forderung", 300),
            ("due", "Fällig", 110),
            ("available", "Maximal zuordenbar", 150),
            ("allocated", "Aktuell zugeordnet", 150),
        ):
            self.claims.heading(key, text=title)
            self.claims.column(key, width=width, anchor="w")
        self.claims.grid(row=0, column=0, sticky="nsew")
        scrollbar = ttk.Scrollbar(allocation_frame, orient="vertical", command=self.claims.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.claims.configure(yscrollcommand=scrollbar.set)
        self.claims.bind("<<TreeviewSelect>>", self._select_claim)

        allocation_actions = ttk.Frame(allocation_frame)
        allocation_actions.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        ttk.Label(allocation_actions, text="Betrag für ausgewählte Forderung").pack(side="left")
        self.allocation_var = tk.StringVar()
        ttk.Entry(allocation_actions, textvariable=self.allocation_var, width=14).pack(
            side="left", padx=(8, 8)
        )
        ttk.Button(allocation_actions, text="Zuordnung setzen", command=self._set_allocation).pack(
            side="left"
        )
        ttk.Button(allocation_actions, text="Zuordnung lösen", command=self._remove_allocation).pack(
            side="left", padx=(8, 0)
        )
        self.total_var = tk.StringVar()
        ttk.Label(allocation_actions, textvariable=self.total_var, style="Mono.TLabel").pack(
            side="right"
        )

        buttons = ttk.Frame(self, padding=(16, 0, 16, 16))
        buttons.grid(row=2, column=0, sticky="e")
        ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=(0, 8))
        ttk.Button(
            buttons,
            text="Zahlung speichern",
            style="Accent.TButton",
            command=self._save,
        ).pack(side="left")

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

    def _refresh_claims(self) -> None:
        selected = self.claims.selection()
        self.claims.delete(*self.claims.get_children())
        ordered = sorted(
            self.claims_by_id.items(),
            key=lambda item: (
                str(item[1].get("due_date", "")),
                str(item[1].get("title", "")).casefold(),
            ),
        )
        for claim_id, claim in ordered:
            self.claims.insert(
                "",
                "end",
                iid=claim_id,
                values=(
                    claim.get("title", "Forderung"),
                    format_date_for_display(str(claim.get("due_date", ""))),
                    f"{money_text(self.capacities[claim_id])} EUR",
                    f"{self.allocations.get(claim_id, '0.00')} EUR",
                ),
            )
        if selected and self.claims.exists(selected[0]):
            self.claims.selection_set(selected[0])
        self._refresh_totals()

    def _select_claim(self, _event=None) -> None:
        selected = self.claims.selection()
        if selected:
            self.allocation_var.set(self.allocations.get(selected[0], "0.00"))

    def _set_allocation(self) -> None:
        selected = self.claims.selection()
        if not selected:
            messagebox.showerror("Forderung auswählen", "Bitte eine Forderung auswählen.", parent=self)
            return
        try:
            amount = decimal_value(self.allocation_var.get(), "Zuordnung")
        except ValueError as exc:
            messagebox.showerror("Ungültige Zuordnung", str(exc), parent=self)
            return
        capacity = self.capacities[selected[0]]
        if amount < 0 or amount > capacity:
            messagebox.showerror(
                "Ungültige Zuordnung",
                f"Für diese Forderung können höchstens {money_text(capacity)} EUR zugeordnet werden.",
                parent=self,
            )
            return
        if amount:
            self.allocations[selected[0]] = money_text(amount)
        else:
            self.allocations.pop(selected[0], None)
        self._refresh_claims()

    def _remove_allocation(self) -> None:
        selected = self.claims.selection()
        if not selected:
            messagebox.showerror("Forderung auswählen", "Bitte eine Forderung auswählen.", parent=self)
            return
        self.allocations.pop(selected[0], None)
        self.allocation_var.set("0.00")
        self._refresh_claims()

    def _refresh_totals(self) -> None:
        allocated = sum(
            (decimal_value(value) for value in self.allocations.values()), Decimal("0")
        )
        try:
            payment_amount = decimal_value(self.variables["amount"].get())
            free = payment_amount - allocated
            self.total_var.set(
                f"Zugeordnet: {money_text(allocated)} EUR · Frei: {money_text(free)} EUR"
            )
        except (ValueError, InvalidOperation):
            self.total_var.set(f"Zugeordnet: {money_text(allocated)} EUR · Betrag ungültig")

    def _save(self) -> None:
        try:
            self.repository.update_payment(
                self.member_id,
                self.payment_id,
                payment_date=self.variables["date"].get(),
                amount=self.variables["amount"].get(),
                allocations=self.allocations,
                gnucash_transaction_id=self.variables["gnucash"].get(),
                reference=self.variables["reference"].get(),
            )
        except RepositoryError as exc:
            messagebox.showerror("Zahlung konnte nicht gespeichert werden", str(exc), parent=self)
            return
        self.destroy()
        self.on_saved()
