"""Qt 6 (PyQt6) implementation of the Ubuntu Studio Installer UI."""

from __future__ import annotations

import threading
from collections.abc import Callable

from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QFontMetrics, QIcon, QTextCursor
from PyQt6.QtWidgets import (
    QApplication,
    QButtonGroup,
    QCheckBox,
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QFrame,
    QHBoxLayout,
    QInputDialog,
    QLabel,
    QMessageBox,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QScrollArea,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

from .base import AudioConfigUI, ChecklistItem, ComboItem
from i18n import _

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

_MAX_DLG_WIDTH = 950


def _item_text(label: str, description: str) -> str:
    """Format a checklist / radio-list item, omitting the dash when
    *description* is empty."""
    if description:
        return f"{label}  \u2014  {description}"
    return label


def _msgbox(icon: QMessageBox.Icon, title: str, text: str) -> QMessageBox:
    """Create a QMessageBox with window icon and word-wrapped text."""
    box = QMessageBox(icon, title, text)
    box.setWindowIcon(QIcon(ICON_PATH))
    for lbl in box.findChildren(QLabel):
        lbl.setWordWrap(True)
        lbl.setMaximumWidth(_MAX_DLG_WIDTH - 80)
    return box


def _wrapping_row(button: QCheckBox | QRadioButton, text: str) -> QWidget:
    """Pair *button* (with no text) and a word-wrapping QLabel in a row.

    Clicking the label toggles the button so the whole row is clickable."""
    label = QLabel(text)
    label.setWordWrap(True)
    label.mousePressEvent = lambda _ev: button.toggle()
    row = QWidget()
    h = QHBoxLayout(row)
    h.setContentsMargins(4, 3, 4, 3)
    h.addWidget(button)
    h.addWidget(label, 1)
    return row


def _auto_size(dlg: QDialog, width: int = 0, height: int = 0) -> None:
    """Size *dlg* to fit its content."""
    if width > 0:
        screen = QApplication.primaryScreen()
        max_h = int(screen.availableGeometry().height() * 0.8) if screen else 800
        dlg.resize(width, 0)
        dlg.adjustSize()
        h = height if height > 0 else min(dlg.sizeHint().height(), max_h)
        dlg.resize(width, h)
        return

    dlg.adjustSize()
    hint = dlg.sizeHint()
    w = min(hint.width(), _MAX_DLG_WIDTH)
    screen = QApplication.primaryScreen()
    max_h = int(screen.availableGeometry().height() * 0.8) if screen else 800
    h = min(hint.height(), max_h)
    dlg.resize(w, h)


def _items_width(items: list[ChecklistItem]) -> int:
    """Return the pixel width for the widest item text."""
    fm = QFontMetrics(QApplication.font())
    widest = max(
        fm.horizontalAdvance(_item_text(it.label, it.description))
        for it in items
    )
    return min(widest + 60, _MAX_DLG_WIDTH)


class QtUI(AudioConfigUI):
    """Concrete UI using PyQt6."""

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

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

    def init(self) -> None:
        if QApplication.instance() is None:
            self._app = QApplication([])
            self._app.setApplicationName(_("Ubuntu Studio Installer"))
            self._app.setWindowIcon(QIcon(ICON_PATH))
        else:
            self._app = QApplication.instance()

    def quit(self) -> None:
        pass  # no explicit quit — dialogs are modal

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

    def show_info(self, title: str, text: str) -> None:
        _msgbox(QMessageBox.Icon.Information, title, text).exec()

    def show_error(self, title: str, text: str) -> None:
        _msgbox(QMessageBox.Icon.Critical, title, text).exec()

    def show_warning(self, title: str, text: str) -> None:
        _msgbox(QMessageBox.Icon.Warning, title, text).exec()

    def show_question(
        self,
        title: str,
        text: str,
        ok_label: str = "Yes",
        cancel_label: str = "No",
    ) -> bool:
        box = _msgbox(QMessageBox.Icon.Question, title, text)
        btn_ok = box.addButton(ok_label, QMessageBox.ButtonRole.AcceptRole)
        box.addButton(cancel_label, QMessageBox.ButtonRole.RejectRole)
        box.exec()
        return box.clickedButton() == btn_ok

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

    def show_entry(
        self,
        title: str,
        text: str,
        default: str = "",
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
    ) -> str | None:
        value, ok = QInputDialog.getText(None, title, text, text=default)
        if ok:
            return value
        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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        layout = QVBoxLayout(dlg)
        layout.setSpacing(10)
        layout.setContentsMargins(12, 12, 12, 12)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        combo_widgets: list[QComboBox] = []
        for combo in combos:
            lbl = QLabel(f"<b>{combo.label}</b>")
            layout.addWidget(lbl)
            cb = QComboBox()
            cb.addItems(combo.options)
            if combo.default in combo.options:
                cb.setCurrentText(combo.default)
            layout.addWidget(cb)
            combo_widgets.append(cb)

        buttons = QDialogButtonBox()
        buttons.addButton(ok_label, QDialogButtonBox.ButtonRole.AcceptRole)
        buttons.addButton(cancel_label, QDialogButtonBox.ButtonRole.RejectRole)
        buttons.accepted.connect(dlg.accept)
        buttons.rejected.connect(dlg.reject)
        layout.addWidget(buttons)

        _auto_size(dlg)

        if dlg.exec() == QDialog.DialogCode.Accepted:
            return [cb.currentText() for cb in combo_widgets]
        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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        layout = QVBoxLayout(dlg)
        layout.setSpacing(10)
        layout.setContentsMargins(12, 12, 12, 12)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.Shape.NoFrame)
        container = QWidget()
        vbox = QVBoxLayout(container)
        vbox.setSpacing(2)
        vbox.setContentsMargins(4, 4, 4, 4)

        checks: list[tuple[str, QCheckBox]] = []
        for item in items:
            cb = QCheckBox()
            cb.setChecked(item.checked)

            if item.children:
                group_widget = QWidget()
                group_layout = QVBoxLayout(group_widget)
                group_layout.setSpacing(2)
                group_layout.setContentsMargins(0, 0, 0, 0)
                depends_children: list[QCheckBox] = []

                top_row = QWidget()
                top_layout = QHBoxLayout(top_row)
                top_layout.setSpacing(4)
                top_layout.setContentsMargins(0, 0, 0, 0)

                expand_btn = QPushButton("\u25b6")
                expand_btn.setCheckable(True)
                expand_btn.setFlat(True)
                expand_btn.setFixedWidth(22)
                expand_btn.setToolTip(_("Show package details"))

                child_container = QWidget()
                child_layout = QVBoxLayout(child_container)
                child_layout.setSpacing(2)
                child_layout.setContentsMargins(28, 0, 0, 0)
                child_container.setVisible(False)

                def _toggle_children(
                    checked: bool,
                    btn: QPushButton = expand_btn,
                    box: QWidget = child_container,
                ) -> None:
                    btn.setText("\u25bc" if checked else "\u25b6")
                    box.setVisible(checked)

                expand_btn.toggled.connect(_toggle_children)

                top_layout.addWidget(expand_btn)
                top_layout.addWidget(_wrapping_row(cb, _item_text(item.label, item.description)), 1)
                group_layout.addWidget(top_row)

                for child in item.children:
                    child_cb = QCheckBox()
                    child_cb.setChecked(child.checked)
                    if "::Depends::" in child.key:
                        depends_children.append(child_cb)
                        child_cb.toggled.connect(
                            lambda checked, parent_cb=cb: (
                                parent_cb.setChecked(False) if not checked else None
                            )
                        )
                    child_layout.addWidget(
                        _wrapping_row(
                            child_cb,
                            _item_text(child.label, child.description),
                        )
                    )
                    checks.append((child.key, child_cb))

                cb.toggled.connect(
                    lambda checked, dep_cbs=depends_children: (
                        [dep_cb.setChecked(True) for dep_cb in dep_cbs]
                        if checked
                        else None
                    )
                )

                group_layout.addWidget(child_container)
                vbox.addWidget(group_widget)
            else:
                vbox.addWidget(_wrapping_row(cb, _item_text(item.label, item.description)))

            checks.append((item.key, cb))

        scroll.setWidget(container)
        layout.addWidget(scroll)

        buttons = QDialogButtonBox()
        buttons.addButton(ok_label, QDialogButtonBox.ButtonRole.AcceptRole)
        buttons.addButton(cancel_label, QDialogButtonBox.ButtonRole.RejectRole)
        buttons.accepted.connect(dlg.accept)
        buttons.rejected.connect(dlg.reject)
        layout.addWidget(buttons)

        _auto_size(dlg, _items_width(items), height)

        if dlg.exec() == QDialog.DialogCode.Accepted:
            return [key for key, cb in checks if cb.isChecked()]
        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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        layout = QVBoxLayout(dlg)
        layout.setSpacing(10)
        layout.setContentsMargins(12, 12, 12, 12)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.Shape.NoFrame)
        container = QWidget()
        vbox = QVBoxLayout(container)
        vbox.setSpacing(2)
        vbox.setContentsMargins(4, 4, 4, 4)

        group = QButtonGroup(dlg)
        radios: list[tuple[str, QRadioButton]] = []
        for item in items:
            rb = QRadioButton()
            rb.setChecked(item.checked)
            group.addButton(rb)
            vbox.addWidget(_wrapping_row(rb, _item_text(item.label, item.description)))
            radios.append((item.key, rb))

        scroll.setWidget(container)
        layout.addWidget(scroll)

        buttons = QDialogButtonBox()
        buttons.addButton(ok_label, QDialogButtonBox.ButtonRole.AcceptRole)
        buttons.addButton(cancel_label, QDialogButtonBox.ButtonRole.RejectRole)
        buttons.accepted.connect(dlg.accept)
        buttons.rejected.connect(dlg.reject)
        layout.addWidget(buttons)

        _auto_size(dlg, _items_width(items), height)

        if dlg.exec() == QDialog.DialogCode.Accepted:
            for key, rb in radios:
                if rb.isChecked():
                    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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))
        dlg.setWindowModality(Qt.WindowModality.ApplicationModal)
        dlg.setMinimumWidth(500)

        layout = QVBoxLayout(dlg)
        layout.setSpacing(10)
        layout.setContentsMargins(12, 12, 12, 12)

        label = QLabel(text)
        label.setWordWrap(True)
        layout.addWidget(label)

        bar = QProgressBar()
        bar.setRange(0, 0)  # indeterminate / pulsating
        layout.addWidget(bar)

        toggle_btn = QPushButton(_("Show Details") + " \u25bc")
        toggle_btn.setFlat(True)
        toggle_btn.setStyleSheet("text-align: left; border: none; outline: none;")
        layout.addWidget(toggle_btn)

        output_edit = QTextEdit()
        output_edit.setReadOnly(True)
        output_edit.setFontFamily("monospace")
        output_edit.setMinimumHeight(200)
        output_edit.setVisible(False)
        layout.addWidget(output_edit)

        def _toggle_details():
            if output_edit.isVisible():
                output_edit.setVisible(False)
                toggle_btn.setText(_("Show Details") + " \u25bc")
                dlg.adjustSize()
            else:
                output_edit.setVisible(True)
                toggle_btn.setText(_("Hide Details") + " \u25b2")

        toggle_btn.clicked.connect(_toggle_details)

        # Tagged message queue: ("text", str) | ("progress", float)
        msg_q: _queue.Queue[tuple[str, str | float]] = _queue.Queue()
        success = {"value": False}

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

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

        def _poll():
            while True:
                try:
                    tag, value = msg_q.get_nowait()
                except _queue.Empty:
                    break
                if tag == "text":
                    output_edit.moveCursor(QTextCursor.MoveOperation.End)
                    output_edit.insertPlainText(value)  # type: ignore[arg-type]
                    output_edit.moveCursor(QTextCursor.MoveOperation.End)
                elif tag == "progress":
                    if bar.maximum() == 0:  # was indeterminate
                        bar.setRange(0, 1000)
                    bar.setValue(int(value * 1000))  # type: ignore[arg-type]

        poll_timer = QTimer()
        poll_timer.timeout.connect(_poll)
        poll_timer.start(100)

        def worker():
            try:
                callback(_on_output, _on_progress)
                success["value"] = True
            except Exception:
                success["value"] = False
            finally:
                QTimer.singleShot(0, dlg.close)

        threading.Thread(target=worker, daemon=True).start()
        dlg.exec()
        poll_timer.stop()
        _poll()  # final drain
        return success["value"]
