#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@gitorious
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
import signal
import sys
import threading
from typing import Optional, TYPE_CHECKING, List, Sequence, Union

try:
    import PyQt6
    import PyQt6.QtGui
except Exception as e:
    from electrum import GuiImportError
    raise GuiImportError(
        "Error: Could not import PyQt6. On Linux systems, "
        "you may try 'sudo apt-get install python3-pyqt6'") from e

from PyQt6.QtGui import QGuiApplication, QCursor
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip
from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt

import PyQt6.QtCore as QtCore

from electrum.logging import Logger, get_logger
_logger = get_logger(__name__)

try:
    # Preload QtMultimedia at app start, if available.
    # We use QtMultimedia on some platforms for camera-handling, and
    # lazy-loading it later led to some crashes. Maybe due to bugs in PyQt. (see #7725)
    from PyQt6.QtMultimedia import QMediaDevices; del QMediaDevices
except (ImportError, RuntimeError) as e:
    _logger.debug(f"failed to import optional dependency: PyQt6.QtMultimedia. exc={repr(e)}")
    pass  # failure is ok; it is an optional dependency.
else:
    _logger.debug(f"successfully preloaded optional dependency: PyQt6.QtMultimedia")

if sys.platform == "linux" and os.environ.get("APPIMAGE"):
    # For AppImage, we default to xcb qt backend, for better support of older system.
    # qt6 normally defaults to QT_QPA_PLATFORM=wayland instead of QT_QPA_PLATFORM=xcb.
    # However, the wayland QPA plugin requires libwayland-client0>=1.19, which is too new
    # for debian 11 or ubuntu 20.04. So instead, we default to the X11 integration (and not wayland).
    # see https://bugreports.qt.io/browse/QTBUG-114635
    os.environ.setdefault("QT_QPA_PLATFORM", "xcb")

from electrum.i18n import _, set_language
from electrum.plugin import run_hook
from electrum.util import (UserCancelled, profiler, send_exception_to_crash_reporter,
                           WalletFileException, get_new_wallet_name, InvalidPassword,
                           standardize_path, UserFacingException)
from electrum.wallet import Wallet, Abstract_Wallet
from electrum.wallet_db import WalletRequiresSplit, WalletRequiresUpgrade, WalletUnfinished
from electrum.gui import BaseElectrumGui
from electrum.simple_config import SimpleConfig
from electrum.wizard import WizardViewState
from electrum.keystore import load_keystore
from electrum.bip32 import is_xprv
from electrum import constants

from electrum.gui.common_qt.i18n import ElectrumTranslator
from electrum.gui.messages import TERMS_OF_USE_LATEST_VERSION

from .util import (read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin, WWLabel,
                   set_windows_os_screenshot_protection_drm_flag)
from .main_window import ElectrumWindow
from .network_dialog import NetworkDialog
from .stylesheet_patcher import patch_qt_stylesheet
from .lightning_dialog import LightningDialog
from .exception_window import Exception_Hook
from .wizard.server_connect import QEServerConnectWizard
from .wizard.wallet import QENewWalletWizard

if TYPE_CHECKING:
    from electrum.daemon import Daemon
    from electrum.plugin import Plugins


class OpenFileEventFilter(QObject):
    def __init__(self, windows: Sequence[ElectrumWindow]):
        self.windows = windows
        super(OpenFileEventFilter, self).__init__()

    def eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.Type.FileOpen:
            if len(self.windows) >= 1:
                self.windows[0].set_payment_identifier(event.url().toString())
                return True
        return False


class ScreenshotProtectionEventFilter(QObject):
    def __init__(self):
        super().__init__()

    def eventFilter(self, obj, event):
        if (
            event.type() == QtCore.QEvent.Type.Show
            and isinstance(obj, QWidget)
            and obj.isWindow()
        ):
            set_windows_os_screenshot_protection_drm_flag(obj)
        return False


class QElectrumApplication(QApplication):
    new_window_signal = pyqtSignal(str, object)
    quit_signal = pyqtSignal()
    refresh_tabs_signal = pyqtSignal()
    refresh_amount_edits_signal = pyqtSignal()
    update_status_signal = pyqtSignal()
    update_fiat_signal = pyqtSignal()
    alias_received_signal = pyqtSignal()


class ElectrumGui(BaseElectrumGui, Logger):

    network_dialog: Optional['NetworkDialog']
    lightning_dialog: Optional['LightningDialog']

    @profiler
    def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):
        BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins)
        Logger.__init__(self)
        self.logger.info(f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}")
        # Uncomment this call to verify objects are being properly
        # GC-ed when windows are closed
        #plugins.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
        #                            ElectrumWindow], interval=5)])
        if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
            QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
        if hasattr(QGuiApplication, 'setDesktopFileName'):
            QGuiApplication.setDesktopFileName('electrum')
        QGuiApplication.setApplicationName("Electrum")
        self.gui_thread = threading.current_thread()
        self.windows = []  # type: List[ElectrumWindow]
        self.open_file_efilter = OpenFileEventFilter(self.windows)
        self.app = QElectrumApplication(sys.argv)
        self.app.installEventFilter(self.open_file_efilter)
        self.screenshot_protection_efilter = ScreenshotProtectionEventFilter()
        if sys.platform in ['win32', 'windows'] and self.config.GUI_QT_SCREENSHOT_PROTECTION:
            self.app.installEventFilter(self.screenshot_protection_efilter)
        # explicitly set 'AA_DontShowIconsInMenus' False so menu icons are shown on MacOS
        self.app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, on=False)
        self.app.setWindowIcon(read_QIcon("electrum.png"))
        self.translator = ElectrumTranslator()
        self.app.installTranslator(self.translator)
        self._cleaned_up = False
        self.network_dialog = None
        self.lightning_dialog = None
        self._num_wizards_in_progress = 0
        self._num_wizards_lock = threading.Lock()
        self.dark_icon = self.config.GUI_QT_DARK_TRAY_ICON
        self.tray = None  # type: Optional[QSystemTrayIcon]
        self._init_tray()
        self.app.new_window_signal.connect(self.start_new_window)
        self.app.quit_signal.connect(self.app.quit, Qt.ConnectionType.QueuedConnection)
        # maybe set dark theme
        self._default_qtstylesheet = self.app.styleSheet()
        self.reload_app_stylesheet()

    def _init_tray(self):
        self.tray = QSystemTrayIcon(self.tray_icon(), None)
        self.tray.setToolTip('Electrum')
        self.tray.activated.connect(self.tray_activated)
        self.build_tray_menu()
        self.tray.show()

    def reload_app_stylesheet(self):
        """Set the Qt stylesheet and custom colors according to the user-selected
        light/dark theme.
        TODO this can ~almost be used to change the theme at runtime (without app restart),
             except for util.ColorScheme... widgets already created with colors set using
             ColorSchemeItem.as_stylesheet() and similar will not get recolored.
             See e.g.
             - in Coins tab, the color for "frozen" UTXOs, or
             - in TxDialog, the receiving/change address colors
        """
        use_dark_theme = self.config.GUI_QT_COLOR_THEME == 'dark'
        if use_dark_theme:
            try:
                import qdarkstyle
                self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt6())
            except BaseException as e:
                use_dark_theme = False
                self.logger.warning(f'Error setting dark theme: {repr(e)}')
        else:
            self.app.setStyleSheet(self._default_qtstylesheet)
        # Apply any necessary stylesheet patches
        patch_qt_stylesheet(use_dark_theme=use_dark_theme)
        # Even if we ourselves don't set the dark theme,
        # the OS/window manager/etc might set *a dark theme*.
        # Hence, try to choose colors accordingly:
        ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)

    def build_tray_menu(self):
        if not self.tray:
            return
        # Avoid immediate GC of old menu when window closed via its action
        if self.tray.contextMenu() is None:
            m = QMenu()
            self.tray.setContextMenu(m)
        else:
            m = self.tray.contextMenu()
            m.clear()
        network = self.daemon.network
        m.addAction(_("Plugins"), self.show_plugins_dialog)
        if network:
            m.addAction(_("Network"), self.show_network_dialog)
        if network and network.lngossip:
            m.addAction(_("Lightning Network"), self.show_lightning_dialog)
        for window in self.windows:
            name = window.wallet.basename()
            submenu = m.addMenu(name)
            submenu.addAction(_("Show/Hide"), window.show_or_hide)
            submenu.addAction(_("Close"), window.close)
        m.addAction(_("Dark/Light"), self.toggle_tray_icon)
        m.addSeparator()
        m.addAction(_("Exit Electrum"), self.app.quit)

    def tray_icon(self):
        if self.dark_icon:
            return read_QIcon('electrum_dark_icon.png')
        else:
            return read_QIcon('electrum_light_icon.png')

    def toggle_tray_icon(self):
        if not self.tray:
            return
        self.dark_icon = not self.dark_icon
        self.config.GUI_QT_DARK_TRAY_ICON = self.dark_icon
        self.tray.setIcon(self.tray_icon())

    def tray_activated(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
            if all([w.is_hidden() for w in self.windows]):
                for w in self.windows:
                    w.bring_to_top()
            else:
                for w in self.windows:
                    w.hide()

    def _cleanup_before_exit(self):
        if self._cleaned_up:
            return
        self._cleaned_up = True
        self.app.new_window_signal.disconnect()
        self.app.removeEventFilter(self.open_file_efilter)
        self.open_file_efilter = None
        # it is save to remove the filter, even if it has not been installed
        self.app.removeEventFilter(self.screenshot_protection_efilter)
        self.screenshot_protection_efilter = None
        # If there are still some open windows, try to clean them up.
        for window in list(self.windows):
            window.close()
            window.clean_up()
        if self.network_dialog:
            self.network_dialog.close()
            self.network_dialog = None
        if self.lightning_dialog:
            self.lightning_dialog.close()
            self.lightning_dialog = None
        # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
        event = QtCore.QEvent(QtCore.QEvent.Type.Clipboard)
        self.app.sendEvent(self.app.clipboard(), event)
        if self.tray:
            self.tray.hide()
            self.tray.deleteLater()
            self.tray = None

    def _maybe_quit_if_no_windows_open(self) -> None:
        """Check if there are any open windows and decide whether we should quit."""
        # keep daemon running after close
        if self.config.get('daemon'):
            return
        # check if a wizard is in progress
        with self._num_wizards_lock:
            if self._num_wizards_in_progress > 0 or len(self.windows) > 0:
                return
        self.app.quit()

    def new_window(self, path, uri=None):
        # Use a signal as can be called from daemon thread
        self.app.new_window_signal.emit(path, uri)

    def show_lightning_dialog(self):
        if not self.daemon.network.has_channel_db():
            return
        if not self.lightning_dialog:
            self.lightning_dialog = LightningDialog(self)
        self.lightning_dialog.bring_to_top()

    def show_plugins_dialog(self):
        from .plugins_dialog import PluginsDialog
        d = PluginsDialog(self.config, self.plugins, gui_object=self)
        d.exec()

    def show_network_dialog(self, proxy_tab=False):
        if self.network_dialog:
            self.network_dialog.show(proxy_tab=proxy_tab)
            self.network_dialog.raise_()
            return
        self.network_dialog = NetworkDialog(network=self.daemon.network)
        self.network_dialog.show(proxy_tab=proxy_tab)

    def _create_window_for_wallet(self, wallet):
        w = ElectrumWindow(self, wallet)
        self.windows.append(w)
        self.build_tray_menu()
        w.warn_if_testnet()
        w.warn_if_watching_only()
        return w

    def count_wizards_in_progress(func):
        def wrapper(self: 'ElectrumGui', *args, **kwargs):
            with self._num_wizards_lock:
                self._num_wizards_in_progress += 1
            try:
                return func(self, *args, **kwargs)
            finally:
                with self._num_wizards_lock:
                    self._num_wizards_in_progress -= 1
                self._maybe_quit_if_no_windows_open()
        return wrapper

    def get_window_for_wallet(self, wallet):
        for window in self.windows:
            if window.wallet.storage.get_path() == wallet.storage.get_path():
                return window

    @count_wizards_in_progress
    def start_new_window(
            self,
            path,
            uri: Optional[str],
            *,
            app_is_starting: bool = False,
            force_wizard: bool = False,
    ) -> Optional[ElectrumWindow]:
        """Raises the window for the wallet if it is open.
        Otherwise, opens the wallet and creates a new window for it.
        Warning: the returned window might be for a completely different wallet
                 than the provided path, as we allow user interaction to change the path.
        """
        if not self.has_accepted_terms_of_use():
            self.logger.warning(f"terms of use not accepted, rejecting to start new window")
            return None

        def __handle_wallet_loading_exc(exc: Exception, pos):
            if isinstance(exc, UserFacingException) \
                    or isinstance(exc, WalletFileException) and not exc.should_report_crash:
                self.logger.exception(f"{pos=}")
                custom_message_box(icon=QMessageBox.Icon.Warning,
                                   parent=None,
                                   title=_('Error'),
                                   text=_('Cannot load wallet') + f' ({pos}):\n' + str(exc))
            else:
                send_exception_to_crash_reporter(exc)

        wallet = None
        # Try to open with daemon first. If this succeeds, there won't be a wizard at all
        # (the wallet main window will appear directly).
        if not force_wizard:
            try:
                wallet = self.daemon.load_wallet(path, None)
            except FileNotFoundError:
                pass  # open with wizard below
            except InvalidPassword:
                pass  # open with wizard below
            except WalletRequiresSplit:
                pass  # open with wizard below
            except WalletRequiresUpgrade:
                pass  # open with wizard below
            except WalletUnfinished:
                pass  # open with wizard below
            except Exception as e:
                __handle_wallet_loading_exc(e, 1)
                # if app is starting, still let wizard appear
                if not app_is_starting:
                    return
        # Open a wizard window. This lets the user e.g. enter a password, or select
        # a different wallet.
        try:
            if not wallet:
                wallet = self._start_wizard_to_select_or_create_wallet(path)
            if not wallet:
                return
            window = self.get_window_for_wallet(wallet)
            # create or raise window
            if not window:
                window = self._create_window_for_wallet(wallet)
        except UserCancelled:
            return
        except Exception as e:
            __handle_wallet_loading_exc(e, 2)
            if app_is_starting:
                # If we raise in this context, there are no more fallbacks, we will shut down.
                # Worst case scenario, we might have gotten here without user interaction,
                # in which case, if we raise now without user interaction, the same sequence of
                # events is likely to repeat when the user restarts the process.
                # So we play it safe: clear path, clear uri, force a wizard to appear.
                try:
                    wallet_dir = os.path.dirname(path)
                    filename = get_new_wallet_name(wallet_dir)
                except OSError:
                    path = self.config.get_fallback_wallet_path()
                else:
                    path = os.path.join(wallet_dir, filename)
                return self.start_new_window(path, uri=None, force_wizard=True)
            return
        window.bring_to_top()
        window.setWindowState(window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive)
        window.activateWindow()
        if uri:
            window.show_send_tab()
            # Handle URI defensively - local attacker with access to RPC server and config file could get here:
            #   - tell user something happened
            window.notify(_("Updated 'Pay To' field to handle external URI"))
            #   - clear all fields in Send tab:
            #     - perhaps user was just filling out the fields, trying to make another payment.
            #       e.g. if the given URI does not have an amount, we should clear the amount field
            window.send_tab.do_clear()
            #   - update "Pay To" field (and maybe others)
            window.send_tab.set_payment_identifier(uri)
        return window

    def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]:
        wizard = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path)
        result = wizard.exec()
        # TODO: use dialog.open() instead to avoid new event loop spawn?
        self.logger.info(f'wizard dialog exec result={result}')
        if result == QDialog.DialogCode.Rejected:
            self.logger.info('wizard dialog cancelled by user')
            return

        d = wizard.get_wizard_data()

        if d['wallet_is_open']:
            wallet_path = standardize_path(d['wallet_name'])
            for window in self.windows:
                if window.wallet.storage.get_path() == wallet_path:
                    return window.wallet
            raise Exception('found by wizard but not here?!')

        if not d['wallet_exists']:
            self.logger.info('about to create wallet')
            wizard.create_storage()
            if d['wallet_type'] == '2fa' and 'x3' not in d:
                return
            wallet_file = wizard.path
        else:
            wallet_file = d['wallet_name']

        password = d.get('password') or None  # convert '' to None

        try:
            wallet = self.daemon.load_wallet(wallet_file, password, upgrade=True)
            return wallet
        except WalletRequiresSplit as e:
            wizard.run_split(wallet_file, e._split_data)
            return
        except WalletUnfinished as e:
            # wallet creation is not complete, 2fa online phase
            db = e._wallet_db
            action = db.get_action()
            assert action[1] == 'accept_terms_of_use', 'only support for resuming trustedcoin split setup'
            k1 = load_keystore(db, 'x1')
            if password is not None:
                xprv = k1.get_master_private_key(password)
            else:
                xprv = db.get('x1')['xprv']
                if not is_xprv(xprv):
                    xprv = k1
            _wiz_data_updates = {
                'wallet_name': wallet_file,
                'xprv1': xprv,
                'xpub1': db.get('x1')['xpub'],
                'xpub2': db.get('x2')['xpub'],
            }
            data = {**d, **_wiz_data_updates}
            wizard = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path,
                                       start_viewstate=WizardViewState('trustedcoin_tos', data, {}))
            result = wizard.exec()
            if result == QDialog.DialogCode.Rejected:
                self.logger.info('wizard dialog cancelled by user')
                return
            db.put('x3', wizard.get_wizard_data()['x3'])
            db.write_and_force_consolidation()  # TODO API for db is a bit weird: there should be a close method

        wallet = self.daemon.load_wallet(wallet_file, password, upgrade=True)
        return wallet

    def close_window(self, window: ElectrumWindow):
        if window in self.windows:
            self.windows.remove(window)
        self.build_tray_menu()
        run_hook('on_close_window', window)
        if window.should_stop_wallet_on_close:
            self.daemon.stop_wallet(window.wallet.storage.get_path())

    def reload_window(self, window):
        # bump counter so that we do not close the app
        self._num_wizards_in_progress += 1
        wallet = window.wallet
        window.should_stop_wallet_on_close = False
        window.close()
        self._create_window_for_wallet(wallet)
        self._num_wizards_in_progress -= 1

    def reload_windows(self):
        for window in list(self.windows):
            self.reload_window(window)

    def has_accepted_terms_of_use(self) -> bool:
        if self.config.TERMS_OF_USE_ACCEPTED >= TERMS_OF_USE_LATEST_VERSION\
                or constants.net.NET_NAME == "regtest":
            return True
        return False

    def ask_terms_of_use(self):
        """Ask the user to accept the terms of use.
        This is only shown if the user has not accepted them yet.
        """
        if self.has_accepted_terms_of_use():
            return
        from electrum.gui.qt.wizard.terms_of_use import QETermsOfUseWizard
        dialog = QETermsOfUseWizard(self.config, self.app)
        result = dialog.exec()
        if result == QDialog.DialogCode.Rejected:
            self.logger.info('terms of use not accepted by user')
            raise UserCancelled()

    def init_network(self):
        """Start the network, including showing a first-start network dialog if config does not exist."""
        if self.daemon.network:
            # first-start network-setup
            if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():
                dialog = QEServerConnectWizard(self.config, self.app, self.plugins, self.daemon)
                result = dialog.exec()
                if result == QDialog.DialogCode.Rejected:
                    self.logger.info('network wizard dialog cancelled by user')
                    raise UserCancelled()

            # start network
            self.daemon.start_network()

    def main(self):
        # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever
        self.app.setQuitOnLastWindowClosed(False)  # so _we_ can decide whether to quit
        self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open)
        self.app.aboutToQuit.connect(self._cleanup_before_exit)
        signal.signal(signal.SIGINT, lambda *args: self.app.quit())
        # hook for crash reporter
        Exception_Hook.maybe_setup(config=self.config)
        # start network, and maybe show first-start network-setup
        try:
            self.ask_terms_of_use()
            self.init_network()
        except UserCancelled:
            return
        except Exception as e:
            self.logger.exception('')
            return
        # start wizard to select/create wallet
        path = self.config.get_wallet_path()
        try:
            if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):
                return
        except Exception as e:
            self.logger.error("error loading wallet (or creating window for it)")
            send_exception_to_crash_reporter(e)
            # Let Qt event loop start properly so that crash reporter window can appear.
            # We will shutdown when the user closes that window, via lastWindowClosed signal.
        # main loop
        self.logger.info("starting Qt main loop")
        self.app.exec()
        # on some platforms the exec_ call may not return, so use _cleanup_before_exit

    def stop(self):
        self.logger.info('closing GUI')
        self.app.quit_signal.emit()

    @classmethod
    def version_info(cls):
        ret = {
            "qt.version": QtCore.QT_VERSION_STR,
            "pyqt.version": QtCore.PYQT_VERSION_STR,
        }
        if hasattr(PyQt6, "__path__"):
            ret["pyqt.path"] = ", ".join(PyQt6.__path__ or [])
        return ret

    def do_copy(self, text: str, *, title: str = None) -> None:
        self.app.clipboard().setText(text)
        message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title)
        # tooltip cannot be displayed immediately when called from a menu; wait 200ms
        QTimer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, None))


def standalone_exception_dialog(exception: Union[str, BaseException]) -> None:
    app = QApplication.instance()
    if not app:
        app = QApplication([])

    msg_box = QMessageBox()
    msg_box.setWindowTitle(_("Error starting Electrum"))
    msg_box.setIcon(QMessageBox.Icon.Critical)
    msg_box.setText(_("An error occurred") + ":")
    msg_box.setInformativeText(str(exception))

    # Add detailed traceback if available
    if hasattr(exception, "__traceback__"):
        import traceback
        detailed_text = ''.join(traceback.format_exception(
            type(exception), exception, exception.__traceback__)
        )
        msg_box.setDetailedText(detailed_text)

    msg_box.exec()
