"""GTK 4 + libadwaita implementation of the Audio Config 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 AudioConfigUI, ChecklistItem, ComboItem  # noqa: E402
from i18n import _  # noqa: E402

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


class _DialogHelper:
    """Small mixin for running modal Gtk 4 dialogs synchronously."""

    @staticmethod
    def _run_dialog(dialog: Gtk.Window) -> int:
        """Block until *dialog* is closed. Returns the response id."""
        loop = GLib.MainLoop()
        result = {"response": Gtk.ResponseType.CANCEL}

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

        dialog.connect("response", on_response)
        dialog.present()
        loop.run()
        return result["response"]


class GtkUI(AudioConfigUI):
    """Concrete UI using GTK 4 / libadwaita."""

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

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

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

    def quit(self) -> None:
        pass

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    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:  # type: ignore[name-defined]
        dlg = Adw.MessageDialog(
            heading=title,
            body=text,
            transient_for=self._transient(),
            modal=True,
        )
        dlg.set_body_use_markup(True)
        dlg.set_default_size(520, -1)
        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:
        """Run an Adw.MessageDialog and return the response id string."""
        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"]

    # ------------------------------------------------------------------
    # Simple message dialogs
    # ------------------------------------------------------------------

    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"

    # ------------------------------------------------------------------
    # Entry dialog
    # ------------------------------------------------------------------

    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)

        box.set_size_request(520, -1)
        dlg.set_extra_child(box)
        dlg.set_default_size(560, -1)
        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

    # ------------------------------------------------------------------
    # Combo-box dialog
    # ------------------------------------------------------------------

    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=f"<b>{combo.label}</b>", 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)

        box.set_size_request(580, -1)
        dlg.set_extra_child(box)
        dlg.set_default_size(660, -1)
        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

    # ------------------------------------------------------------------
    # Checklist dialog
    # ------------------------------------------------------------------

    def show_checklist(
        self,
        title: str,
        text: str,
        items: list[ChecklistItem],
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 950,
        height: int = 350,
    ) -> 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=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:
            cb = Gtk.CheckButton(active=item.checked)

            if item.children:
                expander = Gtk.Expander(label=_("Show package details"))
                depends_children: list[Gtk.CheckButton] = []

                header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
                lbl = Gtk.Label(
                    label=f"<b>{item.label}</b>  —  {item.description}",
                    use_markup=True,
                )
                lbl.set_xalign(0)
                lbl.set_wrap(True)
                header.append(cb)
                header.append(lbl)
                expander.set_label_widget(header)

                child_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
                child_box.set_margin_start(24)
                child_box.set_margin_top(4)

                for child in item.children:
                    child_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
                    child_cb = Gtk.CheckButton(active=child.checked)
                    if "::Depends::" in child.key:
                        depends_children.append(child_cb)
                        child_cb.connect(
                            "toggled",
                            lambda btn, parent_cb=cb: (
                                parent_cb.set_active(False)
                                if not btn.get_active()
                                else None
                            ),
                        )
                    child_lbl = Gtk.Label(
                        label=f"<b>{child.label}</b>  —  {child.description}",
                        use_markup=True,
                    )
                    child_lbl.set_xalign(0)
                    child_lbl.set_wrap(True)
                    child_row.append(child_cb)
                    child_row.append(child_lbl)
                    child_box.append(child_row)
                    checks.append((child.key, child_cb))

                cb.connect(
                    "toggled",
                    lambda parent_btn, dep_cbs=depends_children: (
                        [dep_cb.set_active(True) for dep_cb in dep_cbs]
                        if parent_btn.get_active()
                        else None
                    ),
                )

                expander.set_child(child_box)
                box.append(expander)
            else:
                row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
                lbl = Gtk.Label(
                    label=f"<b>{item.label}</b>  —  {item.description}",
                    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, 580))
        scroll.set_min_content_height(max(height - 120, 380))
        scroll.set_propagate_natural_height(True)
        scroll.set_child(box)

        dlg.set_extra_child(scroll)
        dlg.set_default_size(max(width, 680), max(height, 600))
        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

    # ------------------------------------------------------------------
    # Radio-list dialog
    # ------------------------------------------------------------------

    def show_radiolist(
        self,
        title: str,
        text: str,
        items: list[ChecklistItem],
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 950,
        height: int = 350,
    ) -> 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=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 = Gtk.Label(label=f"<b>{item.label}</b>  —  {item.description}", 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, 580))
        scroll.set_min_content_height(max(height - 120, 380))
        scroll.set_propagate_natural_height(True)
        scroll.set_child(box)

        dlg.set_extra_child(scroll)
        dlg.set_default_size(max(width, 680), max(height, 600))
        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

    # ------------------------------------------------------------------
    # Progress dialog
    # ------------------------------------------------------------------

    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:
            """Called from worker thread."""
            msg_q.put(("text", line))

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

        def _poll() -> bool:
            """Drain the queue, update text and progress bar."""
            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"]
