from __future__ import annotations

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

from ccma.config import AppConfig
from ccma.domain.models import HOUSEKEEPER_MEMBER_FIELD_LABELS
from ccma.services.intervals import (
    IntervalValidationError,
    normalize_anniversary_intervals,
)
from ccma.storage.repository import MemberRepository, RepositoryError, validate_member_number_pattern
from ccma.ui.changelog_view import ChangelogView
from ccma.ui.icons import IconStore
from ccma.ui.labels import THEME_LABELS, display_label, storage_key


class OptionsDialog(tk.Toplevel):
    def __init__(
        self,
        master: tk.Misc,
        config: AppConfig,
        repository: MemberRepository,
        on_saved: Callable[[bool], None] | None = None,
    ):
        super().__init__(master)
        self.config_obj = config
        self.repository = repository
        self.on_saved = on_saved
        self.icons = IconStore(self)
        self.store_var = tk.StringVar(value=config.store_path)
        self.gnucash_var = tk.StringVar(value=config.gnucash_path)
        self.theme_var = tk.StringVar(value=display_label(THEME_LABELS, config.theme_mode))
        self.housekeeper_var = tk.BooleanVar(value=config.run_housekeeper_on_startup)
        self.birthday_before_var = tk.StringVar(value=str(config.birthday_days_before))
        self.birthday_after_var = tk.StringVar(value=str(config.birthday_days_after))
        self.anniversary_before_var = tk.StringVar(value=str(config.anniversary_days_before))
        self.anniversary_after_var = tk.StringVar(value=str(config.anniversary_days_after))
        self.anniversary_intervals_var = tk.StringVar(value=config.anniversary_intervals)
        self.retroactive_claims_var = tk.BooleanVar(value=config.retroactive_claims)
        self.optional_member_field_vars = {
            field: tk.BooleanVar(value=field in config.optional_member_fields)
            for field in HOUSEKEEPER_MEMBER_FIELD_LABELS
        }
        number_policy = repository.get_member_number_policy()
        self.manual_numbers_var = tk.BooleanVar(value=number_policy["mode"] == "manual")
        self.number_pattern_var = tk.StringVar(value=number_policy["pattern"])
        self.number_strategy_var = tk.StringVar(value=number_policy["allocation_strategy"])
        self.number_preview_var = tk.StringVar()
        organization = repository.get_configuration().get("organization") or {}
        if not isinstance(organization, dict):
            organization = {}
        self.organization_vars = {
            key: tk.StringVar(value=str(organization.get(key, "")))
            for key in (
                "name", "street", "postal_code", "city", "country", "email", "phone",
                "website", "iban", "bic", "creditor_id",
            )
        }
        self.title("Optionen")
        self.geometry("900x650")
        self.minsize(760, 540)
        self.transient(master.winfo_toplevel())
        self.grab_set()
        self.resizable(True, True)
        self._build_ui()
        self.bind("<Escape>", lambda _event: self.destroy())
        self.after_idle(self._center_on_parent)

    def _build_ui(self) -> None:
        root = ttk.Frame(self, padding=16)
        root.grid(row=0, column=0, sticky="nsew")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)

        notebook = ttk.Notebook(root)
        notebook.grid(row=0, column=0, sticky="nsew")
        paths = ttk.Frame(notebook, padding=16)
        appearance = ttk.Frame(notebook, padding=16)
        member_numbers = ttk.Frame(notebook, padding=16)
        organization = ttk.Frame(notebook, padding=16)
        automation = ttk.Frame(notebook, padding=16)
        changelog = ChangelogView(notebook)
        notebook.add(paths, text="Pfade")
        notebook.add(appearance, text="Darstellung")
        notebook.add(member_numbers, text="Mitgliedsnummern")
        notebook.add(organization, text="Verein / Absender")
        notebook.add(automation, text="Hausmeister")
        notebook.add(changelog, text="Changelog")
        self._build_paths(paths)
        self._build_appearance(appearance)
        self._build_member_numbers(member_numbers)
        self._build_organization(organization)
        self._build_automation(automation)

        buttons = ttk.Frame(root)
        buttons.grid(row=1, column=0, sticky="e", pady=(12, 0))
        ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=(0, 8))
        save_icon = self.icons.get("content-save", 16)
        save = ttk.Button(
            buttons,
            text="Speichern",
            image=save_icon,
            compound="left",
            style="Accent.TButton",
            command=self._save,
        )
        save.image = save_icon
        save.pack(side="left")

    def _build_paths(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(1, weight=1)
        folder_icon = self.icons.get("folder-open", 18)
        file_icon = self.icons.get("file-search", 18) or folder_icon

        ttk.Label(parent, text="Mitglieder-Store").grid(row=0, column=0, sticky="w", padx=(0, 12), pady=6)
        ttk.Entry(parent, textvariable=self.store_var, width=62).grid(row=0, column=1, sticky="ew", pady=6)
        browse_store = ttk.Button(
            parent,
            text="Auswählen",
            image=folder_icon,
            compound="left",
            command=self._choose_store,
        )
        browse_store.image = folder_icon
        browse_store.grid(row=0, column=2, padx=(8, 0), pady=6)
        ttk.Label(
            parent,
            text="Ein Wechsel wird nach einem Neustart aktiv. Neue Verzeichnisse können angelegt werden.",
            style="Muted.TLabel",
            wraplength=620,
        ).grid(row=1, column=1, columnspan=2, sticky="w", pady=(0, 12))

        ttk.Label(parent, text="GnuCash-Datei").grid(row=2, column=0, sticky="w", padx=(0, 12), pady=6)
        ttk.Entry(parent, textvariable=self.gnucash_var).grid(row=2, column=1, sticky="ew", pady=6)
        browse_gnucash = ttk.Button(
            parent,
            text="Auswählen",
            image=file_icon,
            compound="left",
            command=self._choose_gnucash,
        )
        browse_gnucash.image = file_icon
        browse_gnucash.grid(row=2, column=2, padx=(8, 0), pady=6)
        ttk.Label(
            parent,
            text="Optional. Die Datei wird zukünftig ausschließlich lesend eingebunden.",
            style="Muted.TLabel",
        ).grid(row=3, column=1, columnspan=2, sticky="w")

    def _build_appearance(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(1, weight=1)
        ttk.Label(parent, text="Farbschema").grid(row=0, column=0, sticky="w", padx=(0, 12), pady=6)
        ttk.Combobox(
            parent,
            textvariable=self.theme_var,
            values=list(THEME_LABELS.values()),
            state="readonly",
            width=18,
        ).grid(row=0, column=1, sticky="w", pady=6)
        ttk.Label(
            parent,
            text="Das Theme wird direkt nach dem Speichern umgeschaltet.",
            style="Muted.TLabel",
        ).grid(row=1, column=1, sticky="w")

    def _build_organization(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(1, weight=1)
        fields = (
            ("Vereinsname", "name"),
            ("Straße und Hausnummer", "street"),
            ("Postleitzahl", "postal_code"),
            ("Ort", "city"),
            ("Land", "country"),
            ("E-Mail-Adresse", "email"),
            ("Telefonnummer", "phone"),
            ("Webseite", "website"),
            ("IBAN", "iban"),
            ("BIC", "bic"),
            ("SEPA-Gläubiger-ID", "creditor_id"),
        )
        for row, (label, key) in enumerate(fields):
            ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
            ttk.Entry(parent, textvariable=self.organization_vars[key], width=48).grid(
                row=row, column=1, sticky="ew", pady=5
            )

    def _build_automation(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(1, weight=1)
        ttk.Checkbutton(
            parent,
            text="Hausmeister beim Programmstart ausführen",
            variable=self.housekeeper_var,
            style="Switch",
        ).grid(row=0, column=0, columnspan=3, sticky="w", pady=6)
        ttk.Label(
            parent,
            text="Der Lauf prüft nur und führt keine Mahnungen oder Statusänderungen automatisch aus.",
            style="Muted.TLabel",
            wraplength=620,
        ).grid(row=1, column=0, columnspan=3, sticky="w", pady=(0, 18))

        ttk.Label(parent, text="Geburtstage melden").grid(row=2, column=0, sticky="w", pady=6)
        birthday_window = ttk.Frame(parent)
        birthday_window.grid(row=2, column=1, sticky="w", pady=6)
        ttk.Spinbox(birthday_window, from_=0, to=365, textvariable=self.birthday_before_var, width=5).pack(
            side="left"
        )
        ttk.Label(birthday_window, text="Tage vorher  / ").pack(side="left", padx=(6, 0))
        ttk.Spinbox(birthday_window, from_=0, to=365, textvariable=self.birthday_after_var, width=5).pack(
            side="left"
        )
        ttk.Label(birthday_window, text="Tage nachher").pack(side="left", padx=(6, 0))

        ttk.Label(parent, text="Jubiläen melden").grid(row=3, column=0, sticky="w", pady=6)
        anniversary_window = ttk.Frame(parent)
        anniversary_window.grid(row=3, column=1, sticky="w", pady=6)
        ttk.Spinbox(
            anniversary_window,
            from_=0,
            to=365,
            textvariable=self.anniversary_before_var,
            width=5,
        ).pack(side="left")
        ttk.Label(anniversary_window, text="Tage vorher  / ").pack(side="left", padx=(6, 0))
        ttk.Spinbox(
            anniversary_window,
            from_=0,
            to=365,
            textvariable=self.anniversary_after_var,
            width=5,
        ).pack(side="left")
        ttk.Label(anniversary_window, text="Tage nachher").pack(side="left", padx=(6, 0))

        ttk.Label(parent, text="Jubiläumsintervalle").grid(row=4, column=0, sticky="w", pady=6)
        ttk.Entry(parent, textvariable=self.anniversary_intervals_var).grid(
            row=4, column=1, sticky="ew", pady=6
        )
        ttk.Label(
            parent,
            text="Komma oder Semikolon; ohne Einheit = Jahre. Beispiel: 30D;2M;1;10Y.",
            style="Muted.TLabel",
        ).grid(row=5, column=1, sticky="w")
        ttk.Checkbutton(
            parent,
            text="Beitragsforderungen rückwirkend seit Beitritt anlegen",
            variable=self.retroactive_claims_var,
            style="Switch",
        ).grid(row=6, column=0, columnspan=3, sticky="w", pady=(18, 0))
        optional_fields = ttk.LabelFrame(parent, text="Bei leeren Mitgliedsfeldern nicht meckern", padding=12)
        optional_fields.grid(row=7, column=0, columnspan=3, sticky="ew", pady=(18, 0))
        for column in range(2):
            optional_fields.columnconfigure(column, weight=1)
        for index, (field, label) in enumerate(HOUSEKEEPER_MEMBER_FIELD_LABELS.items()):
            ttk.Checkbutton(
                optional_fields,
                text=label,
                variable=self.optional_member_field_vars[field],
            ).grid(row=index // 2, column=index % 2, sticky="w", padx=(0, 16), pady=4)

    def _build_member_numbers(self, parent: ttk.Frame) -> None:
        parent.columnconfigure(1, weight=1)
        ttk.Checkbutton(
            parent,
            text="Mitgliedsnummern manuell angeben",
            variable=self.manual_numbers_var,
            style="Switch",
            command=self._update_member_number_controls,
        ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 16))
        ttk.Label(parent, text="Automatisches Pattern").grid(
            row=1, column=0, sticky="w", padx=(0, 12), pady=6
        )
        self.pattern_entry = ttk.Entry(parent, textvariable=self.number_pattern_var, width=36)
        self.pattern_entry.grid(row=1, column=1, sticky="ew", pady=6)
        ttk.Label(
            parent,
            text="Platzhalter: {number}, {number:04d} und optional {year}",
            style="Muted.TLabel",
        ).grid(row=2, column=1, sticky="w", pady=(0, 12))
        ttk.Label(parent, text="Vergabestrategie").grid(
            row=3, column=0, sticky="nw", padx=(0, 12), pady=6
        )
        strategy_frame = ttk.Frame(parent)
        strategy_frame.grid(row=3, column=1, sticky="w", pady=6)
        ttk.Radiobutton(
            strategy_frame,
            text="Lücken füllen (nächste freie Nummer)",
            variable=self.number_strategy_var,
            value="next_free",
            command=self._update_number_preview,
        ).pack(anchor="w")
        ttk.Radiobutton(
            strategy_frame,
            text="Immer Max++ (höchste Nummer + 1)",
            variable=self.number_strategy_var,
            value="max_plus_one",
            command=self._update_number_preview,
        ).pack(anchor="w", pady=(4, 0))
        ttk.Label(parent, text="Nächste Nummer").grid(row=4, column=0, sticky="w", padx=(0, 12), pady=6)
        ttk.Label(parent, textvariable=self.number_preview_var, style="TimelineHeader.TLabel").grid(
            row=4, column=1, sticky="w", pady=6
        )
        ttk.Label(
            parent,
            text=(
                "Die Strategie wird im Mitglieder-Store gespeichert. Die Vergabe ist gesperrt, "
                "solange ein neues Mitglied angelegt wird, damit keine Nummer doppelt entsteht."
            ),
            style="Muted.TLabel",
            wraplength=620,
        ).grid(row=5, column=0, columnspan=2, sticky="w", pady=(14, 0))
        self.number_pattern_var.trace_add("write", lambda *_args: self._update_number_preview())
        self._update_member_number_controls()

    def _update_member_number_controls(self) -> None:
        if self.manual_numbers_var.get():
            self.pattern_entry.state(["disabled"])
            self.number_preview_var.set("MANUELLE EINGABE")
        else:
            self.pattern_entry.state(["!disabled"])
            self._update_number_preview()

    def _update_number_preview(self) -> None:
        if self.manual_numbers_var.get():
            return
        pattern = self.number_pattern_var.get()
        try:
            preview = self.repository.preview_member_number(
                pattern, self.number_strategy_var.get()
            )
        except RepositoryError as exc:
            preview = f"UNGÜLTIG: {exc}"
        self.number_preview_var.set(preview)

    def _choose_store(self) -> None:
        current = Path(self.store_var.get()).expanduser() if self.store_var.get().strip() else Path.home()
        initial = current if current.is_dir() else current.parent
        selected = filedialog.askdirectory(
            parent=self,
            title="Mitglieder-Store auswählen oder anlegen",
            initialdir=str(initial),
            mustexist=False,
        )
        if selected:
            self.store_var.set(selected)

    def _choose_gnucash(self) -> None:
        current = Path(self.gnucash_var.get()).expanduser() if self.gnucash_var.get().strip() else Path.home()
        initial = current.parent if current.suffix else current
        selected = filedialog.askopenfilename(
            parent=self,
            title="GnuCash-Datei auswählen",
            initialdir=str(initial),
            filetypes=(
                ("GnuCash-Dateien", "*.gnucash *.xac"),
                ("Alle Dateien", "*"),
            ),
        )
        if selected:
            self.gnucash_var.set(selected)

    def _save(self) -> None:
        number_mode = "manual" if self.manual_numbers_var.get() else "automatic"
        number_pattern = self.number_pattern_var.get().strip()
        try:
            validate_member_number_pattern(number_pattern)
        except RepositoryError as exc:
            messagebox.showerror("Ungültiges Mitgliedsnummern-Pattern", str(exc), parent=self)
            return
        try:
            birthday_before = _parse_day_window(self.birthday_before_var.get(), "Geburtstage vorher")
            birthday_after = _parse_day_window(self.birthday_after_var.get(), "Geburtstage nachher")
            anniversary_before = _parse_day_window(self.anniversary_before_var.get(), "Jubiläen vorher")
            anniversary_after = _parse_day_window(self.anniversary_after_var.get(), "Jubiläen nachher")
            anniversary_intervals = normalize_anniversary_intervals(self.anniversary_intervals_var.get())
        except (ValueError, IntervalValidationError) as exc:
            messagebox.showerror("Ungültige Hausmeister-Einstellung", str(exc), parent=self)
            return
        store_text = self.store_var.get().strip()
        if not store_text:
            messagebox.showerror(
                "Ungültige Einstellung", "Ein Mitglieder-Store ist erforderlich.", parent=self
            )
            return
        store = Path(store_text).expanduser().resolve()
        if store.exists() and not store.is_dir():
            messagebox.showerror(
                "Ungültiger Store", "Der Mitglieder-Store ist kein Verzeichnis.", parent=self
            )
            return
        if not store.exists():
            create = messagebox.askyesno(
                "Store anlegen",
                f"Das Verzeichnis existiert noch nicht:\n\n{store}\n\nJetzt anlegen?",
                parent=self,
            )
            if not create:
                return
            try:
                store.mkdir(parents=True)
            except OSError as exc:
                messagebox.showerror("Store konnte nicht angelegt werden", str(exc), parent=self)
                return

        gnucash_text = self.gnucash_var.get().strip()
        gnucash = Path(gnucash_text).expanduser().resolve() if gnucash_text else None
        if gnucash and not gnucash.is_file():
            messagebox.showerror(
                "Ungültige GnuCash-Datei", "Die ausgewählte Datei existiert nicht.", parent=self
            )
            return

        old_store = Path(self.config_obj.store_path).expanduser().resolve()
        store_changed = old_store != store
        self.config_obj.store_path = str(store)
        self.config_obj.gnucash_path = str(gnucash) if gnucash else ""
        self.config_obj.theme_mode = storage_key(THEME_LABELS, self.theme_var.get())
        self.config_obj.run_housekeeper_on_startup = self.housekeeper_var.get()
        self.config_obj.birthday_days_before = birthday_before
        self.config_obj.birthday_days_after = birthday_after
        self.config_obj.anniversary_days_before = anniversary_before
        self.config_obj.anniversary_days_after = anniversary_after
        self.config_obj.anniversary_intervals = anniversary_intervals
        self.config_obj.retroactive_claims = self.retroactive_claims_var.get()
        self.config_obj.optional_member_fields = tuple(
            field for field, variable in self.optional_member_field_vars.items() if variable.get()
        )
        try:
            self.config_obj.save()
            self.repository.save_member_number_policy(
                mode=number_mode,
                pattern=number_pattern,
                allocation_strategy=self.number_strategy_var.get(),
            )
            self.repository.save_organization(
                {key: variable.get() for key, variable in self.organization_vars.items()}
            )
        except (OSError, RepositoryError) as exc:
            messagebox.showerror("Optionen konnten nicht gespeichert werden", str(exc), parent=self)
            return
        if self.on_saved:
            self.on_saved(store_changed)
        self.destroy()

    def _center_on_parent(self) -> None:
        self.update_idletasks()
        parent = self.master.winfo_toplevel()
        x = parent.winfo_rootx() + max(0, (parent.winfo_width() - self.winfo_width()) // 2)
        y = parent.winfo_rooty() + max(0, (parent.winfo_height() - self.winfo_height()) // 2)
        self.geometry(f"+{x}+{y}")


def _parse_day_window(value: str, label: str) -> int:
    try:
        parsed = int(value.strip())
    except ValueError as exc:
        raise ValueError(f"{label} muss eine ganze Zahl sein.") from exc
    if not 0 <= parsed <= 365:
        raise ValueError(f"{label} muss zwischen 0 und 365 Tagen liegen.")
    return parsed
