from __future__ import annotations

import re
from collections.abc import Callable
from typing import Any

from ui.style import add_css_class
from ui.view_model import AccountVM, DashboardCallbacks, QuotaBarVM

_MENU_ITEMS: tuple[tuple[str, str], ...] = (
    ("Set as default", "set_default"),
    ("Reload", "reload"),
    ("Set limits…", "set_limits"),
    ("Re-authenticate…", "reauthenticate"),
    ("Rename…", "rename"),
    ("Remove…", "remove"),
)

_HEALTH_BADGE_LEVELS = {"ok": "ok", "unknown": "warn", "broken": "err"}
_BADGE_LEVEL_CLASSES = ("ok", "warn", "err")
_BAR_LEVEL_CLASSES = ("hot", "crit")


def _load_gtk_modules() -> tuple[Any, bool]:
    try:
        import gi

        gi.require_version("Gtk", "3.0")
        from gi.repository import Gtk

        return Gtk, True
    except Exception:
        return _FallbackGtk, False


class _FallbackWidget:
    def __init__(self, text: str = "") -> None:
        self.text = text
        self.label = text
        self.visible = True
        self.sensitive = True
        self.fraction = 0.0
        self.hexpand = False
        self.halign: int | None = None
        self.valign: int | None = None
        self.children: list[object] = []
        self.end_children: list[object] = []
        self.tooltip_text = ""

    def set_text(self, text: str) -> None:
        self.text = text
        self.label = text

    def get_text(self) -> str:
        return self.text

    def set_label(self, label: str) -> None:
        self.text = label
        self.label = label

    def set_visible(self, visible: bool) -> None:
        self.visible = visible

    def get_visible(self) -> bool:
        return self.visible

    def set_sensitive(self, sensitive: bool) -> None:
        self.sensitive = sensitive

    def set_tooltip_text(self, text: str) -> None:
        self.tooltip_text = text

    def set_fraction(self, fraction: float) -> None:
        self.fraction = max(0.0, min(1.0, float(fraction)))

    def set_hexpand(self, hexpand: bool) -> None:
        self.hexpand = bool(hexpand)

    def set_halign(self, halign: int) -> None:
        self.halign = halign

    def set_valign(self, valign: int) -> None:
        self.valign = valign

    def set_xalign(self, _xalign: float) -> None:
        return

    def add(self, child: object) -> None:
        self.children.append(child)

    def pack_start(self, child: object, *_args: object) -> None:
        self.children.append(child)

    def pack_end(self, child: object, *_args: object) -> None:
        self.children.append(child)
        self.end_children.insert(0, child)

    def remove(self, child: object) -> None:
        if child in self.children:
            self.children.remove(child)


class _FallbackLabel(_FallbackWidget):
    pass


class _FallbackButton(_FallbackWidget):
    def __init__(self, label: str = "") -> None:
        super().__init__(label)
        self._callback: Callable[..., Any] | None = None

    def connect(self, signal: str, callback: Callable[..., Any]) -> None:
        if signal == "clicked":
            self._callback = callback

    def emit(self, signal: str) -> None:
        if signal == "clicked" and self.sensitive and self._callback is not None:
            self._callback(self)


class _FallbackProgressBar(_FallbackWidget):
    def __init__(self) -> None:
        super().__init__(text="")
        self.show_text = False

    def set_show_text(self, show_text: bool) -> None:
        self.show_text = bool(show_text)


class _FallbackBox(_FallbackWidget):
    def __init__(self, orientation: int | None = None, spacing: int = 0) -> None:
        super().__init__(text="")
        self.orientation = orientation
        self.spacing = spacing


class _FallbackMenuItem(_FallbackWidget):
    def __init__(self, label: str = "") -> None:
        super().__init__(label)
        self._callback: Callable[..., Any] | None = None

    def connect(self, signal: str, callback: Callable[..., Any]) -> None:
        if signal == "activate":
            self._callback = callback

    def emit(self, signal: str) -> None:
        if signal == "activate" and self.sensitive and self.visible and self._callback is not None:
            self._callback(self)


class _FallbackMenu(_FallbackWidget):
    def __init__(self) -> None:
        super().__init__(text="")

    def append(self, child: object) -> None:
        self.children.append(child)

    def show_all(self) -> None:
        self.visible = True

    def popup_at_pointer(self, _event: object) -> None:
        return

    def popup(self, *_args: object) -> None:
        return


class _FallbackGtk:
    class Orientation:
        VERTICAL = 1
        HORIZONTAL = 2

    class Align:
        START = 1
        CENTER = 2
        END = 3

    Label = _FallbackLabel
    Box = _FallbackBox
    Button = _FallbackButton
    ProgressBar = _FallbackProgressBar
    Menu = _FallbackMenu
    MenuItem = _FallbackMenuItem


def _new_box(gtk: Any, orientation: int, spacing: int) -> Any:
    try:
        return gtk.Box(orientation=orientation, spacing=spacing)
    except TypeError:
        return gtk.Box(orientation, spacing)


def _new_label(gtk: Any, text: str) -> Any:
    try:
        return gtk.Label(label=text)
    except TypeError:
        return gtk.Label(text)


def _new_button(gtk: Any, label: str) -> Any:
    try:
        return gtk.Button(label=label)
    except TypeError:
        return gtk.Button(label)


def _pack(parent: Any, child: Any) -> None:
    if hasattr(parent, "pack_start"):
        parent.pack_start(child, False, False, 0)
    else:
        parent.add(child)


def _pack_end(parent: Any, child: Any) -> None:
    if hasattr(parent, "pack_end"):
        parent.pack_end(child, False, False, 0)
    else:
        parent.add(child)


def _set_compact(widget: Any, gtk: Any, *, align: str = "END") -> None:
    if hasattr(widget, "set_hexpand"):
        widget.set_hexpand(False)
    gtk_align = getattr(getattr(gtk, "Align", None), align, None)
    if gtk_align is not None and hasattr(widget, "set_halign"):
        widget.set_halign(gtk_align)
    center = getattr(getattr(gtk, "Align", None), "CENTER", None)
    if center is not None and hasattr(widget, "set_valign"):
        widget.set_valign(center)


def _set_css_class(widget: Any, name: str, present: bool) -> None:
    if present:
        add_css_class(widget, name)
        return
    try:
        widget.get_style_context().remove_class(name)
        return
    except Exception:
        pass
    classes = getattr(widget, "css_classes", None)
    if isinstance(classes, list) and name in classes:
        classes.remove(name)


class AccountCard:
    def __init__(
        self,
        vm: AccountVM,
        callbacks: DashboardCallbacks,
        gtk_module: Any | None = None,
    ) -> None:
        self._callbacks = callbacks
        self._vm = vm
        loaded, _ = _load_gtk_modules()
        self.gtk = gtk_module if gtk_module is not None else loaded
        self.menu_items: list[tuple[str, Callable[[], None]]] = []
        self._menu_action_widgets: dict[str, Any] = {}
        self.widget = self._build()
        self.update(vm)

    def _build(self) -> Any:
        gtk = self.gtk
        vertical = getattr(getattr(gtk, "Orientation", None), "VERTICAL", 0)
        horizontal = getattr(getattr(gtk, "Orientation", None), "HORIZONTAL", 0)
        container = _new_box(gtk, vertical, 6)
        add_css_class(container, "account-card")

        header = _new_box(gtk, horizontal, 8)
        self.header = header
        self.alias_label = _new_label(gtk, "")
        add_css_class(self.alias_label, "account-name")
        if hasattr(self.alias_label, "set_xalign"):
            self.alias_label.set_xalign(0.0)
        self.default_badge = _new_label(gtk, "DEFAULT")
        add_css_class(self.default_badge, "default-badge")
        self.health_badge = _new_label(gtk, "")
        add_css_class(self.health_badge, "health-badge")
        self.primary_button = _new_button(gtk, "")
        add_css_class(self.primary_button, "suggested-action")
        add_css_class(self.primary_button, "card-primary")
        _set_compact(self.primary_button, gtk)
        self.primary_button.connect("clicked", self._on_primary_clicked)
        self.lock_button = _new_button(gtk, "")
        add_css_class(self.lock_button, "card-lock")
        _set_compact(self.lock_button, gtk)
        self.lock_button.connect("clicked", self._on_lock_clicked)
        self.menu_button = self._build_menu(gtk, vertical)
        add_css_class(self.menu_button, "card-menu")
        _set_compact(self.menu_button, gtk)
        for child in (
            self.alias_label,
            self.default_badge,
            self.health_badge,
        ):
            _pack(header, child)
        _pack_end(header, self.menu_button)
        _pack_end(header, self.lock_button)
        _pack_end(header, self.primary_button)

        self.meta = _new_box(gtk, horizontal, 6)
        self.plan_label = _new_label(gtk, "")
        self.checked_label = _new_label(gtk, "")

        self._primary_widgets = self._build_quota_row(gtk, horizontal)
        self._secondary_widgets = self._build_quota_row(gtk, horizontal)
        (
            self.primary_row,
            self.primary_window_label,
            self.primary_bar,
            self.primary_value_label,
            self.primary_reset_label,
            self.primary_display_label,
        ) = self._primary_widgets
        (
            self.secondary_row,
            self.secondary_window_label,
            self.secondary_bar,
            self.secondary_value_label,
            self.secondary_reset_label,
            self.secondary_display_label,
        ) = self._secondary_widgets

        for row in (header, self.primary_row, self.secondary_row):
            _pack(container, row)
        for togglable in (
            self.default_badge,
            self.primary_button,
            self.primary_row,
            self.secondary_row,
            *self._menu_action_widgets.values(),
        ):
            if hasattr(togglable, "set_no_show_all"):
                togglable.set_no_show_all(True)
        return container

    @staticmethod
    def _build_quota_row(
        gtk: Any, horizontal: int
    ) -> tuple[Any, Any, Any, Any, Any, Any]:
        row = _new_box(gtk, horizontal, 10)
        add_css_class(row, "quota-row")
        window_label = _new_label(gtk, "")
        add_css_class(window_label, "quota-label")
        bar = gtk.ProgressBar()
        add_css_class(bar, "quota-bar")
        if hasattr(bar, "set_hexpand"):
            bar.set_hexpand(True)
        if hasattr(bar, "set_valign") and hasattr(gtk, "Align"):
            bar.set_valign(gtk.Align.CENTER)
        value_label = _new_label(gtk, "")
        reset_label = _new_label(gtk, "")
        display_label = _new_label(gtk, "")
        add_css_class(display_label, "quota-value")
        if hasattr(display_label, "set_xalign"):
            display_label.set_xalign(1.0)
        _set_compact(display_label, gtk)
        _pack(row, window_label)
        if hasattr(row, "pack_start"):
            row.pack_start(bar, True, True, 0)
        else:
            row.add(bar)
        _pack_end(row, display_label)
        return row, window_label, bar, value_label, reset_label, display_label

    def _build_menu(self, gtk: Any, vertical: int) -> Any:
        self.menu_items = [
            (label, self._make_dispatch(action)) for label, action in _MENU_ITEMS
        ]
        widget = self._build_popover_menu(gtk, vertical)
        if widget is None:
            widget = self._build_menu_shell(gtk)
        if widget is None:
            widget = _new_button(gtk, "⋮")
        return widget

    def _build_popover_menu(self, gtk: Any, vertical: int) -> Any | None:
        menu_button_cls = getattr(gtk, "MenuButton", None)
        popover_cls = getattr(gtk, "Popover", None)
        if menu_button_cls is None or popover_cls is None:
            return None
        try:
            button = menu_button_cls()
            button.set_label("⋮")
            popover = popover_cls()
            box = _new_box(gtk, vertical, 0)
            for label, action in _MENU_ITEMS:
                item = _new_button(gtk, label)
                if action == "remove":
                    add_css_class(item, "destructive-action")
                item.connect(
                    "clicked",
                    lambda _b, a=action, p=popover: self._menu_activate(a, p),
                )
                self._menu_action_widgets[action] = item
                _pack(box, item)
            popover.add(box)
            box.show_all()
            button.set_popover(popover)
            return button
        except Exception:
            self._menu_action_widgets = {}
            return None

    def _build_menu_shell(self, gtk: Any) -> Any | None:
        menu_cls = getattr(gtk, "Menu", None)
        item_cls = getattr(gtk, "MenuItem", None)
        if menu_cls is None or item_cls is None:
            return None
        try:
            menu = menu_cls()
            for label, action in _MENU_ITEMS:
                item = item_cls(label=label)
                if action == "remove":
                    add_css_class(item, "destructive-action")
                item.connect("activate", lambda _i, a=action: self._dispatch(a))
                self._menu_action_widgets[action] = item
                menu.append(item)
            menu.show_all()
            button = _new_button(gtk, "⋮")
            button.connect("clicked", lambda _b, m=menu: self._popup_menu(m))
            return button
        except Exception:
            self._menu_action_widgets = {}
            return None

    @staticmethod
    def _popup_menu(menu: Any) -> None:
        try:
            if hasattr(menu, "popup_at_pointer"):
                menu.popup_at_pointer(None)
            else:
                menu.popup(None, None, None, None, 0, 0)
        except Exception:
            pass

    def _menu_activate(self, action: str, popover: Any) -> None:
        try:
            popover.popdown()
        except Exception:
            pass
        self._dispatch(action)

    def _make_dispatch(self, action: str) -> Callable[[], None]:
        def dispatch() -> None:
            self._dispatch(action)

        return dispatch

    def _dispatch(self, action: str) -> None:
        vm = self._vm
        callbacks = self._callbacks
        if action == "set_default":
            callbacks.on_set_default(vm.tool, vm.slug)
        elif action == "reload":
            callbacks.on_reload(vm.tool, vm.slug)
        elif action in ("repair", "reauthenticate"):
            callbacks.on_repair(vm.tool, vm.slug)
        elif action == "rename":
            callbacks.on_rename(vm.tool, vm.slug)
        elif action == "set_limits":
            callbacks.on_set_limits(vm.tool, vm.slug)
        elif action == "remove":
            callbacks.on_remove(vm.tool, vm.slug)

    def _on_lock_clicked(self, _button: Any) -> None:
        self._callbacks.on_toggle_lock(self._vm.tool, self._vm.slug)

    def _on_primary_clicked(self, _button: Any) -> None:
        action = self._vm.primary_action
        if action is not None:
            self._dispatch(action)

    def update(self, vm: AccountVM) -> None:
        self._vm = vm
        self.alias_label.set_text(vm.alias)
        self.plan_label.set_text(vm.plan_text)
        self.checked_label.set_text(vm.checked_text)
        self.default_badge.set_visible(vm.is_default)
        self.health_badge.set_text(vm.health_text)
        self.health_badge.set_visible(vm.health != "ok")
        badge_level = _HEALTH_BADGE_LEVELS.get(vm.health, "warn")
        for level in _BADGE_LEVEL_CLASSES:
            _set_css_class(self.health_badge, level, level == badge_level)
        _set_css_class(self.widget, "attention", vm.attention)
        _set_css_class(self.widget, "locked", vm.locked)
        self.lock_button.set_label("🔒" if vm.locked else "🔓")
        if hasattr(self.lock_button, "set_tooltip_text"):
            self.lock_button.set_tooltip_text(
                "Unlock account for new sessions"
                if vm.locked
                else "Lock account against new sessions"
            )
        self.lock_button.set_sensitive("account_lock" not in vm.busy_actions)
        _set_css_class(self.lock_button, "locked", vm.locked)
        self._update_primary_button(vm)
        self._update_quota_row(self._primary_widgets, vm.primary)
        self._update_quota_row(self._secondary_widgets, vm.secondary)
        self._update_menu_actions(vm)

    def _update_primary_button(self, vm: AccountVM) -> None:
        # Set as default lives in the ⋮ menu; only urgent actions stay as a header button.
        action = vm.primary_action if vm.primary_action != "set_default" else None
        label = vm.primary_action_label if action is not None else ""
        self.primary_button.set_label(label)
        self.primary_button.set_visible(action is not None)
        busy_key = "reauthenticate" if action == "repair" else action
        self.primary_button.set_sensitive(
            action is not None and busy_key not in vm.busy_actions
        )

    def _update_menu_actions(self, vm: AccountVM) -> None:
        for action, item in self._menu_action_widgets.items():
            if action == "set_default":
                available = not vm.is_default
                item.set_visible(available)
                item.set_sensitive(
                    available
                    and not vm.locked
                    and "set_default" not in vm.busy_actions
                )
                continue
            item.set_visible(True)
            busy_key = "reauthenticate" if action == "repair" else action
            blocked_by_lock = vm.locked and action in {"reload", "reauthenticate"}
            item.set_sensitive(
                not blocked_by_lock and busy_key not in vm.busy_actions
            )

    @staticmethod
    def _update_quota_row(
        widgets: tuple[Any, Any, Any, Any, Any, Any], bar_vm: QuotaBarVM | None
    ) -> None:
        row, window_label, bar, value_label, reset_label, display_label = widgets
        if bar_vm is None:
            row.set_visible(False)
            return
        controls_show_all = hasattr(row, "set_no_show_all")
        if controls_show_all:
            row.set_no_show_all(False)
        try:
            if hasattr(row, "show_all"):
                row.show_all()
        finally:
            if controls_show_all:
                row.set_no_show_all(True)
        row.set_visible(True)
        window_label.set_text(bar_vm.window_label)
        bar.set_fraction(bar_vm.fraction)
        value_label.set_text(bar_vm.percent_text)
        reset_label.set_text(bar_vm.reset_text)
        reset_text = re.sub(r"(?<=[dhm])(?=\d)", " ", bar_vm.reset_text)
        parts = [bar_vm.percent_text]
        if reset_text:
            parts.append(reset_text)
        if bar_vm.cap_stop_at_pct is not None:
            parts.append(f"stop at {bar_vm.cap_stop_at_pct}% left")
        display_label.set_text(" · ".join(parts))
        for level in _BAR_LEVEL_CLASSES:
            _set_css_class(bar, level, bar_vm.level == level)
