import functools
import importlib
import os
import subprocess
import sys
import webbrowser
import zipfile
from pathlib import Path

from PySide6 import QtCore, QtGui, QtWidgets, QtSvg

from .. import core
from .background import BackgroundTaskRunner, Promise


def require_login(method):
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        # If we're already logged in just call the method.
        if self.username:
            method(self, *args, **kwargs)
            return

        # Try to log in, but note that the user may have cancelled.
        def after_login_attempt():
            if self.username:
                method(self, *args, **kwargs)
        self.toggle_login().then(after_login_attempt)
    return wrapper


class PackageManagerPanel(QtWidgets.QWidget):
    def __init__(self, core_app, parent=None):
        super().__init__(parent)
        self.username = None
        self.status_message = ""
        self.progress_message = ""

        self.create_widgets()
        self.create_layout()
        self.create_connections()

        self.core_app = core_app
        self.background_runner = BackgroundTaskRunner(core_app, self)

        hooks = core_app.hooks
        hooks.run_generator_in_background = (
            self.background_runner.run_generator_in_background)
        hooks.print_info = self.append_status_message
        hooks.prompt_yes_or_no = self.prompt_yes_or_no

        self.background_runner.started.connect(self.on_background_started)
        self.background_runner.finished.connect(self.on_background_finished)
        self.background_runner.progress_percentage.connect(
            self.set_progress_percentage)
        self.background_runner.progress_message.connect(
            self.set_progress_message)
        self.background_runner.append_status.connect(
            self.append_status_message)
        self.background_runner.error.connect(self.on_background_error)

        self.cancel_button.clicked.connect(
            self.background_runner.on_cancel_background_work)

        self.update_login_icon()
        self.refresh_package_list()

    # UI construction:
    # -----------------------------------------------------------------------

    def create_widgets(self):
        fixed_font = QtGui.QFontDatabase.systemFont(
            QtGui.QFontDatabase.SystemFont.FixedFont)
        default_fixed_font_size = fixed_font.pointSize()
        if default_fixed_font_size <= 0:
            default_fixed_font_size = self.font().pointSize()
        fixed_font.setPointSize(default_fixed_font_size + 1)

        # Top button row
        self.install_button = QtWidgets.QPushButton("Install")

        self.login_indicator_button = QtWidgets.QPushButton()
        self.login_indicator_button.setLayoutDirection(QtCore.Qt.RightToLeft)
        self.login_indicator_button.setIcon(_get_houdini_icon(
            "PRIMITIVES_packedagent",
            QtWidgets.QStyle.StandardPixmap.SP_DesktopIcon))
        self.login_indicator_button.setIconSize(QtCore.QSize(24, 24))

        self.refresh_button = QtWidgets.QToolButton()
        self.refresh_button.setToolTip("Refresh")
        self.refresh_button.setIcon(_safe_standard_icon(
            QtWidgets.QStyle.StandardPixmap.SP_BrowserReload))
        self.refresh_button.setIcon(_get_houdini_icon(
            "BUTTONS_reload",
            QtWidgets.QStyle.StandardPixmap.SP_BrowserReload))

        self.menu_button = QtWidgets.QToolButton()
        self.menu_button.setToolTip("Menu")
        self.menu_button.setText("☰")
        self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)

        self.main_menu = QtWidgets.QMenu(self)
        self.populate_main_menu()
        self.menu_button.setMenu(self.main_menu)

        # Package table
        self.table = QtWidgets.QTableWidget(0, 3, self)
        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.table.setSelectionMode(
            QtWidgets.QAbstractItemView.SingleSelection)
        self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.table.setContextMenuPolicy(
            QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
        self.table.verticalHeader().setVisible(False)
        self.table.setShowGrid(False)

        # Install an event filter to highlight entire rows on hover instead of
        # cells.
        self.table.setMouseTracking(True)
        self.table.viewport().installEventFilter(self)

        self.table.setHorizontalHeaderLabels(["Package", "Version", "Status"])
        header = self.table.horizontalHeader()
        header.setSectionResizeMode(
            0, QtWidgets.QHeaderView.ResizeMode.Stretch)
        header.setSectionResizeMode(
            1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
        header.setSectionResizeMode(
            2, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)

        # Always make the header non-bold, regardless of selected rows.
        header.setHighlightSections(False)

        # Progress bar
        self.progress_bar = QtWidgets.QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)

        # Status text area
        self.status_display = QtWidgets.QPlainTextEdit()
        self.status_display.setReadOnly(True)
        self.status_display.setMinimumHeight(35)
        self.status_display.setSizePolicy(
            QtWidgets.QSizePolicy.Preferred,
            QtWidgets.QSizePolicy.MinimumExpanding)
        self.status_display.setLineWrapMode(
            QtWidgets.QPlainTextEdit.LineWrapMode.WidgetWidth)
        self.status_display.setWordWrapMode(
            QtGui.QTextOption.WrapMode.WordWrap)
        self.status_display.setFont(fixed_font)

        self.cancel_button = QtWidgets.QPushButton("Cancel")
        self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
        self.progress_row = QtWidgets.QWidget()
        self.progress_row.setVisible(False)

    def eventFilter(self, obj, event):
        if obj is self.table.viewport():
            if event.type() == QtCore.QEvent.Type.MouseMove:
                index = self.table.indexAt(event.pos())
                # Highlight the whole row for the hovered cell, clearing the
                # highlight if it's over an empty area.
                if index.isValid():
                    self.table.selectRow(index.row())
                else:
                    self.table.clearSelection()
            elif event.type() == QtCore.QEvent.Type.Leave:
                # The mouse left the table so clear the highlight.
                self.table.clearSelection()

        return super().eventFilter(obj, event)

    def create_layout(self):
        progress_layout = QtWidgets.QHBoxLayout(self.progress_row)
        progress_layout.setContentsMargins(0, 0, 0, 0)
        progress_layout.setSpacing(4)
        progress_layout.addWidget(self.progress_bar, 1)
        progress_layout.addWidget(self.cancel_button)

        bottom_container = QtWidgets.QWidget()
        bottom_layout = QtWidgets.QVBoxLayout(bottom_container)
        bottom_layout.setContentsMargins(0, 0, 0, 0)
        bottom_layout.setSpacing(2)
        bottom_layout.addWidget(self.progress_row)
        bottom_layout.addWidget(self.status_display)

        self.progress_bar.setFixedHeight(self.cancel_button.sizeHint().height())

        self.splitter.addWidget(self.table)
        self.splitter.addWidget(bottom_container)
        self.splitter.setStretchFactor(0, 3)
        self.splitter.setStretchFactor(1, 1)

        top_row = QtWidgets.QHBoxLayout()
        top_row.setContentsMargins(0, 0, 0, 0)
        top_row.addWidget(self.install_button)
        top_row.addStretch(1)
        top_row.addWidget(self.login_indicator_button)
        top_row.addWidget(self.refresh_button)
        top_row.addWidget(self.menu_button)

        main_layout = QtWidgets.QVBoxLayout(self)
        main_layout.setContentsMargins(4, 4, 4, 4)
        main_layout.setSpacing(4)
        main_layout.addLayout(top_row)
        main_layout.addWidget(self.splitter)

        self.setLayout(main_layout)

        QtCore.QTimer.singleShot(0, self.initialize_splitter_sizes)

    def initialize_splitter_sizes(self):
        # The total height might be zero if we haven't laid out yet.
        total_height = self.splitter.size().height()
        if total_height <= 0:
            return

        bottom_height = min(
            70, total_height)
        top_height = max(0, total_height - bottom_height)
        self.splitter.setSizes([top_height, bottom_height])


    def create_connections(self):
        self.table.customContextMenuRequested.connect(
            self.show_table_context_menu)

        self.install_button.clicked.connect(self.select_package_to_install)
        self.refresh_button.clicked.connect(self.on_refresh_package_list)
        self.login_indicator_button.clicked.connect(self.toggle_login)

    def set_ui_busy(self, busy):
        for widget in (
                self.install_button,
                self.login_indicator_button,
                self.refresh_button,
                self.menu_button,
                self.table):
            widget.setEnabled(not busy)

    # Menus:
    # -----------------------------------------------------------------------

    def populate_main_menu(self):
        self.action_login_logout = self.main_menu.addAction("Log in / Log out")
        self.action_install = self.main_menu.addAction("Install")
        self.action_install_from_requirements = self.main_menu.addAction(
            "Install from requirements file")
        self.action_author_new = self.main_menu.addAction(
            "Author a new package")
        self.action_create_auth_key = self.main_menu.addAction(
            "Create an authorization key")
        self.action_uninstall_auth_key = self.main_menu.addAction(
            "Uninstall authorization key")
        self.action_upgrade_hpackage = self.main_menu.addAction(
            "Upgrade this panel")

        self.action_login_logout.triggered.connect(self.toggle_login)
        self.action_install.triggered.connect(self.select_package_to_install)
        self.action_install_from_requirements.triggered.connect(
            self.install_from_requirements)
        self.action_author_new.triggered.connect(self.author_new_package)
        self.action_create_auth_key.triggered.connect(self.create_auth_key)
        self.action_uninstall_auth_key.triggered.connect(
            self.uninstall_auth_key)
        self.action_upgrade_hpackage.triggered.connect(self.upgrade_hpackage)

    def show_table_context_menu(self, pos):
        item = self.table.itemAt(pos)
        if not item:
            return

        menu = QtWidgets.QMenu(self)
        self.context_action_open_page = menu.addAction("Open package web page")
        self.context_action_upgrade_latest = menu.addAction("Upgrade to latest")
        self.context_action_switch_version = menu.addAction(
            "Switch to specific version")
        self.context_action_uninstall = menu.addAction("Uninstall")
        self.context_action_show_modifications = menu.addAction(
            "Show local modifications")
        self.context_action_upload = menu.addAction("Upload")
        self.context_action_publish = menu.addAction("Publish")
        self.context_action_unpublish = menu.addAction("Unpublish")

        row = item.row()
        is_logged_in = self.username is not None
        state = self.row_package_state(row)
        is_installed_locally = bool(self.row_package_version(row))
        # Possible states are: "uploading", "scan queued", "reviewing",
        # "unsafe", "reviewed", "published", "not uploaded".

        self.context_action_open_page.triggered.connect(
            lambda _: self.open_package_web_page(row))
        if state in ("uploading", "not uploaded"):
            self.context_action_open_page.setEnabled(False)

        self.context_action_upgrade_latest.triggered.connect(
            lambda _: self.upgrade_to_latest(row))
        if not is_installed_locally:
            self.context_action_upgrade_latest.setEnabled(False)

        self.context_action_switch_version.triggered.connect(
            lambda _: self.switch_to_specific_version(row))
        if not is_installed_locally:
            self.context_action_switch_version.setEnabled(False)

        self.context_action_uninstall.triggered.connect(
            lambda _: self.uninstall_package(row))
        if not is_installed_locally:
            self.context_action_uninstall.setEnabled(False)

        self.context_action_show_modifications.triggered.connect(
            lambda _: self.show_modifications(row))
        if not is_logged_in or state in ("uploading", "not uploaded"):
            self.context_action_show_modifications.setEnabled(False)

        self.context_action_upload.triggered.connect(
            lambda _: self.upload_package(row))
        self.context_action_upload.setEnabled(is_logged_in)

        self.context_action_publish.triggered.connect(
            lambda _: self.publish_package(row))
        if not is_logged_in or state != "reviewed":
            self.context_action_publish.setEnabled(False)

        self.context_action_unpublish.triggered.connect(
            lambda _: self.unpublish_package(row))
        if not is_logged_in or state != "published":
            self.context_action_unpublish.setEnabled(False)

        global_pos = self.table.viewport().mapToGlobal(pos)
        menu.exec(global_pos)

    # Other dialogs:
    # -----------------------------------------------------------------------
    def prompt_yes_or_no(self, message, default_to_yes, title="Question"):
        reply = QtWidgets.QMessageBox.question(self,
            title, message,
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
            (QtWidgets.QMessageBox.Yes if default_to_yes
                else QtWidgets.QMessageBox.No))
        return reply == QtWidgets.QMessageBox.Yes

    # Login indicator:
    # -----------------------------------------------------------------------

    def update_login_icon(self, *, force_refresh=False):
        authentication = self.core_app.find_authentication(
            force=force_refresh, find_all=True)
        self.username = None
        if authentication and authentication.validation_output:
            self.username = authentication.validation_output.get(
                "username")

        if self.username:
            self.login_indicator_button.setToolTip("Logged in")
            self.login_indicator_button.setText(self.username + " ")
            self.login_indicator_button.setFlat(True)
        else:
            self.login_indicator_button.setToolTip("Not logged in")
            self.login_indicator_button.setText("Log in ")
            self.login_indicator_button.setFlat(False)

        self.action_login_logout.setText(
            "Logout" if self.username else "Log in")

    def toggle_login(self):
        if not self.username:
            def after_log_in(login_succeeded):
                if login_succeeded:
                    self.clear_status_bar()
                self.update_login_icon()
                self.refresh_package_list()
            return Promise(core.log_in(self.core_app)).then(after_log_in)

        if not self.prompt_yes_or_no(
                f"Log out of {self.core_app.server}?", default_to_yes=False,
                title="Confirm Logout"):
            return Promise()

        def after_logout():
            self.username = None
            self.update_login_icon()
            self.refresh_package_list()
        return Promise(core.logout(self.core_app)).then(after_logout)

    # Table helpers (stubbed integration points):
    # -----------------------------------------------------------------------

    def refresh_package_list(self):
        self.table.setRowCount(0)
        installed_packages = core.get_installed_packages(self.core_app)
        package_name_to_state = core.get_package_name_to_state(
            self.core_app, installed_packages)

        for package_name, info in sorted(installed_packages.items()):
            row_index = self.table.rowCount()
            self.table.insertRow(row_index)
            row_data = (
                package_name, info.get("version", ""),
                package_name_to_state.get(package_name, ""))

            for column_index, value in enumerate(row_data):
                self.table.setItem(row_index, column_index,
                    QtWidgets.QTableWidgetItem(value))

    def row_package_name(self, row):
        return self._get_table_text(row, 0)

    def row_package_version(self, row):
        return self._get_table_text(row, 1)

    def row_package_state(self, row):
        return self._get_table_text(row, 2)

    def _get_table_text(self, row, col):
        item = self.table.item(row, col)
        if not item:
            return ""
        return item.text()

    # Status message helpers:
    # -----------------------------------------------------------------------

    def append_status_message(self, text, word_wrap=True):
        if self.status_message:
            self.status_message += "\n"

        if text.endswith("\n"):
            text = text[:-1]
        self.status_message += text
        self.update_combined_status_message()

    def set_progress_message(self, text):
        if text.endswith("\n"):
            text = text[:-1]
        self.progress_message = text
        self.update_combined_status_message()

    def clear_status_bar(self):
        self.status_message = ""
        self.progress_message = ""
        self.update_combined_status_message()

    def update_combined_status_message(self):
        text = self.status_message
        if self.progress_message:
            text += "\n"
            text += self.progress_message

        self.status_display.setPlainText(text)
        scroll_bar = self.status_display.verticalScrollBar()
        scroll_bar.setValue(scroll_bar.maximum())

    # Progress bar helpers:
    # -----------------------------------------------------------------------

    def on_background_started(self):
        self.progress_bar.setRange(0, 100)
        self.progress_row.setVisible(True)
        self.set_ui_busy(True)

    def on_background_finished(self, _result):
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_row.setVisible(False)
        self.set_ui_busy(False)

    def set_progress_percentage(self, value):
        self.progress_bar.setValue(value)

    def on_background_error(self, message):
        QtWidgets.QMessageBox.critical(self, "Error", message)

    # Action handlers (replace with real logic):
    # -----------------------------------------------------------------------

    def on_refresh_package_list(self):
        self.refresh_package_list()
        self.clear_status_bar()

    @require_login
    def create_auth_key(self):
        authentication = self.core_app.find_authentication(find_all=True)
        if authentication is None:
            return

        if authentication.client_id:
            QtWidgets.QMessageBox.critical(
                self, "Error",
                "You are already using key-based authorization.")
            return

        if not self.prompt_yes_or_no(
                "Create an key to use for authentication? This key will be"
                " saved to your hserver.ini file.",
                default_to_yes=True,
                title="Create Authentication Key?"):
            return

        def after():
            if self.core_app.toplevel_call_succeeded:
                QtWidgets.QMessageBox.information(self, "Key Created",
                    "An authentication key was successfully created.")
        Promise(core.create_and_install_oauth2_keys(self.core_app)
            ).then(after)

    @require_login
    def uninstall_auth_key(self):
        authentication = self.core_app.find_authentication(find_all=True)
        if authentication is None:
            return

        client_id = authentication.client_id
        if not client_id:
            QtWidgets.QMessageBox.critical(
                self, "Error",
                "You are not currently using key-based authorization and have"
                " no keys installed.")
            return

        messagebox = QtWidgets.QMessageBox(
            QtWidgets.QMessageBox.Icon.Question,
            "Uninstall Authentication Key",
            "Are you sure you want to delete your authentication key from"
            " hserver.ini?\n"
            f"Client ID: {client_id}",
            parent=self)
        no_button = messagebox.addButton("No", messagebox.RejectRole)
        uninstall_button = messagebox.addButton(
            "Yes, but keep it on sidefx.com", messagebox.AcceptRole)
        delete_button = messagebox.addButton(
            "Yes, and delete it from sidefx.com", messagebox.DestructiveRole)
        messagebox.setDefaultButton(no_button)
        messagebox.setEscapeButton(no_button)
        messagebox.exec()

        clicked_button = messagebox.clickedButton()
        if clicked_button is no_button:
            return

        assert clicked_button in (uninstall_button, delete_button)
        def after():
            self.core_app.find_authentication(force=True, find_all=True)
            self.update_login_icon()
            self.refresh_package_list()
            if self.core_app.toplevel_call_succeeded:
                QtWidgets.QMessageBox.information(self, "Key Uninstalled",
                    "The authentication key was successfully uninstalled.")

        if clicked_button is uninstall_button:
            Promise(core.uninstall_oauth2_keys(self.core_app, client_id)
                ).then(after)
        else:
            Promise(core.uninstall_and_delete_oauth2_keys(
                    self.core_app, client_id)
                ).then(after)


    def select_package_to_install(self):
        self.clear_status_bar()
        dlg = TextInputDialog(
            "Install Package", "Package name:", "Install", self)
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return

        package_name = dlg.input_text()
        if not package_name:
            return

        Promise(core.install(self.core_app, package_name)).then(
            self.refresh_package_list)

    def install_from_requirements(self):
        self.clear_status_bar()

        # Note that `path` will be empty if the user pressed cancel.
        current_dir = os.getcwd()
        start_dir = (os.path.expanduser("~")
            if current_dir == os.environ.get("HFS") else current_dir)
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "Select hrequirements.txt file",
            start_dir,
            "Requirements files (*.txt *.hrequirements);;All files (*)")
        if not path:
            return

        # Run the install logic for that file
        Promise(core.install_from_requirements(
            self.core_app, path)).then(self.refresh_package_list)

    def author_new_package(self):
        self.clear_status_bar()
        dlg = TextInputDialog(
            "Create a Package", "Package name:", "Next", self)
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return

        # TODO: Check if the package name is available.
        package_name = dlg.input_text()
        dlg = TextAreaDialog("Create a Package",
            "Enter the contents of the README.md, using markdown format:",
            ok_text="Next", parent=self)
        dlg.text_edit.setPlainText(
            f"Enter a one line description of what {package_name} does.\n\n"
            "## Description\n"
            "Enter a longer description here.")
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return

        readme_contents = dlg.text()
        if not readme_contents.endswith("\n"):
            readme_contents += "\n"

        dlg = FileReferenceStyleDialog(package_name, parent=self)
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return
        use_explicit = dlg.use_explicit()

        def after_reserve(package_dir):
            if package_dir is None:
                return
            (package_dir / "README.md").write_text(readme_contents)
            (package_dir / "getting-started.txt").unlink()
            self.refresh_package_list()
            QtWidgets.QMessageBox.information(self, "Package Created",
                f"{package_name} was successfully created and registered.\n\n"
                f"Please add content to {package_dir}")

        Promise(core.reserve_upload(
            self.core_app, package_name, use_explicit,
            print_output=False)).then(after_reserve)

    def upgrade_hpackage(self):
        self.clear_status_bar()

        latest_version = core.get_latest_hpackage_version(self.core_app)
        cur_version = core.__version__
        if core.Version(cur_version) >= core.Version(latest_version):
            QtWidgets.QMessageBox.information(self, "Nothing to upgrade",
                "You are already running the latest version of hpackage"
                f" ({cur_version}).")
            return

        python_ver = ".".join(str(part) for part in sys.version_info[:2])
        home_python_dir = (Path(core.houdini_user_pref_dir()) /
            f"python{python_ver}libs")
        home_python_dir.mkdir(exist_ok=True)
        hpackage_dir = home_python_dir / "hpackage"

        if hpackage_dir.is_symlink():
            QtWidgets.QMessageBox.critical(
                self, "Error",
                f"Your hpackage directory at {hpackage_dir} is a symlink so an"
                " upgrade can't be peformed for fear of overwriting files"
                " elsewhere.")
            return

        if not self.prompt_yes_or_no(
                f"Upgrade hpackage from {cur_version} to {latest_version}?",
                default_to_yes=True,
                title="Upgrade hpackage"):
            return

        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", "-U", "hpackage",
                "--target", str(home_python_dir)])
        hpackage = sys.modules["hpackage"]
        importlib.reload(hpackage)
        importlib.reload(hpackage.core)
        importlib.reload(hpackage.ui)
        importlib.reload(hpackage.ui.background)

        import hou
        for tab in hou.ui.paneTabs():
            if (tab.type() == hou.paneTabType.PythonPanel and
                    tab.activeInterface().name() == "hpackage"):
                tab.reloadActiveInterface()

    def open_package_web_page(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        url = core.get_package_page_url(self.core_app, package_name)
        webbrowser.open(url, new=2)

    def upgrade_to_latest(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        Promise(core.install(self.core_app, package_name, do_upgrade=True)
            ).then(self.refresh_package_list)

    def switch_to_specific_version(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        version = self.row_package_version(row)

        dlg = TextInputDialog(
            "Switch to specific version", "Version:", "Switch", self,
            initial_text=version)
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return

        version = dlg.input_text()
        if not version:
            return

        Promise(core.upgrade(
                self.core_app, f"{package_name}=={version}")
            ).then(self.refresh_package_list)

    def uninstall_package(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        if not self.prompt_yes_or_no(
                f"Uninstall {package_name}?", default_to_yes=True,
                title="Uninstall Package"):
            return
        Promise(core.uninstall(self.core_app, package_name)
            ).then(self.refresh_package_list)

    @require_login
    def show_modifications(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        operation_and_file_names = core.show_statuses(
            self.core_app, [package_name], print_output=False)

        dlg = InfoTableDialog("Modified Files",
            f"Local modifications to files in {package_name}:",
            operation_and_file_names, headers=["Operation", "Subpath"],
            parent=self)
        dlg.exec()

    @require_login
    def upload_package(self, row):
        self.clear_status_bar()
        package_name = self.row_package_name(row)

        dlg = ConfirmUploadDialog(package_name, parent=self)
        if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted:
            return

        Promise(core.upload(self.core_app, package_name,
            requires_grant=dlg.requires_grant(),
            auto_publish=dlg.auto_publish())
        ).then(
            self.refresh_package_list)

    @require_login
    def publish_unpublish_delete_package(
            self, row, operation, prompt, title, default_to_yes):
        self.clear_status_bar()
        package_name = self.row_package_name(row)
        package_version = self.row_package_version(row)

        if not self.prompt_yes_or_no(
                prompt.format(
                    package_name=package_name, package_version=package_version),
                default_to_yes=default_to_yes,
                title=title):
            return

        Promise(operation(self.core_app, f"{package_name}=={package_version}")
            ).then(self.refresh_package_list)

    @require_login
    def publish_package(self, row):
        self.publish_unpublish_delete_package(
            row, core.publish,
            "Publish version {package_version} of {package_name}?",
            "Publish Package",
            default_to_yes=True)

    @require_login
    def unpublish_package(self, row):
        self.publish_unpublish_delete_package(
            row, core.unpublish,
            "Unpublish version {package_version} of {package_name}?",
            "Unpublish Package",
            default_to_yes=False)

    @require_login
    def delete_package(self, row):
        self.publish_unpublish_delete_package(
            row, core.delete,
            "Delete unpublished version {package_version} of package"
            " {package_name} from the server?",
            "Delete Package",
            default_to_yes=False)


class ConfirmUploadDialog(QtWidgets.QDialog):
    def __init__(self, package_name, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Confirm Upload")

        label = QtWidgets.QLabel(f"Upload {package_name}?")
        self.auto_publish_checkbox = QtWidgets.QCheckBox(
            "Automatically publish when security scan completes")
        self.requires_grant_checkbox = QtWidgets.QCheckBox(
            "Make this package private so I can sell it")

        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok |
                QtWidgets.QDialogButtonBox.StandardButton.Cancel,
            parent=self)

        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addWidget(self.auto_publish_checkbox)
        layout.addWidget(self.requires_grant_checkbox)
        layout.addWidget(buttons)

        self.setLayout(layout)

    def auto_publish(self):
        return self.auto_publish_checkbox.isChecked()

    def requires_grant(self):
        return self.requires_grant_checkbox.isChecked()


class TextInputDialog(QtWidgets.QDialog):
    def __init__(
            self, title, prompt, ok_button_name="OK", parent=None,
            initial_text=""):
        super().__init__(parent)
        self.setWindowTitle(title)

        label = QtWidgets.QLabel(prompt)
        self.name_edit = QtWidgets.QLineEdit()
        self.name_edit.setText(initial_text)
        QtCore.QTimer.singleShot(0, self._initialize_focus)

        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok |
                QtWidgets.QDialogButtonBox.StandardButton.Cancel,
            parent=self)

        ok_button = buttons.button(
            QtWidgets.QDialogButtonBox.StandardButton.Ok)
        ok_button.setText(ok_button_name)

        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addWidget(self.name_edit)
        layout.addWidget(buttons)

        self.setLayout(layout)

    def input_text(self):
        return self.name_edit.text().strip()

    def _initialize_focus(self):
        self.name_edit.setFocus()
        self.name_edit.selectAll()


class InfoTableDialog(QtWidgets.QDialog):
    def __init__(self, title, label_text, rows, headers=None, parent=None):
        super().__init__(parent)
        self.setWindowTitle(title)

        label = QtWidgets.QLabel(label_text, self)

        table = QtWidgets.QTableWidget(self)
        table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        table.setSelectionBehavior(
            QtWidgets.QAbstractItemView.SelectRows)
        table.setSelectionMode(
            QtWidgets.QAbstractItemView.SingleSelection)

        # Determine the table dimensions.
        table.setRowCount(len(rows))
        num_cols = max((len(row) for row in rows), default=0)
        if not num_cols and headers:
            num_cols = len(headers)
        table.setColumnCount(num_cols)

        if headers:
            if len(headers) > num_cols:
                headers = list(headers)[:num_cols]
            elif len(headers) < num_cols:
                headers = list(headers) + [None] * (num_cols - len(headers))
            table.setHorizontalHeaderLabels(headers)

        # Set the table cells.
        for row_idx, row_data in enumerate(rows):
            for col_idx, value in enumerate(row_data):
                item = QtWidgets.QTableWidgetItem(str(value))
                table.setItem(row_idx, col_idx, item)

        table.resizeColumnsToContents()
        header = table.horizontalHeader()
        header.setSectionResizeMode(
            QtWidgets.QHeaderView.ResizeMode.Interactive)
        header.setStretchLastSection(True)

        # We let "Close" behave as "accept" so Esc/Enter work sensibly.
        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Close,
            parent=self)
        button_box.rejected.connect(self.reject)
        button_box.accepted.connect(self.accept)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addWidget(table)
        layout.addWidget(button_box)

        # Give a reasonable default size; still fully resizable.
        self.resize(600, 400)


class TextAreaDialog(QtWidgets.QDialog):
    def __init__(
            self, title, label_text, initial_text="", ok_text="OK",
            parent=None):
        super().__init__(parent)
        self.setWindowTitle(title)

        label = QtWidgets.QLabel(label_text, self)

        self.text_edit = QtWidgets.QPlainTextEdit(self)
        self.text_edit.setPlainText(initial_text)
        self.text_edit.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Expanding)

        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok |
            QtWidgets.QDialogButtonBox.StandardButton.Cancel,
            parent=self)

        ok_button = button_box.button(
            QtWidgets.QDialogButtonBox.StandardButton.Ok)
        # Qt creates the button lazily so we force its creation.
        if ok_button is None:
            button_box.setStandardButtons(
                QtWidgets.QDialogButtonBox.StandardButton.Ok |
                QtWidgets.QDialogButtonBox.StandardButton.Cancel)
            ok_button = button_box.button(
                QtWidgets.QDialogButtonBox.StandardButton.Ok)
        ok_button.setText(ok_text)

        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addWidget(self.text_edit)
        layout.addWidget(button_box)

        self.resize(600, 400)

    def text(self):
        return self.text_edit.toPlainText()


class FileReferenceStyleDialog(QtWidgets.QDialog):
    def __init__(self, package_name="", parent=None):
        super().__init__(parent)
        self.setWindowTitle("Create a Package")
        self._use_explicit = True

        package_name = package_name.strip()
        implicit_name = package_name or "<name>"
        explicit_name = package_name.upper() if package_name else "<NAME>"

        label = QtWidgets.QLabel(
            "How should the generated package reference its files?", self)
        label.setWordWrap(True)

        self.implicit_button = QtWidgets.QCommandLinkButton(
            "Implicit",
            "Reference files using Houdini's path search, using "
            f"{implicit_name}/<subpath>",
            self)
        self.explicit_button = QtWidgets.QCommandLinkButton(
            "Explicit",
            f"Reference files using $PKG_{explicit_name}/<subpath>",
            self)
        self.explicit_recommended_label = QtWidgets.QLabel(
            "Recommended", self)
        self.explicit_recommended_label.setAlignment(
            QtCore.Qt.AlignmentFlag.AlignHCenter)
        explicit_font = self.explicit_recommended_label.font()
        explicit_font.setBold(True)
        self.explicit_recommended_label.setFont(explicit_font)
        self.implicit_spacer_label = QtWidgets.QLabel(" ", self)
        self.implicit_spacer_label.setAlignment(
            QtCore.Qt.AlignmentFlag.AlignHCenter)
        self.implicit_spacer_label.setFont(explicit_font)

        self.implicit_button.clicked.connect(self._accept_implicit)
        self.explicit_button.clicked.connect(self._accept_explicit)
        self.explicit_button.setDefault(True)
        self.explicit_button.setAutoDefault(True)
        self.explicit_button.setFocusPolicy(
            QtCore.Qt.FocusPolicy.StrongFocus)
        button_font = self.implicit_button.font()
        button_font_size = button_font.pointSize()
        if button_font_size > 0:
            button_font.setPointSize(button_font_size + 2)
        self.implicit_button.setFont(button_font)
        self.explicit_button.setFont(button_font)
        button_style = """
            QCommandLinkButton {
                border: 1px solid palette(mid);
                border-radius: 6px;
                padding: 12px;
                background: palette(base);
            }
            QCommandLinkButton:hover {
                border-color: palette(highlight);
                background: palette(alternate-base);
            }
            QCommandLinkButton:focus {
                border: 2px solid palette(highlight);
            }
            QCommandLinkButton:pressed {
                background: palette(button);
            }
        """
        self.implicit_button.setStyleSheet(button_style)
        self.explicit_button.setStyleSheet(button_style)
        self.implicit_button.setMinimumHeight(110)
        self.explicit_button.setMinimumHeight(110)

        choices_layout = QtWidgets.QHBoxLayout()
        choices_layout.setSpacing(12)
        implicit_layout = QtWidgets.QVBoxLayout()
        implicit_layout.setSpacing(4)
        implicit_layout.addWidget(self.implicit_spacer_label)
        implicit_layout.addWidget(self.implicit_button)
        choices_layout.addLayout(implicit_layout)

        explicit_layout = QtWidgets.QVBoxLayout()
        explicit_layout.setSpacing(4)
        explicit_layout.addWidget(self.explicit_recommended_label)
        explicit_layout.addWidget(self.explicit_button)
        choices_layout.addLayout(explicit_layout)

        cancel_button = QtWidgets.QPushButton("Cancel", self)
        cancel_button.clicked.connect(self.reject)

        button_layout = QtWidgets.QHBoxLayout()
        button_layout.addStretch(1)
        button_layout.addWidget(cancel_button)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(label)
        layout.addLayout(choices_layout)
        layout.addLayout(button_layout)

        self.resize(760, 220)

    def use_explicit(self):
        return self._use_explicit

    def showEvent(self, event):
        super().showEvent(event)
        self.explicit_button.setFocus(
            QtCore.Qt.FocusReason.ActiveWindowFocusReason)

    def _accept_implicit(self):
        self._use_explicit = False
        self.accept()

    def _accept_explicit(self):
        self._use_explicit = True
        self.accept()


def _get_houdini_icon(
        icon_name, fallback_pixmap=None, size=QtCore.QSize(24, 24)):
    with zipfile.ZipFile(
            Path(os.environ["HFS"]) / "houdini/config/Icons/icons.zip") as zf:
        try:
            icon_data = zf.read(icon_name.replace("_", "/", 1) + ".svg")
        except KeyError:
            if fallback_pixmap:
                return _safe_standard_icon(fallback_pixmap)
            return QtGui.QIcon()

    renderer = QtSvg.QSvgRenderer(icon_data)
    assert renderer.isValid()
    pixmap = QtGui.QPixmap(size)
    pixmap.fill(QtCore.Qt.transparent)
    painter = QtGui.QPainter(pixmap)
    renderer.render(painter)
    painter.end()

    return QtGui.QIcon(pixmap)


def _safe_standard_icon(pixmap):
    """Safely fetch a standard icon without crashing on Houdini's QProxyStyle
    lifetime shenanigans.
    """
    app = QtWidgets.QApplication.instance()
    if not app:
        return QtGui.QIcon()

    try:
        style = app.style()
        if not style:
            return QtGui.QIcon()
        return style.standardIcon(pixmap)
    except RuntimeError:
        # Fall back to an empty icon if Houdini's proxy style is gone
        return QtGui.QIcon()


def onCreateInterface(parent=None):
    # Houdini calls this to build the panel widget
    return PackageManagerPanel(core.App(), parent)
