"""GTK 4 + libadwaita implementation of the Edubuntu Installer UI."""

from __future__ import annotations

import threading
from collections.abc import Callable

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, GLib, Gtk  # noqa: E402

from .base import InstallerUI, ChecklistItem, ComboItem  # noqa: E402
from i18n import _  # noqa: E402

ICON_PATH = "/usr/share/icons/hicolor/scalable/apps/edubuntu-installer.svg"


class GtkUI(InstallerUI):

    def __init__(self) -> None:
        self._app: Adw.Application | None = None

    def init(self) -> None:
        self._app = Adw.Application(application_id="org.edubuntu.Installer")
        self._app.register()

    def quit(self) -> None:
        pass

    def _transient(self) -> Gtk.Window | None:
        if self._app:
            return self._app.get_active_window()
        return None

    def _make_message_dialog(
        self,
        title: str,
        text: str,
        msg_type: str = "info",
        buttons: list[tuple[str, int]] | None = None,
    ) -> Adw.MessageDialog:
        dlg = Adw.MessageDialog(
            heading=title,
            body=text,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        if buttons is None:
            dlg.add_response("ok", _("OK"))
            dlg.set_default_response("ok")
        else:
            for label, resp_id in buttons:
                dlg.add_response(str(resp_id), label)
        return dlg

    def _run_adw_dialog(self, dlg: Adw.MessageDialog) -> str:
        loop = GLib.MainLoop()
        result = {"response": "cancel"}

        def on_response(_dlg, resp):
            result["response"] = resp
            loop.quit()

        dlg.connect("response", on_response)
        if self._app:
            dlg.set_application(self._app)
        dlg.present()
        loop.run()
        return result["response"]

    def show_info(self, title: str, text: str) -> None:
        dlg = self._make_message_dialog(title, text, "info")
        self._run_adw_dialog(dlg)

    def show_error(self, title: str, text: str) -> None:
        dlg = self._make_message_dialog(title, text, "error")
        self._run_adw_dialog(dlg)

    def show_warning(self, title: str, text: str) -> None:
        dlg = self._make_message_dialog(title, text, "warning")
        self._run_adw_dialog(dlg)

    def show_question(
        self,
        title: str,
        text: str,
        ok_label: str = "Yes",
        cancel_label: str = "No",
    ) -> bool:
        dlg = Adw.MessageDialog(
            heading=title,
            body=text,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")
        resp = self._run_adw_dialog(dlg)
        return resp == "ok"

    def show_entry(
        self,
        title: str,
        text: str,
        default: str = "",
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
    ) -> str | None:
        dlg = Adw.MessageDialog(
            heading=title,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        box.set_margin_start(12)
        box.set_margin_end(12)
        box.set_margin_top(12)
        box.set_margin_bottom(12)

        label = Gtk.Label(label=text, wrap=True, use_markup=True)
        label.set_xalign(0)
        box.append(label)

        entry = Gtk.Entry(text=default)
        entry.set_activates_default(True)
        box.append(entry)

        dlg.set_extra_child(box)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")

        resp = self._run_adw_dialog(dlg)
        if resp == "ok":
            return entry.get_text()
        return None

    def show_combo_dialog(
        self,
        title: str,
        text: str,
        combos: list[ComboItem],
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
    ) -> list[str] | None:
        dlg = Adw.MessageDialog(
            heading=title,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        box.set_margin_start(12)
        box.set_margin_end(12)
        box.set_margin_top(12)
        box.set_margin_bottom(12)

        label = Gtk.Label(label=text, wrap=True, use_markup=True)
        label.set_xalign(0)
        box.append(label)

        dropdowns: list[Gtk.DropDown] = []
        for combo in combos:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
            lbl = Gtk.Label(label="<b>{}</b>".format(combo.label), use_markup=True)
            lbl.set_xalign(0)
            lbl.set_hexpand(True)
            row.append(lbl)

            string_list = Gtk.StringList.new(combo.options)
            dd = Gtk.DropDown(model=string_list)
            try:
                idx = combo.options.index(combo.default)
            except ValueError:
                idx = 0
            dd.set_selected(idx)
            row.append(dd)
            box.append(row)
            dropdowns.append(dd)

        dlg.set_extra_child(box)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")

        resp = self._run_adw_dialog(dlg)
        if resp == "ok":
            results = []
            for dd, combo in zip(dropdowns, combos):
                idx = dd.get_selected()
                results.append(combo.options[idx])
            return results
        return None

    def show_checklist(
        self,
        title: str,
        text: str,
        items: list[ChecklistItem],
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 0,
        height: int = 0,
    ) -> list[str] | None:
        dlg = Adw.MessageDialog(
            heading=title,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        if width > 0:
            dlg.set_default_size(width, max(height, 400))

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box.set_margin_start(12)
        box.set_margin_end(12)
        box.set_margin_top(12)
        box.set_margin_bottom(12)

        info_label = Gtk.Label(label=text, wrap=True, use_markup=True)
        info_label.set_xalign(0)
        box.append(info_label)

        checks: list[tuple[str, Gtk.CheckButton]] = []
        for item in items:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            cb = Gtk.CheckButton(active=item.checked)
            lbl_text = "<b>{}</b>  \u2014  {}".format(item.label, item.description) if item.description else "<b>{}</b>".format(item.label)
            lbl = Gtk.Label(label=lbl_text, use_markup=True)
            lbl.set_xalign(0)
            lbl.set_wrap(True)
            row.append(cb)
            row.append(lbl)
            box.append(row)
            checks.append((item.key, cb))

        scroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
        scroll.set_min_content_width(max(width - 80, 850))
        scroll.set_min_content_height(max(height - 120, 200))
        scroll.set_child(box)

        dlg.set_extra_child(scroll)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")

        resp = self._run_adw_dialog(dlg)
        if resp == "ok":
            return [key for key, cb in checks if cb.get_active()]
        return None

    def show_radiolist(
        self,
        title: str,
        text: str,
        items: list[ChecklistItem],
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 0,
        height: int = 0,
    ) -> str | None:
        dlg = Adw.MessageDialog(
            heading=title,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        if width > 0:
            dlg.set_default_size(width, max(height, 400))

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box.set_margin_start(12)
        box.set_margin_end(12)
        box.set_margin_top(12)
        box.set_margin_bottom(12)

        info_label = Gtk.Label(label=text, wrap=True, use_markup=True)
        info_label.set_xalign(0)
        box.append(info_label)

        group: Gtk.CheckButton | None = None
        radios: list[tuple[str, Gtk.CheckButton]] = []
        for item in items:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            rb = Gtk.CheckButton(active=item.checked)
            if group is not None:
                rb.set_group(group)
            else:
                group = rb
            lbl_text = "<b>{}</b>  \u2014  {}".format(item.label, item.description) if item.description else "<b>{}</b>".format(item.label)
            lbl = Gtk.Label(label=lbl_text, use_markup=True)
            lbl.set_xalign(0)
            lbl.set_wrap(True)
            row.append(rb)
            row.append(lbl)
            box.append(row)
            radios.append((item.key, rb))

        scroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
        scroll.set_min_content_width(max(width - 80, 850))
        scroll.set_min_content_height(max(height - 120, 200))
        scroll.set_child(box)

        dlg.set_extra_child(scroll)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")

        resp = self._run_adw_dialog(dlg)
        if resp == "ok":
            for key, rb in radios:
                if rb.get_active():
                    return key
        return None

    def show_installer_tabs(
        self,
        title: str,
        pkg_text: str,
        pkg_items: list[ChecklistItem],
        global_setup_text: str,
        global_setup_items: list[ChecklistItem],
        per_user_setup_text: str,
        usernames: list[str],
        user_default_fn,
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 0,
        height: int = 0,
    ) -> tuple[list[str], str, str, str] | None:
        dlg = Adw.MessageDialog(
            heading=title,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        if width > 0:
            dlg.set_default_size(width, max(height, 400))

        notebook = Gtk.Notebook()

        pkg_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        pkg_box.set_margin_start(12)
        pkg_box.set_margin_end(12)
        pkg_box.set_margin_top(12)
        pkg_box.set_margin_bottom(12)

        pkg_label = Gtk.Label(label=pkg_text, wrap=True, use_markup=True)
        pkg_label.set_xalign(0)
        pkg_box.append(pkg_label)

        pkg_checks: list[tuple[str, Gtk.CheckButton]] = []
        for item in pkg_items:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            cb = Gtk.CheckButton(active=item.checked)
            lbl_text = "<b>{}</b>  \u2014  {}".format(
                item.label, item.description,
            ) if item.description else "<b>{}</b>".format(item.label)
            lbl = Gtk.Label(label=lbl_text, use_markup=True)
            lbl.set_xalign(0)
            lbl.set_wrap(True)
            row.append(cb)
            row.append(lbl)
            pkg_box.append(row)
            pkg_checks.append((item.key, cb))

        pkg_scroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
        pkg_scroll.set_min_content_width(max(width - 80, 850))
        pkg_scroll.set_min_content_height(max(height - 200, 200))
        pkg_scroll.set_child(pkg_box)
        notebook.append_page(pkg_scroll, Gtk.Label(label=_("Packages")))

        setup_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        setup_box.set_margin_start(12)
        setup_box.set_margin_end(12)
        setup_box.set_margin_top(12)
        setup_box.set_margin_bottom(12)

        g_frame = Gtk.Frame(label=global_setup_text)
        g_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        g_inner.set_margin_start(8)
        g_inner.set_margin_end(8)
        g_inner.set_margin_top(4)
        g_inner.set_margin_bottom(8)

        g_group: Gtk.CheckButton | None = None
        g_radios: list[tuple[str, Gtk.CheckButton]] = []
        for item in global_setup_items:
            rb = Gtk.CheckButton(label=item.label, active=item.checked)
            if g_group is not None:
                rb.set_group(g_group)
            else:
                g_group = rb
            g_inner.append(rb)
            g_radios.append((item.key, rb))

        g_frame.set_child(g_inner)
        setup_box.append(g_frame)

        u_frame = Gtk.Frame(label=per_user_setup_text)
        u_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        u_inner.set_margin_start(8)
        u_inner.set_margin_end(8)
        u_inner.set_margin_top(4)
        u_inner.set_margin_bottom(8)

        selected_user = [usernames[0] if usernames else ""]

        if usernames:
            combo_row = Gtk.Box(
                orientation=Gtk.Orientation.HORIZONTAL, spacing=8,
            )
            combo_lbl = Gtk.Label(label=_("User:"))
            combo_row.append(combo_lbl)
            combo = Gtk.ComboBoxText()
            for u in usernames:
                combo.append_text(u)
            combo.set_active(0)
            combo_row.append(combo)
            u_inner.append(combo_row)

            u_group: Gtk.CheckButton | None = None
            u_radios: list[tuple[str, Gtk.CheckButton]] = []
            for item in global_setup_items:
                rb = Gtk.CheckButton(label=item.label)
                if u_group is not None:
                    rb.set_group(u_group)
                else:
                    u_group = rb
                u_inner.append(rb)
                u_radios.append((item.key, rb))

            def _set_user_radios(username: str) -> None:
                current = user_default_fn(username)
                for key, rb in u_radios:
                    rb.set_active(key == current)

            _set_user_radios(usernames[0])

            def _on_user_changed(cb):
                uname = cb.get_active_text()
                if uname:
                    selected_user[0] = uname
                    _set_user_radios(uname)
                    _check_changes()

            combo.connect("changed", _on_user_changed)
        else:
            u_radios = []
            no_users = Gtk.Label(label=_("No non-administrator users found."))
            no_users.set_xalign(0)
            u_inner.append(no_users)

        u_frame.set_child(u_inner)
        setup_box.append(u_frame)

        setup_scroll = Gtk.ScrolledWindow(
            hscrollbar_policy=Gtk.PolicyType.NEVER,
        )
        setup_scroll.set_min_content_width(max(width - 80, 850))
        setup_scroll.set_min_content_height(max(height - 200, 200))
        setup_scroll.set_child(setup_box)
        notebook.append_page(setup_scroll, Gtk.Label(label=_("Default Setup")))

        dlg.set_extra_child(notebook)
        dlg.add_response("cancel", cancel_label)
        dlg.add_response("ok", ok_label)
        dlg.set_default_response("ok")
        dlg.set_close_response("cancel")
        dlg.set_response_enabled("ok", False)

        initial_pkgs = {item.key for item in pkg_items if item.checked}
        initial_g_choice = ""
        for item in global_setup_items:
            if item.checked:
                initial_g_choice = item.key
                break
        initial_u_choices: dict[str, str] = {
            u: user_default_fn(u) for u in usernames
        } if usernames else {}

        def _check_changes() -> None:
            current_pkgs = {key for key, cb in pkg_checks if cb.get_active()}
            changed = current_pkgs != initial_pkgs
            if not changed:
                for key, rb in g_radios:
                    if rb.get_active():
                        changed = key != initial_g_choice
                        break
            if not changed and u_radios:
                uname = selected_user[0]
                for key, rb in u_radios:
                    if rb.get_active():
                        changed = key != initial_u_choices.get(uname, "")
                        break
            dlg.set_response_enabled("ok", changed)

        for _key, cb in pkg_checks:
            cb.connect("toggled", lambda _w: _check_changes())
        for _key, rb in g_radios:
            rb.connect("toggled", lambda _w: _check_changes())
        for _key, rb in u_radios:
            rb.connect("toggled", lambda _w: _check_changes())

        resp = self._run_adw_dialog(dlg)
        if resp == "ok":
            pkgs = [key for key, cb in pkg_checks if cb.get_active()]
            g_choice = ""
            for key, rb in g_radios:
                if rb.get_active():
                    g_choice = key
                    break
            u_choice = ""
            for key, rb in u_radios:
                if rb.get_active():
                    u_choice = key
                    break
            return (pkgs, g_choice, selected_user[0], u_choice)
        return None

    def show_progress(
        self,
        title: str,
        text: str,
        callback: Callable[
            [Callable[[str], None], Callable[[float], None]], None
        ],
    ) -> bool:
        import queue as _queue

        dlg = Adw.MessageDialog(
            heading=title,
            body=text,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)

        outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        outer.set_margin_start(12)
        outer.set_margin_end(12)
        outer.set_margin_top(12)
        outer.set_margin_bottom(12)

        progress = Gtk.ProgressBar()
        progress.set_show_text(False)
        outer.append(progress)

        expander = Gtk.Expander(label=_("Show Details"))

        text_view = Gtk.TextView()
        text_view.set_editable(False)
        text_view.set_cursor_visible(False)
        text_view.set_monospace(True)
        text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)

        scroll = Gtk.ScrolledWindow()
        scroll.set_min_content_width(500)
        scroll.set_min_content_height(200)
        scroll.set_child(text_view)

        expander.set_child(scroll)
        outer.append(expander)

        dlg.set_extra_child(outer)

        # Tagged message queue: ("text", str) | ("progress", float)
        msg_q: _queue.Queue[tuple[str, str | float]] = _queue.Queue()
        buf = text_view.get_buffer()
        success = {"value": False}
        pulsing = [True]
        loop = GLib.MainLoop()

        def _on_output(line: str) -> None:
            msg_q.put(("text", line))

        def _on_progress(fraction: float) -> None:
            msg_q.put(("progress", fraction))

        def _poll() -> bool:
            text_changed = False
            while True:
                try:
                    tag, value = msg_q.get_nowait()
                except _queue.Empty:
                    break
                if tag == "text":
                    end_iter = buf.get_end_iter()
                    buf.insert(end_iter, value)  # type: ignore[arg-type]
                    text_changed = True
                elif tag == "progress":
                    if pulsing[0]:
                        GLib.source_remove(pulse_id[0])
                        pulsing[0] = False
                    progress.set_fraction(value)  # type: ignore[arg-type]
            if text_changed:
                end_iter = buf.get_end_iter()
                text_view.scroll_to_iter(end_iter, 0, False, 0, 0)
            return True  # keep timer alive

        def _pulse() -> bool:
            progress.pulse()
            return True

        poll_id = GLib.timeout_add(100, _poll)
        pulse_id = [GLib.timeout_add(100, _pulse)]

        def worker():
            try:
                callback(_on_output, _on_progress)
                success["value"] = True
            except Exception:
                success["value"] = False
            finally:
                GLib.idle_add(_finish)

        def _finish():
            GLib.source_remove(poll_id)
            if pulsing[0]:
                GLib.source_remove(pulse_id[0])
            _poll()  # final drain
            dlg.close()
            loop.quit()
            return False

        threading.Thread(target=worker, daemon=True).start()
        if self._app:
            dlg.set_application(self._app)
        dlg.present()
        loop.run()
        return success["value"]
