"""Qt 6 (PyQt6) implementation of the Edubuntu 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,
    QGroupBox,
    QHBoxLayout,
    QInputDialog,
    QLabel,
    QMessageBox,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QScrollArea,
    QTabWidget,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

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

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

_MAX_DLG_WIDTH = 950


def _item_text(label: str, description: str) -> str:
    if description:
        return "{0}  \u2014  {1}".format(label, description)
    return label


def _msgbox(icon: QMessageBox.Icon, title: str, text: str) -> QMessageBox:
    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:
    label = QLabel(text)
    label.setWordWrap(True)
    label.mousePressEvent = lambda _ev: button.toggle()
    row = QWidget()
    h = QHBoxLayout(row)
    h.setContentsMargins(0, 0, 0, 0)
    h.addWidget(button)
    h.addWidget(label, 1)
    return row


def _auto_size(dlg: QDialog, width: int = 0, height: int = 0) -> None:
    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:
    fm = QFontMetrics(QApplication.font())
    widest = max(
        fm.horizontalAdvance(_item_text(it.label, it.description))
        for it in items
    )
    # ~60 px for checkbox/radio indicator + layout margins + scrollbar
    return min(widest + 60, _MAX_DLG_WIDTH)


class QtUI(InstallerUI):

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

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

    def quit(self) -> None:
        pass

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

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

    def show_warning(self, title: str, text: str) -> None:
        box = _msgbox(QMessageBox.Icon.Warning, title, text)
        box.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

    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

    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)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        combo_widgets: list[QComboBox] = []
        for combo in combos:
            lbl = QLabel("<b>{}</b>".format(combo.label))
            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

    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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        layout = QVBoxLayout(dlg)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        container = QWidget()
        vbox = QVBoxLayout(container)

        checks: list[tuple[str, QCheckBox]] = []
        for item in items:
            cb = QCheckBox()
            cb.setChecked(item.checked)
            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

    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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        layout = QVBoxLayout(dlg)
        info = QLabel(text)
        info.setWordWrap(True)
        layout.addWidget(info)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        container = QWidget()
        vbox = QVBoxLayout(container)

        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

    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=None,
        ok_label: str = "OK",
        cancel_label: str = "Cancel",
        width: int = 0,
        height: int = 0,
    ) -> tuple[list[str], str, str, str] | None:
        dlg = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))

        outer = QVBoxLayout(dlg)
        tabs = QTabWidget()

        pkg_page = QWidget()
        pkg_layout = QVBoxLayout(pkg_page)

        pkg_label = QLabel(pkg_text)
        pkg_label.setWordWrap(True)
        pkg_layout.addWidget(pkg_label)

        pkg_scroll = QScrollArea()
        pkg_scroll.setWidgetResizable(True)
        pkg_container = QWidget()
        pkg_vbox = QVBoxLayout(pkg_container)

        pkg_checks: list[tuple[str, QCheckBox]] = []
        for item in pkg_items:
            cb = QCheckBox()
            cb.setChecked(item.checked)
            pkg_vbox.addWidget(
                _wrapping_row(cb, _item_text(item.label, item.description)),
            )
            pkg_checks.append((item.key, cb))

        pkg_scroll.setWidget(pkg_container)
        pkg_layout.addWidget(pkg_scroll)
        tabs.addTab(pkg_page, _("Packages"))

        setup_page = QWidget()
        setup_layout = QVBoxLayout(setup_page)

        g_group_box = QGroupBox(global_setup_text)
        g_vbox = QVBoxLayout(g_group_box)
        g_btn_group = QButtonGroup(dlg)

        g_radios: list[tuple[str, QRadioButton]] = []
        for item in global_setup_items:
            rb = QRadioButton(item.label)
            rb.setChecked(item.checked)
            g_btn_group.addButton(rb)
            g_vbox.addWidget(rb)
            g_radios.append((item.key, rb))

        setup_layout.addWidget(g_group_box)

        u_group_box = QGroupBox(per_user_setup_text)
        u_vbox = QVBoxLayout(u_group_box)

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

        if usernames:
            header = QHBoxLayout()
            header.addWidget(QLabel(_("User:")))
            combo = QComboBox()
            combo.addItems(usernames)
            header.addWidget(combo, 1)
            u_vbox.addLayout(header)

            u_btn_group = QButtonGroup(dlg)
            u_radios: list[tuple[str, QRadioButton]] = []
            for item in global_setup_items:
                rb = QRadioButton(item.label)
                u_btn_group.addButton(rb)
                u_vbox.addWidget(rb)
                u_radios.append((item.key, rb))

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

            _set_user_radios(usernames[0])

            def _on_user_changed(uname: str) -> None:
                selected_user[0] = uname
                _set_user_radios(uname)
                _check_changes()

            combo.currentTextChanged.connect(_on_user_changed)
        else:
            u_radios = []
            u_vbox.addWidget(
                QLabel(_("No non-administrator users found.")),
            )

        setup_layout.addWidget(u_group_box)
        setup_layout.addStretch()
        tabs.addTab(setup_page, _("Default Setup"))

        outer.addWidget(tabs)

        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.isChecked()}
            changed = current_pkgs != initial_pkgs
            if not changed:
                for key, rb in g_radios:
                    if rb.isChecked():
                        changed = key != initial_g_choice
                        break
            if not changed and u_radios:
                uname = selected_user[0]
                for key, rb in u_radios:
                    if rb.isChecked():
                        changed = key != initial_u_choices.get(uname, "")
                        break
            btn_ok.setEnabled(changed)

        for _key, cb in pkg_checks:
            cb.stateChanged.connect(lambda _s: _check_changes())
        for _key, rb in g_radios:
            rb.toggled.connect(lambda _c: _check_changes())
        for _key, rb in u_radios:
            rb.toggled.connect(lambda _c: _check_changes())

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

        w = _items_width(pkg_items) if pkg_items else 800
        _auto_size(dlg, w, height)

        if dlg.exec() == QDialog.DialogCode.Accepted:
            pkgs = [key for key, cb in pkg_checks if cb.isChecked()]
            g_choice = ""
            for key, rb in g_radios:
                if rb.isChecked():
                    g_choice = key
                    break
            u_choice = ""
            for key, rb in u_radios:
                if rb.isChecked():
                    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 = QDialog()
        dlg.setWindowTitle(title)
        dlg.setWindowIcon(QIcon(ICON_PATH))
        dlg.setWindowModality(Qt.WindowModality.ApplicationModal)
        dlg.setMinimumWidth(500)

        layout = QVBoxLayout(dlg)

        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"]
