from __future__ import annotations

import tkinter as tk
from tkinter import messagebox, ttk

from ccma import __version__
from ccma.config import AppConfig
from ccma.domain.models import Asset, HousekeeperFinding, Member
from ccma.services.housekeeper import Housekeeper
from ccma.storage.repository import MemberRepository, RepositoryError
from ccma.ui.asset_tab import AssetTab
from ccma.ui.claim_tab import ClaimTab
from ccma.ui.dialogs import EditAssetDialog, IssueAssetDialog, NewAssetDialog, NewMemberDialog
from ccma.ui.icons import IconStore
from ccma.ui.member_tab import MemberTab
from ccma.ui.messages import MessageAction, MessageBannerList, TabMessage
from ccma.ui.options_dialog import OptionsDialog
from ccma.ui.sepa_dialog import SepaExportDialog
from ccma.ui.theme import load_theme
from ccma.ui.work_tabs import AssetsTab, DashboardTab, HousekeeperTab, MembersTab, SearchResultsTab


class TabManager:
    def __init__(self, notebook: ttk.Notebook):
        self.notebook = notebook
        self.tabs: dict[str, tk.Widget] = {}
        self.images: dict[str, tk.PhotoImage] = {}
        self.icon_names: dict[str, str] = {}

    def add(
        self,
        key: str,
        widget: tk.Widget,
        title: str,
        *,
        select: bool = True,
        image: tk.PhotoImage | None = None,
        icon_name: str | None = None,
    ) -> None:
        self.tabs[key] = widget
        options: dict[str, object] = {"text": title, "padding": (6, 3)}
        if image:
            options.update(image=image, compound="left")
            self.images[key] = image
        if icon_name:
            self.icon_names[key] = icon_name
        self.notebook.add(widget, **options)
        if select:
            self.notebook.select(widget)

    def focus(self, key: str) -> bool:
        widget = self.tabs.get(key)
        if widget and widget.winfo_exists():
            self.notebook.select(widget)
            return True
        self.tabs.pop(key, None)
        return False

    def close(self, key: str) -> None:
        widget = self.tabs.pop(key, None)
        self.images.pop(key, None)
        self.icon_names.pop(key, None)
        if widget and widget.winfo_exists():
            self.notebook.forget(widget)
            widget.destroy()

    def refresh_icons(self, icons: IconStore) -> None:
        for key, icon_name in self.icon_names.items():
            widget = self.tabs.get(key)
            image = icons.get(icon_name, 16)
            if widget and widget.winfo_exists() and image:
                self.images[key] = image
                self.notebook.tab(widget, image=image, compound="left")


class MainWindow(ttk.Frame):
    def __init__(
        self,
        master: tk.Tk,
        repository: MemberRepository,
        config: AppConfig,
        findings: list[HousekeeperFinding],
        validation_errors: list[str],
    ):
        super().__init__(master, padding=(12, 8))
        self.repository = repository
        self.config = config
        self.findings = findings
        self.validation_errors = validation_errors
        self.search_var = tk.StringVar()
        self.status_var = tk.StringVar(value="Bereit.")
        self.search_counter = 1
        self.icons = IconStore(self)
        self.ribbon: ttk.Frame | None = None
        self.housekeeper_button: ttk.Button | None = None
        self._build_ui()
        if validation_errors:
            self.after_idle(self._show_validation_warning)

    def _build_ui(self) -> None:
        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)
        self._build_ribbon()
        # The read-only notice sits above the tabs rather than inside one, so it stays
        # visible no matter which member, asset or housekeeper tab is in front.
        self.messages = MessageBannerList(self)
        self.messages.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        self.notebook = ttk.Notebook(self)
        self.notebook.grid(row=2, column=0, sticky="nsew", pady=(10, 0))
        self.tabs = TabManager(self.notebook)
        self.dashboard = DashboardTab(
            self.notebook,
            self.repository.member_count(),
            self.findings,
            self.open_housekeeper,
        )
        self.tabs.add(
            "dashboard",
            self.dashboard,
            "Dashboard",
            image=self.icons.get("view-dashboard", 16),
            icon_name="view-dashboard",
        )
        status = ttk.Frame(self, padding=(6, 5))
        status.grid(row=3, column=0, sticky="ew")
        status.columnconfigure(0, weight=1)
        ttk.Label(status, textvariable=self.status_var, style="Status.TLabel").grid(
            row=0, column=0, sticky="w"
        )
        self.store_var = tk.StringVar()
        ttk.Label(status, textvariable=self.store_var, style="Status.TLabel").grid(
            row=0, column=1, sticky="e"
        )
        # Everything that reflects write access is refreshed from one place, so a
        # store that becomes writable mid-session updates all of it together.
        self._refresh_store_state()

    def _refresh_store_state(self) -> None:
        """Window title, status bar and banner all say whether the store can be
        written to -- they are rebuilt together so none of them can be left behind."""
        read_only = self.repository.read_only
        title = f"CCMA · v{__version__}"
        self.master.title(f"{title} · NUR LESEN (Store schreibgeschützt)" if read_only else title)
        store_label = f"STORE {self.repository.root}"
        if read_only:
            store_label = f"{store_label}  ·  NUR LESEN"
        self.store_var.set(f"{store_label}  ·  VERSION {__version__}")
        messages = []
        if read_only:
            messages.append(
                TabMessage(
                    "warning",
                    "ACHTUNG: Der Mitglieder-Store ist schreibgeschützt eingebunden. "
                    "Alle Daten sind nur lesbar – Änderungen, Dokumente, E-Mails und der "
                    "Hausmeister sind deaktiviert. Den Store mit Schreibrechten einbinden "
                    "und hier auf „Erneut prüfen“ klicken – ein Neustart ist dafür nicht "
                    "nötig.",
                    MessageAction("Erneut prüfen", self._recheck_store_access),
                )
            )
        self.messages.set_messages(messages)

    def _recheck_store_access(self) -> None:
        """Lets the board remount the volume without restarting first -- if it came
        back writable, every read-only marker goes away and the housekeeper, skipped
        at startup, can run right away."""
        if self.repository.refresh_read_only():
            self._refresh_store_state()
            self.status_var.set("Der Store ist weiterhin schreibgeschützt.")
            return
        self._refresh_store_state()
        self.status_var.set("Der Store ist wieder beschreibbar.")
        # The startup pass was skipped, so the task list is empty rather than current:
        # offer the run that fills it instead of leaving a misleading dashboard.
        if messagebox.askyesno(
            "Store beschreibbar",
            "Der Mitglieder-Store ist jetzt mit Schreibrechten eingebunden. Der "
            "Hausmeister wurde beim Start übersprungen, seine Vorgangsliste ist daher "
            "noch leer.\n\nJetzt einen Hausmeisterlauf starten?",
            parent=self,
        ):
            self.run_housekeeper()

    def _refuse_read_only(self, action: str) -> bool:
        if not self.repository.read_only:
            return False
        messagebox.showwarning(
            "Store ist schreibgeschützt",
            f"{action} ist nicht möglich, solange der Mitglieder-Store nur lesend "
            "eingebunden ist.",
            parent=self,
        )
        return True

    def _build_ribbon(self) -> None:
        ribbon = ttk.Frame(self, style="Ribbon.TFrame")
        self.ribbon = ribbon
        ribbon.grid(row=0, column=0, sticky="ew")
        ribbon.columnconfigure(2, weight=1)
        title = ttk.Frame(ribbon)
        title.grid(row=0, column=0, sticky="w", padx=(0, 18))
        ttk.Label(title, text="CCMA", style="AppTitle.TLabel").pack(anchor="w")
        ttk.Label(title, text="// MEMBER ADMIN", style="Mono.TLabel").pack(anchor="w")
        search = ttk.Frame(ribbon)
        search.grid(row=0, column=1, sticky="w")
        entry = ttk.Entry(search, textvariable=self.search_var, font=("TkDefaultFont", 11), width=40)
        entry.grid(row=0, column=0, sticky="w")
        entry.bind("<Return>", lambda _event: self.search())
        entry.bind("<Control-l>", lambda _event: entry.focus_set())
        ttk.Label(search, text="Name · E-Mail · Geburtsdatum · Mitgliedsnummer", style="Mono.TLabel").grid(
            row=1, column=0, sticky="w", pady=(3, 0)
        )
        search_icon = self.icons.get("magnify", 24)
        search_button = ttk.Button(
            search,
            text="Suchen",
            image=search_icon,
            compound="top",
            width=14,
            command=self.search,
        )
        search_button.image = search_icon
        search_button.grid(row=0, column=1, rowspan=2, sticky="ns", padx=(6, 0))

        ttk.Separator(ribbon, orient="vertical").grid(
            row=0,
            column=3,
            sticky="ns",
            padx=16,
        )
        actions = ttk.Frame(ribbon)
        actions.grid(row=0, column=4, sticky="e")
        members_icon = self.icons.get("account-group", 24) or self.icons.get("account-multiple", 24)
        members_button = ttk.Button(
            actions,
            text="Mitglieder",
            image=members_icon,
            compound="top",
            width=14,
            command=self.open_members,
        )
        members_button.image = members_icon
        members_button.pack(side="left", padx=(0, 6))
        assets_icon = self.icons.get("key-variant", 24) or self.icons.get("package-variant-closed", 24)
        assets_button = ttk.Button(
            actions,
            text="Inventar",
            image=assets_icon,
            compound="top",
            width=14,
            command=self.open_assets,
        )
        assets_button.image = assets_icon
        assets_button.pack(side="left", padx=(0, 6))
        sepa_icon = self.icons.get("bank-transfer", 24) or self.icons.get("bank", 24)
        sepa_button = ttk.Button(
            actions, text="SEPA", image=sepa_icon, compound="top", width=14,
            command=self.open_sepa_export,
        )
        sepa_button.image = sepa_icon
        sepa_button.pack(side="left", padx=(0, 6))
        new_icon = self.icons.get("account-plus", 24)
        new_button = ttk.Button(
            actions,
            text="Neues Mitglied",
            image=new_icon,
            compound="top",
            width=14,
            command=self.new_member,
        )
        new_button.image = new_icon
        new_button.pack(side="left", padx=(0, 6))
        housekeeper_icon = self.icons.get("broom", 24) or self.icons.get("clipboard-check", 24)
        self.housekeeper_button = ttk.Button(
            actions,
            text=f"Hausmeister ({len(self.findings)})",
            image=housekeeper_icon,
            compound="top",
            width=14,
            style="Accent.TButton",
            command=self.open_housekeeper,
        )
        self.housekeeper_button.image = housekeeper_icon
        self.housekeeper_button.pack(side="left", padx=(0, 6))
        theme_icon = self.icons.get("theme-light-dark", 24)
        theme_button = ttk.Button(
            actions,
            text="Light/Dark",
            image=theme_icon,
            compound="top",
            width=14,
            command=self.toggle_theme,
        )
        theme_button.image = theme_icon
        theme_button.pack(side="left", padx=(0, 6))
        options_icon = self.icons.get("cog", 24)
        options_button = ttk.Button(
            actions,
            text="Optionen",
            image=options_icon,
            compound="top",
            width=14,
            command=self.open_options,
        )
        options_button.image = options_icon
        options_button.pack(side="left", padx=(0, 6))
        exit_icon = self.icons.get("logout", 24) or self.icons.get("exit-to-app", 24)
        exit_button = ttk.Button(
            actions,
            text="Beenden",
            image=exit_icon,
            compound="top",
            width=14,
            command=getattr(self.master, "close", self.master.destroy),
        )
        exit_button.image = exit_icon
        exit_button.pack(side="left")

    def search(self) -> None:
        query = self.search_var.get().strip()
        if not query:
            self.status_var.set("Bitte einen Suchbegriff eingeben.")
            return
        results = self.repository.search(query)
        self.search_var.set("")
        if len(results) == 1:
            self.open_member(results[0].member_id)
            self.status_var.set(f"Eindeutiger Treffer: {results[0].display_name}")
            return
        key = f"search:{self.search_counter}"
        self.search_counter += 1
        tab = SearchResultsTab(
            self.notebook,
            query,
            results,
            self.open_member,
            lambda: self.tabs.close(key),
        )
        self.tabs.add(
            key,
            tab,
            f"Suche: {query[:20]}",
            image=self.icons.get("format-list-text", 16),
            icon_name="format-list-text",
        )
        self.status_var.set(f"{len(results)} Treffer für {query}")

    def open_member(self, member_id: str) -> None:
        key = f"member:{member_id}"
        if self.tabs.focus(key):
            return
        try:
            member = self.repository.get_member(member_id)
        except RepositoryError as exc:
            self.status_var.set(str(exc))
            messagebox.showerror("Mitgliederakte beschädigt", str(exc), parent=self)
            return
        tab = MemberTab(
            self.notebook,
            self.repository,
            member_id,
            self.config,
            on_close=lambda: self.tabs.close(key),
            on_changed=self.refresh_overview,
            on_open_claim=self.open_claim,
            on_open_assets=self.open_assets,
            on_open_asset=self.open_asset,
            on_return_asset=self.return_asset,
            on_run_housekeeper=self.run_housekeeper_for_member,
        )
        self.tabs.add(
            key,
            tab,
            member.display_name or member.member_number,
            image=self.icons.get("account", 16),
            icon_name="account",
        )

    def open_claim(self, member_id: str, claim_id: str) -> None:
        key = f"claim:{member_id}:{claim_id}"
        if self.tabs.focus(key):
            return
        try:
            _data, claim = self.repository.get_claim(member_id, claim_id)
        except RepositoryError as exc:
            messagebox.showerror("Forderung konnte nicht geöffnet werden", str(exc), parent=self)
            return
        tab = ClaimTab(
            self.notebook,
            self.repository,
            member_id,
            claim_id,
            on_close=lambda: self.tabs.close(key),
            on_changed=lambda: self._claim_changed(member_id),
        )
        self.tabs.add(
            key,
            tab,
            str(claim.get("title") or "Forderung"),
            image=self.icons.get("receipt", 16) or self.icons.get("file-document", 16),
            icon_name="receipt",
        )

    def open_asset(self, asset_id: str) -> None:
        key = f"asset:{asset_id}"
        if self.tabs.focus(key):
            return
        try:
            asset = self.repository.get_asset(asset_id)
        except RepositoryError as exc:
            messagebox.showerror("Asset konnte nicht geöffnet werden", str(exc), parent=self)
            return
        tab = AssetTab(
            self.notebook,
            self.repository,
            asset_id,
            on_close=lambda: self.tabs.close(key),
            on_changed=self.refresh_overview,
            on_open_member=self.open_member,
            on_issue_asset=self.issue_asset,
            on_return_asset=self.return_asset,
            on_open_claim=self.open_claim,
        )
        self.tabs.add(
            key,
            tab,
            asset.label or asset.asset_id,
            image=self.icons.get("key-variant", 16) or self.icons.get("package-variant-closed", 16),
            icon_name="key-variant",
        )

    def _claim_changed(self, member_id: str) -> None:
        member_tab = self.tabs.tabs.get(f"member:{member_id}")
        if isinstance(member_tab, MemberTab) and member_tab.winfo_exists():
            member_tab.refresh()
        self.refresh_overview()

    def open_members(self) -> None:
        key = "members"
        if self.tabs.focus(key):
            return
        tab = MembersTab(
            self.notebook,
            self.repository.list_members(),
            self.findings,
            self.open_member,
            lambda: self.tabs.close(key),
        )
        self.tabs.add(
            key,
            tab,
            "Mitglieder",
            image=self.icons.get("account-group", 16) or self.icons.get("account-multiple", 16),
            icon_name="account-group",
        )

    def open_assets(self) -> None:
        key = "assets"
        if self.tabs.focus(key):
            return
        tab = AssetsTab(
            self.notebook,
            self.repository.list_assets(),
            self._member_reference_label,
            self.new_asset,
            self.open_asset,
            self.edit_asset,
            self.issue_asset,
            self.return_asset,
            lambda: self.tabs.close(key),
        )
        self.tabs.add(
            key,
            tab,
            "Inventar",
            image=self.icons.get("key-variant", 16) or self.icons.get("package-variant-closed", 16),
            icon_name="key-variant",
        )

    def open_sepa_export(self) -> None:
        SepaExportDialog(self, self.repository)

    def new_member(self) -> None:
        if self._refuse_read_only("Das Anlegen eines Mitglieds"):
            return
        NewMemberDialog(self, self.repository, self._member_created)

    def new_asset(self) -> None:
        if self._refuse_read_only("Das Anlegen eines Assets"):
            return
        NewAssetDialog(self, self.repository, self._asset_created)

    def _member_created(self, member: Member) -> None:
        self.refresh_overview()
        self.open_member(member.member_id)
        self.status_var.set(f"Mitgliederakte für {member.display_name} angelegt.")

    def _asset_created(self, asset: Asset) -> None:
        self.refresh_overview()
        self.open_assets()
        self.status_var.set(f"Asset angelegt: {asset.label}")

    def issue_asset(self, asset_id: str, preselected_member_id: str = "") -> None:
        try:
            IssueAssetDialog(
                self,
                self.repository,
                asset_id,
                self._asset_changed,
                preselected_member_id=preselected_member_id,
            )
        except (RepositoryError, tk.TclError) as exc:
            messagebox.showerror("Asset konnte nicht ausgegeben werden", str(exc), parent=self)

    def edit_asset(self, asset_id: str) -> None:
        try:
            EditAssetDialog(
                self,
                self.repository,
                asset_id,
                self._asset_changed,
            )
        except (RepositoryError, tk.TclError) as exc:
            messagebox.showerror("Asset konnte nicht bearbeitet werden", str(exc), parent=self)

    def return_asset(self, asset_id: str) -> None:
        try:
            asset = self.repository.get_asset(asset_id)
        except RepositoryError as exc:
            messagebox.showerror("Asset konnte nicht geladen werden", str(exc), parent=self)
            return
        if not messagebox.askyesno(
            "Asset zurücknehmen",
            f"{asset.label}\n\nDieses Asset wirklich als zurückgegeben markieren?",
            parent=self,
        ):
            return
        try:
            asset = self.repository.return_asset(asset_id)
        except RepositoryError as exc:
            messagebox.showerror("Rückgabe fehlgeschlagen", str(exc), parent=self)
            return
        self._asset_changed(asset)

    def _asset_changed(self, asset: Asset) -> None:
        self.refresh_overview()
        self.status_var.set(f"Asset aktualisiert: {asset.label}")

    def open_housekeeper(self) -> None:
        key = "housekeeper"
        if self.tabs.focus(key):
            return
        tab = HousekeeperTab(
            self.notebook,
            self.findings,
            self._open_housekeeper_target,
            self.run_housekeeper,
            self.delete_housekeeper_task,
            lambda: self.tabs.close(key),
        )
        self.tabs.add(
            key,
            tab,
            "Hausmeister",
            image=self.icons.get("broom", 16),
            icon_name="broom",
        )

    def run_housekeeper(self) -> list[HousekeeperFinding]:
        if self._refuse_read_only("Ein Hausmeisterlauf"):
            return self.findings
        self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).run()
        self.refresh_overview(run_housekeeper=False)
        self.status_var.set(f"Hausmeisterlauf beendet: {len(self.findings)} Vorgänge.")
        return self.findings

    def run_housekeeper_for_member(
        self, member_id: str, retroactive_claims: bool = False
    ) -> list[HousekeeperFinding]:
        if self._refuse_read_only("Ein Hausmeisterlauf"):
            return self.findings
        self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).run(
            member_id=member_id, retroactive_claims=retroactive_claims or None
        )
        self.refresh_overview(run_housekeeper=False)
        self.status_var.set(f"Hausmeisterlauf für Mitglied beendet: {len(self.findings)} Vorgänge insgesamt.")
        return self.findings

    def delete_housekeeper_task(self, key: str) -> list[HousekeeperFinding]:
        if self._refuse_read_only("Das Löschen eines Hausmeister-Tasks"):
            return self.findings
        self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).delete_task(key)
        self.refresh_overview(run_housekeeper=False)
        self.status_var.set("Hausmeister-Task gelöscht.")
        return self.findings

    def refresh_overview(self, *, run_housekeeper: bool = True) -> None:
        if run_housekeeper and not self.repository.read_only:
            self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).run()
        self.dashboard.update_data(self.repository.member_count(), self.findings)
        members_tab = self.tabs.tabs.get("members")
        if isinstance(members_tab, MembersTab) and members_tab.winfo_exists():
            members_tab.refresh(self.repository.list_members(), self.findings)
        assets_tab = self.tabs.tabs.get("assets")
        if isinstance(assets_tab, AssetsTab) and assets_tab.winfo_exists():
            assets_tab.refresh(self.repository.list_assets())
        for key, widget in self.tabs.tabs.items():
            if key.startswith("member:") and isinstance(widget, MemberTab) and widget.winfo_exists():
                widget.refresh()
            if key.startswith("asset:") and isinstance(widget, AssetTab) and widget.winfo_exists():
                widget.refresh()
        if self.housekeeper_button and self.housekeeper_button.winfo_exists():
            self.housekeeper_button.configure(text=f"Hausmeister ({len(self.findings)})")

    def toggle_theme(self) -> None:
        self.config.theme_mode = "light" if self.config.theme_mode == "dark" else "dark"
        self.config.save()
        load_theme(self.master, self.config.theme_mode)
        self._refresh_ribbon_icons()
        self.status_var.set(f"Theme: {self.config.theme_mode}.")

    def open_options(self) -> None:
        OptionsDialog(self, self.config, self.repository, self._options_saved)

    def _options_saved(self, store_changed: bool) -> None:
        load_theme(self.master, self.config.theme_mode)
        self._refresh_ribbon_icons()
        if store_changed:
            self.status_var.set("Mitglieder-Store geändert. Die Änderung wird beim Neustart aktiv.")
            messagebox.showinfo(
                "Neustart erforderlich",
                "Der neue Mitglieder-Store wird beim nächsten Programmstart geöffnet.",
                parent=self,
            )
        else:
            self.status_var.set("Optionen gespeichert.")

    def _refresh_ribbon_icons(self) -> None:
        if self.ribbon and self.ribbon.winfo_exists():
            self.ribbon.destroy()
        self.icons = IconStore(self)
        self._build_ribbon()
        self.tabs.refresh_icons(self.icons)

    def _show_validation_warning(self) -> None:
        display_errors = [
            item for item in self.validation_errors if "Hash fehlt oder stimmt nicht" not in item
        ]
        if not display_errors:
            return
        messagebox.showwarning(
            "Datenprüfung",
            "Der Store enthält ungültige Akten:\n\n" + "\n".join(display_errors[:12]),
            parent=self,
        )

    def _open_housekeeper_target(self, finding: HousekeeperFinding) -> None:
        if finding.target_type == "asset" and finding.asset_id:
            self.open_asset(finding.asset_id)
            return
        if finding.member_id:
            self.open_member(finding.member_id)

    def _member_reference_label(self, member_id: str) -> str:
        if not member_id:
            return "—"
        try:
            member = self.repository.get_member(member_id)
        except RepositoryError:
            return member_id
        prefix = member.member_number or member.member_id
        return f"{prefix} · {member.display_name or member.member_id}"
