from __future__ import annotations

from collections.abc import Callable, Mapping
from typing import Any

from health_client import AccountSnapshot
from limit_warning import WINDOW_5H, WINDOW_7D

_CAP_MIN_PCT = 1
_CAP_MAX_PCT = 100

_RESPONSE_SAVE = -5
_RESPONSE_CANCEL = -6

_WINDOW_LABELS = {
    WINDOW_5H: "5h",
    WINDOW_7D: "7d",
}


def _load_gtk_modules() -> Any:
    try:
        import gi

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

        return Gtk
    except Exception:
        return _FallbackGtk


def _available_windows(snapshot: AccountSnapshot) -> tuple[str, ...]:
    windows: list[str] = []
    if snapshot.primary_used_pct is not None:
        windows.append(WINDOW_5H)
    if snapshot.secondary_used_pct is not None:
        windows.append(WINDOW_7D)
    return tuple(windows)


def _remaining_pct(snapshot: AccountSnapshot, window: str) -> int | None:
    if window == WINDOW_5H:
        used = snapshot.primary_used_pct
    else:
        used = snapshot.secondary_used_pct
    if used is None:
        return None
    return max(0, min(100, 100 - used))


def _clamp_cap_pct(value: int) -> int:
    return max(_CAP_MIN_PCT, min(_CAP_MAX_PCT, value))


def _read_cap_pct(text: str) -> int | None:
    stripped = text.strip()
    if not stripped:
        return None
    try:
        parsed = int(stripped)
    except ValueError:
        return None
    if parsed == 0:
        return None
    return _clamp_cap_pct(parsed)


class _FallbackWidget:
    def __init__(self, text: str = "") -> None:
        self.text = text
        self.label = text
        self.visible = True
        self.sensitive = True
        self.children: list[Any] = []

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

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

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

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

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

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

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


class _FallbackLabel(_FallbackWidget):
    pass


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


class _FallbackEntry(_FallbackWidget):
    pass


class _FallbackComboBoxText(_FallbackWidget):
    def __init__(self) -> None:
        super().__init__(text="")
        self._ids: list[str] = []
        self._labels: list[str] = []
        self._active_id: str | None = None

    def append(self, id: str, text: str) -> None:
        self._ids.append(id)
        self._labels.append(text)

    def set_active_id(self, id: str) -> None:
        if id in self._ids:
            self._active_id = id

    def get_active_id(self) -> str | None:
        return self._active_id

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

    def emit_changed(self) -> None:
        callback = getattr(self, "_changed_callback", None)
        if callable(callback):
            callback(self)


class _FallbackDialog(_FallbackWidget):
    def __init__(self, title: str = "") -> None:
        super().__init__(text=title)
        self.title = title
        self.transient_for: Any | None = None
        self.modal = False
        self.destroyed = False
        self.buttons: list[tuple[str, int]] = []
        self._content = _FallbackBox()

    def set_title(self, title: str) -> None:
        self.title = title

    def set_transient_for(self, window: Any) -> None:
        self.transient_for = window

    def set_modal(self, modal: bool) -> None:
        self.modal = bool(modal)

    def add_button(self, label: str, response: int) -> None:
        self.buttons.append((label, response))

    def get_content_area(self) -> _FallbackBox:
        return self._content

    def run(self) -> int:
        return _RESPONSE_CANCEL

    def destroy(self) -> None:
        self.visible = False
        self.destroyed = True


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

    class ResponseType:
        OK = _RESPONSE_SAVE
        CANCEL = _RESPONSE_CANCEL

    Label = _FallbackLabel
    Box = _FallbackBox
    Entry = _FallbackEntry
    ComboBoxText = _FallbackComboBoxText
    Dialog = _FallbackDialog


class LimitsDialog:
    def __init__(
        self,
        slug: str,
        snapshot: AccountSnapshot,
        caps: Mapping[str, int],
        on_save: Callable[[str, int | None], None],
        transient_for: Any | None = None,
        gtk_module: Any | None = None,
    ) -> None:
        self._slug = slug
        self._snapshot = snapshot
        self._caps = caps
        self._on_save = on_save
        self._windows = _available_windows(snapshot)
        self.gtk = gtk_module if gtk_module is not None else _load_gtk_modules()
        self.saved: tuple[str, int | None] | None = None

        response_type = getattr(self.gtk, "ResponseType", None)
        self._response_save = getattr(response_type, "OK", _RESPONSE_SAVE)
        self._response_cancel = getattr(response_type, "CANCEL", _RESPONSE_CANCEL)

        self.dialog = self._new_dialog(self.gtk, f"Limits — {slug}", transient_for)
        if hasattr(self.dialog, "add_button"):
            self.dialog.add_button("Cancel", self._response_cancel)
            self.dialog.add_button("Save", self._response_save)

        self.window_combo = self._new_window_combo(self.gtk, self._windows)
        self.remaining_label = self._new_label(self.gtk, "")
        self.cap_entry = self._new_entry(self.gtk)

        self._build_content()
        self._sync_for_window(self.window_combo.get_active_id())
        if self.window_combo is not None and hasattr(self.window_combo, "connect"):
            self.window_combo.connect("changed", self._on_window_changed)

        save_sensitive = bool(self._windows)
        if hasattr(self.dialog, "set_response_sensitive"):
            try:
                self.dialog.set_response_sensitive(self._response_save, save_sensitive)
            except Exception:
                pass

    def open(self) -> None:
        dialog = self.dialog
        if hasattr(dialog, "show_all"):
            dialog.show_all()
        run = getattr(dialog, "run", None)
        if not callable(run):
            return
        response = run()
        if response == self._response_save:
            self._handle_save()
        if hasattr(dialog, "destroy"):
            dialog.destroy()

    def simulate_save(
        self,
        window: str,
        cap_text: str,
    ) -> None:
        self.window_combo.set_active_id(window)
        self._sync_for_window(window)
        self.cap_entry.set_text(cap_text)
        self._handle_save()

    def _handle_save(self) -> None:
        window = self.window_combo.get_active_id()
        if window is None:
            return
        cap_pct = _read_cap_pct(self.cap_entry.get_text())
        self.saved = (window, cap_pct)
        self._on_save(window, cap_pct)

    def _on_window_changed(self, _combo: Any) -> None:
        self._sync_for_window(self.window_combo.get_active_id())

    def _sync_for_window(self, window: str | None) -> None:
        if window is None:
            self.remaining_label.set_text("")
            self.cap_entry.set_text("")
            return
        remaining = _remaining_pct(self._snapshot, window)
        if remaining is not None:
            self.remaining_label.set_text(f"now: {remaining}% remaining")
        else:
            self.remaining_label.set_text("")
        existing = self._caps.get(window)
        self.cap_entry.set_text(str(existing) if existing is not None else "")

    def _build_content(self) -> None:
        orientation = getattr(self.gtk, "Orientation", None)
        horizontal = getattr(orientation, "HORIZONTAL", 0)
        content = None
        get_content_area = getattr(self.dialog, "get_content_area", None)
        if callable(get_content_area):
            content = get_content_area()
        if content is None:
            return

        window_row = self._new_box(self.gtk, horizontal, spacing=12)
        self._pack(window_row, self._new_label(self.gtk, "Window"))
        self._pack(window_row, self.window_combo)
        self._pack(content, window_row)

        cap_row = self._new_box(self.gtk, horizontal, spacing=12)
        self._pack(cap_row, self._new_label(self.gtk, "Stop when remaining falls below:"))
        self._pack(cap_row, self.cap_entry)
        self._pack(cap_row, self._new_label(self.gtk, "%"))
        self._pack(content, cap_row)

        self._pack(content, self.remaining_label)

    @staticmethod
    def _new_dialog(gtk: Any, title: str, transient_for: Any | None) -> Any:
        try:
            dialog = gtk.Dialog(title=title)
        except TypeError:
            dialog = gtk.Dialog(title)
        if transient_for is not None and hasattr(dialog, "set_transient_for"):
            try:
                dialog.set_transient_for(transient_for)
            except Exception:
                pass
        if hasattr(dialog, "set_modal"):
            dialog.set_modal(True)
        return dialog

    @staticmethod
    def _new_window_combo(gtk: Any, windows: tuple[str, ...]) -> Any:
        combo_cls = getattr(gtk, "ComboBoxText", _FallbackComboBoxText)
        combo = combo_cls()
        for window in windows:
            combo.append(window, _WINDOW_LABELS[window])
        if windows:
            combo.set_active_id(windows[0])
        return combo

    @staticmethod
    def _new_entry(gtk: Any) -> Any:
        return gtk.Entry()

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

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

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