from __future__ import annotations

import tkinter as tk
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk

from ccma.domain.contributions import CLAIM_STATUS_LABELS, claim_status, claim_total, money_text
from ccma.domain.dates import age_label, date_input_hint, format_date_for_display
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
from ccma.domain.models import Event
from ccma.storage.repository import MemberRepository, RepositoryError
from ccma.ui.document_dialog import DocumentTemplateDialog
from ccma.ui.file_open import open_path
from ccma.ui.labels import display_label, storage_key


class MemberTab(ttk.Frame):
    def __init__(
        self,
        master: tk.Misc,
        repository: MemberRepository,
        member_id: str,
        on_close: Callable[[], None],
        on_changed: Callable[[], None],
        on_open_claim: Callable[[str, str], None],
    ):
        super().__init__(master, padding=12)
        self.repository = repository
        self.member_id = member_id
        self.on_close = on_close
        self.on_changed = on_changed
        self.on_open_claim = on_open_claim
        self.member = repository.get_member(member_id)
        self.variables: dict[str, tk.Variable] = {}
        self._build_ui()
        self.refresh()

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        header = ttk.Frame(self)
        header.grid(row=0, column=0, sticky="ew", pady=(0, 10))
        header.columnconfigure(0, weight=1)
        self.title_var = tk.StringVar()
        self.status_var = tk.StringVar()
        ttk.Label(header, textvariable=self.title_var, style="TabTitle.TLabel").grid(
            row=0, column=0, sticky="w"
        )
        ttk.Label(header, textvariable=self.status_var, style="Mono.TLabel").grid(
            row=1, column=0, sticky="w", pady=(3, 0)
        )
        ttk.Button(header, text="Tab schließen", command=self.on_close).grid(
            row=0, column=1, rowspan=2, sticky="e"
        )

        self.pane = ttk.Panedwindow(self, orient="horizontal")
        self.pane.grid(row=1, column=0, sticky="nsew")
        self.details_pane = ttk.Frame(self.pane, padding=(0, 0, 10, 0))
        self.timeline_pane = ttk.Frame(self.pane, padding=(10, 0, 0, 0))
        self.pane.add(self.details_pane, weight=2)
        self.pane.add(self.timeline_pane, weight=3)
        self._build_details(self.details_pane)
        self._build_timeline(self.timeline_pane)
        self._pane_position_initialized = False
        self.pane.bind("<Configure>", self._set_initial_pane_position, add="+")

    def _set_initial_pane_position(self, event: tk.Event | None = None) -> None:
        if self._pane_position_initialized:
            return
        try:
            width = int(getattr(event, "width", 0)) or self.pane.winfo_width()
            if width > 1:
                self.pane.sashpos(0, max(360, int(width * 0.4)))
                self._pane_position_initialized = True
        except tk.TclError:
            return

    def _build_details(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(0, weight=1)
        parent.rowconfigure(0, weight=1)
        notebook = ttk.Notebook(parent)
        notebook.grid(row=0, column=0, sticky="nsew")
        data_tab = ttk.Frame(notebook, padding=16)
        address_tab = ttk.Frame(notebook, padding=16)
        banking_tab = ttk.Frame(notebook, padding=16)
        contribution_tab = ttk.Frame(notebook, padding=16)
        documents_tab = ttk.Frame(notebook, padding=16)
        notebook.add(data_tab, text="Stammdaten")
        notebook.add(address_tab, text="Anschrift")
        notebook.add(banking_tab, text="Bank / SEPA")
        notebook.add(contribution_tab, text="Forderungen")
        notebook.add(documents_tab, text="Dokumente")

        fields = [
            ("Mitgliedsnummer", "member_number"),
            ("Vorname", "first_name"),
            ("Nachname", "last_name"),
            ("E-Mail-Adresse", "email"),
            ("Telefonnummer", "phone"),
            (f"Geburtsdatum ({date_input_hint()})", "birth_date"),
            (f"Aufnahmebeschluss ({date_input_hint()})", "accepted_at"),
            (f"Mitglied seit ({date_input_hint()})", "membership_started_at"),
        ]
        for row, (label, key) in enumerate(fields):
            variable = tk.StringVar()
            self.variables[key] = variable
            ttk.Label(data_tab, text=label).grid(row=row, column=0, sticky="w", pady=5, padx=(0, 12))
            if key == "birth_date":
                birth_row = ttk.Frame(data_tab)
                birth_row.grid(row=row, column=1, sticky="ew", pady=5)
                birth_row.columnconfigure(0, weight=1)
                ttk.Entry(birth_row, textvariable=variable, width=24).grid(row=0, column=0, sticky="ew")
                self.age_var = tk.StringVar(value="Alter: —")
                ttk.Label(birth_row, textvariable=self.age_var, style="Mono.TLabel").grid(
                    row=0, column=1, sticky="w", padx=(10, 0)
                )
                variable.trace_add(
                    "write", lambda *_args, source=variable: self.age_var.set(age_label(source.get()))
                )
            else:
                ttk.Entry(data_tab, textvariable=variable, width=42).grid(
                    row=row, column=1, sticky="ew", pady=5
                )
        self.variables["status"] = tk.StringVar()
        ttk.Label(data_tab, text="Status").grid(row=len(fields), column=0, sticky="w", pady=5, padx=(0, 12))
        ttk.Combobox(
            data_tab,
            textvariable=self.variables["status"],
            values=list(STATUS_LABELS.values()),
            state="readonly",
            width=39,
        ).grid(row=len(fields), column=1, sticky="ew", pady=5)
        self.variables["notes"] = tk.StringVar()
        ttk.Label(data_tab, text="Interne Notiz").grid(
            row=len(fields) + 1, column=0, sticky="nw", pady=5, padx=(0, 12)
        )
        ttk.Entry(data_tab, textvariable=self.variables["notes"]).grid(
            row=len(fields) + 1, column=1, sticky="ew", pady=5
        )
        data_tab.columnconfigure(1, weight=1)
        ttk.Button(data_tab, text="Änderungen speichern", style="Accent.TButton", command=self._save).grid(
            row=len(fields) + 2, column=1, sticky="e", pady=(18, 0)
        )

        address_fields = (
            ("Straße und Hausnummer", "street"),
            ("Adresszusatz", "address_addition"),
            ("Postleitzahl", "postal_code"),
            ("Ort", "city"),
            ("Land", "country"),
        )
        address_tab.columnconfigure(1, weight=1)
        for row, (label, key) in enumerate(address_fields):
            self.variables[key] = tk.StringVar()
            ttk.Label(address_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
            ttk.Entry(address_tab, textvariable=self.variables[key], width=42).grid(
                row=row, column=1, sticky="ew", pady=5
            )
        ttk.Button(
            address_tab, text="Anschrift speichern", style="Accent.TButton", command=self._save
        ).grid(row=len(address_fields), column=1, sticky="e", pady=(18, 0))

        banking_fields = (
            ("Kontoinhaber", "account_holder"),
            ("IBAN", "iban"),
            ("BIC", "bic"),
            ("Mandatsreferenz", "mandate_reference"),
            (f"Mandat erteilt am ({date_input_hint()})", "mandate_signed_at"),
            (f"Mandat widerrufen am ({date_input_hint()})", "mandate_revoked_at"),
        )
        banking_tab.columnconfigure(1, weight=1)
        for row, (label, key) in enumerate(banking_fields):
            self.variables[key] = tk.StringVar()
            ttk.Label(banking_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
            ttk.Entry(banking_tab, textvariable=self.variables[key], width=42).grid(
                row=row, column=1, sticky="ew", pady=5
            )
        self.variables["mandate_active"] = tk.BooleanVar()
        ttk.Checkbutton(
            banking_tab,
            text="SEPA-Lastschriftmandat ist aktiv",
            variable=self.variables["mandate_active"],
            style="Switch",
        ).grid(row=len(banking_fields), column=0, columnspan=2, sticky="w", pady=(12, 5))
        ttk.Button(
            banking_tab, text="Bankdaten speichern", style="Accent.TButton", command=self._save
        ).grid(row=len(banking_fields) + 1, column=1, sticky="e", pady=(18, 0))

        contribution_tab.columnconfigure(0, weight=1)
        contribution_tab.rowconfigure(1, weight=1)
        self.contribution_summary = tk.StringVar()
        ttk.Label(contribution_tab, textvariable=self.contribution_summary, style="Mono.TLabel").grid(
            row=0, column=0, sticky="w", pady=(0, 10)
        )
        self.claims = ttk.Treeview(
            contribution_tab, columns=("title", "due", "amount", "status"), show="headings"
        )
        for key, title, width in (
            ("title", "Forderung", 220),
            ("due", "Fällig", 100),
            ("amount", "Betrag", 90),
            ("status", "Status", 110),
        ):
            self.claims.heading(key, text=title)
            self.claims.column(key, width=width, anchor="w")
        self.claims.grid(row=1, column=0, sticky="nsew")
        self.claims.bind("<Double-1>", lambda _event: self._open_selected_claim())
        self.claims.bind("<Return>", lambda _event: self._open_selected_claim())

        documents_tab.columnconfigure(0, weight=1)
        documents_tab.rowconfigure(1, weight=1)
        document_buttons = ttk.Frame(documents_tab)
        document_buttons.grid(row=0, column=0, sticky="ew", pady=(0, 10))
        ttk.Button(
            document_buttons,
            text="Dokument aus Template",
            style="Accent.TButton",
            command=self._create_document,
        ).pack(side="left", padx=(0, 8))
        ttk.Button(document_buttons, text="Dateiordner öffnen", command=self._open_files).pack(
            side="left"
        )
        self.documents = ttk.Treeview(
            documents_tab,
            columns=("name", "type", "modified", "size"),
            show="headings",
        )
        for key, title, width in (
            ("name", "Dokument", 280),
            ("type", "Typ", 65),
            ("modified", "Geändert", 135),
            ("size", "Größe", 80),
        ):
            self.documents.heading(key, text=title)
            self.documents.column(key, width=width, anchor="w")
        self.documents.grid(row=1, column=0, sticky="nsew")
        document_scroll = ttk.Scrollbar(
            documents_tab,
            orient="vertical",
            command=self.documents.yview,
        )
        document_scroll.grid(row=1, column=1, sticky="ns")
        self.documents.configure(yscrollcommand=document_scroll.set)
        self.documents.bind("<Double-1>", lambda _event: self._open_selected_document())
        self.documents.bind("<Return>", lambda _event: self._open_selected_document())
        self.document_paths: dict[str, Path] = {}

    def _build_timeline(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(0, weight=1)
        parent.rowconfigure(1, weight=1)
        ttk.Label(parent, text="// CHRONIK", style="TimelineHeader.TLabel").grid(
            row=0, column=0, sticky="w", pady=(0, 8)
        )
        self.timeline = ttk.Treeview(
            parent, columns=("time", "summary"), show="headings", style="Timeline.Treeview"
        )
        self.timeline.heading("time", text="Zeit")
        self.timeline.heading("summary", text="Ereignis")
        self.timeline.column("time", width=135, stretch=False)
        self.timeline.column("summary", width=320, stretch=True)
        self.timeline.grid(row=1, column=0, sticky="nsew")
        compose = ttk.Frame(parent)
        compose.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        compose.columnconfigure(0, weight=1)
        self.comment_var = tk.StringVar()
        comment = ttk.Entry(compose, textvariable=self.comment_var)
        comment.grid(row=0, column=0, sticky="ew", padx=(0, 6))
        comment.bind("<Return>", lambda _event: self._add_comment())
        ttk.Button(compose, text="Kommentar", command=self._add_comment).grid(row=0, column=1)

    def refresh(self) -> None:
        self.member = self.repository.get_member(self.member_id)
        self.title_var.set(f"{self.member.member_number or '—'} · {self.member.display_name}")
        self.status_var.set(STATUS_LABELS.get(self.member.status, self.member.status.upper()))
        date_fields = {
            "birth_date",
            "accepted_at",
            "membership_started_at",
            "mandate_signed_at",
            "mandate_revoked_at",
        }
        for key, variable in self.variables.items():
            value = getattr(self.member, key)
            if key == "status":
                variable.set(display_label(STATUS_LABELS, str(value)))
            else:
                variable.set(format_date_for_display(value) if key in date_fields else value)
        self._refresh_events()
        self._refresh_contributions()
        self._refresh_documents()

    def _refresh_events(self) -> None:
        self.timeline.delete(*self.timeline.get_children())
        try:
            events = self.repository.get_events(self.member_id)
        except RepositoryError as exc:
            messagebox.showerror("Chronik beschädigt", str(exc), parent=self)
            return
        for event in reversed(events):
            self.timeline.insert("", "end", values=(_format_timestamp(event), _event_label(event)))

    def _refresh_contributions(self) -> None:
        self.claims.delete(*self.claims.get_children())
        try:
            data = self.repository.get_contributions(self.member_id)
        except RepositoryError as exc:
            self.contribution_summary.set(f"FEHLER: {exc}")
            return
        for index, claim in enumerate(data.claims):
            claim_id = str(claim.get("claim_id") or f"missing-id-{index}")
            status = claim_status(data, claim)
            self.claims.insert(
                "",
                "end",
                iid=claim_id,
                values=(
                    claim.get("title", "Beitrag"),
                    format_date_for_display(str(claim.get("due_date", ""))),
                    money_text(claim_total(claim)),
                    CLAIM_STATUS_LABELS.get(status, status.upper()),
                ),
            )
        self.contribution_summary.set(f"{len(data.claims)} Forderungen · {len(data.payments)} Zahlungen")

    def _open_selected_claim(self) -> None:
        selected = self.claims.selection()
        if selected and not selected[0].startswith("missing-id-"):
            self.on_open_claim(self.member_id, selected[0])

    def _refresh_documents(self) -> None:
        self.documents.delete(*self.documents.get_children())
        self.document_paths.clear()
        root = self.repository.members_root / self.member_id / "files"
        for index, path in enumerate(sorted(root.rglob("*"))):
            if path.is_file():
                item_id = f"document:{index}"
                self.document_paths[item_id] = path
                try:
                    stat = path.stat()
                    modified = datetime.fromtimestamp(stat.st_mtime).strftime("%d.%m.%Y %H:%M")
                    size = _file_size(stat.st_size)
                except OSError:
                    modified = "—"
                    size = "—"
                self.documents.insert(
                    "",
                    "end",
                    iid=item_id,
                    values=(
                        path.relative_to(root),
                        path.suffix.removeprefix(".").upper() or "DATEI",
                        modified,
                        size,
                    ),
                )

    def _save(self) -> None:
        for key, variable in self.variables.items():
            raw_value = variable.get()
            value = raw_value.strip() if isinstance(raw_value, str) else raw_value
            if key == "status":
                value = storage_key(STATUS_LABELS, value)
            setattr(self.member, key, value)
        try:
            self.repository.save_member(self.member)
        except RepositoryError as exc:
            messagebox.showerror("Speichern fehlgeschlagen", str(exc), parent=self)
            return
        self.refresh()
        self.on_changed()

    def _add_comment(self) -> None:
        text = self.comment_var.get().strip()
        if not text:
            return
        self.repository.append_event(
            self.member_id,
            event_type="board_comment",
            summary=text,
            actor_type="user",
            actor_name="Vorstand",
        )
        self.comment_var.set("")
        self._refresh_events()

    def _open_files(self) -> None:
        path = self.repository.members_root / self.member_id / "files"
        self._open_path(path)

    def _open_selected_document(self) -> None:
        selected = self.documents.selection()
        if selected and selected[0] in self.document_paths:
            self._open_path(self.document_paths[selected[0]])

    def _open_path(self, path: Path) -> None:
        try:
            open_path(path)
        except OSError as exc:
            messagebox.showerror("Datei konnte nicht geöffnet werden", str(exc), parent=self)

    def _create_document(self) -> None:
        DocumentTemplateDialog(
            self,
            self.repository,
            self.member_id,
            self._document_generated,
        )

    def _document_generated(self, _path: Path) -> None:
        self._refresh_documents()
        self._refresh_events()
        self.on_changed()


def _format_timestamp(event: Event) -> str:
    try:
        return datetime.fromisoformat(event.timestamp).strftime("%d.%m.%Y %H:%M")
    except ValueError:
        return event.timestamp[:16]


def _event_label(event: Event) -> str:
    if event.actor_type == "system":
        return f"[AUTO] {event.summary}"
    return event.summary


def _file_size(size: int) -> str:
    if size < 1024:
        return f"{size} B"
    if size < 1024 * 1024:
        return f"{size / 1024:.1f} KiB"
    return f"{size / (1024 * 1024):.1f} MiB"
