import functools
import sys
import traceback

from qtpy.QtCore import QObject, Qt, Signal, Slot
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget


def SafeSlot(*slot_args, **slot_kwargs):  # pylint: disable=invalid-name
    """Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
    to the passed function, to display errors instead of potentially raising an exception

    'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
    otherwise error display is left to the original exception hook
    """
    popup_error = bool(slot_kwargs.pop("popup_error", False))

    def error_managed(method):
        @Slot(*slot_args, **slot_kwargs)
        @functools.wraps(method)
        def wrapper(*args, **kwargs):
            try:
                return method(*args, **kwargs)
            except Exception:
                ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=popup_error)

        return wrapper

    return error_managed


class WarningPopupUtility(QObject):
    """
    Utility class to show warning popups in the application.
    """

    @SafeSlot(str, str, str, QWidget)
    def show_warning_message(self, title, message, detailed_text, widget):
        msg = QMessageBox(widget)
        msg.setIcon(QMessageBox.Warning)
        msg.setWindowTitle(title)
        msg.setText(message)
        msg.setStandardButtons(QMessageBox.Ok)
        msg.setDetailedText(detailed_text)
        msg.exec_()

    def show_warning(self, title: str, message: str, detailed_text: str, widget: QWidget = None):
        """
        Show a warning message with the given title, message, and detailed text.

        Args:
            title (str): The title of the warning message.
            message (str): The main text of the warning message.
            detailed_text (str): The detailed text to show when the user expands the message.
            widget (QWidget): The parent widget for the message box.
        """
        self.show_warning_message(title, message, detailed_text, widget)


_popup_utility_instance = None


class _ErrorPopupUtility(QObject):
    """
    Utility class to manage error popups in the application to show error messages to the users.
    This class is singleton and the error popup can be enabled or disabled globally or attach to widget methods with decorator @error_managed.
    """

    error_occurred = Signal(str, str, QWidget)

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.error_occurred.connect(self.show_error_message)
        self.enable_error_popup = False
        self._initialized = True
        sys.excepthook = self.custom_exception_hook

    @SafeSlot(str, str, QWidget)
    def show_error_message(self, title, message, widget):
        detailed_text = self.format_traceback(message)
        error_message = self.parse_error_message(detailed_text)

        msg = QMessageBox(widget)
        msg.setIcon(QMessageBox.Critical)
        msg.setWindowTitle(title)
        msg.setText(error_message)
        msg.setStandardButtons(QMessageBox.Ok)
        msg.setDetailedText(detailed_text)
        msg.setTextInteractionFlags(Qt.TextSelectableByMouse)
        msg.setMinimumWidth(600)
        msg.setMinimumHeight(400)
        msg.exec_()

    def format_traceback(self, traceback_message: str) -> str:
        """
        Format the traceback message to be displayed in the error popup by adding indentation to each line.

        Args:
            traceback_message(str): The traceback message to be formatted.

        Returns:
            str: The formatted traceback message.
        """
        formatted_lines = []
        lines = traceback_message.split("\n")
        for line in lines:
            formatted_lines.append("    " + line)  # Add indentation to each line
        return "\n".join(formatted_lines)

    def parse_error_message(self, traceback_message):
        lines = traceback_message.split("\n")
        error_message = "Error occurred. See details."
        capture = False
        captured_message = []

        for line in lines:
            if "raise" in line:
                capture = True
                continue
            if capture:
                if line.strip() and not line.startswith("  File "):
                    captured_message.append(line.strip())
                else:
                    break

        if captured_message:
            error_message = " ".join(captured_message)
        return error_message

    def custom_exception_hook(self, exctype, value, tb, popup_error=False):
        if popup_error or self.enable_error_popup:
            error_message = traceback.format_exception(exctype, value, tb)
            self.error_occurred.emit(
                "Method error" if popup_error else "Application Error",
                "".join(error_message),
                self.parent(),
            )
        else:
            sys.__excepthook__(exctype, value, tb)  # Call the original excepthook

    def enable_global_error_popups(self, state: bool):
        """
        Enable or disable global error popups for all applications.

        Args:
            state(bool): True to enable error popups, False to disable error popups.
        """
        self.enable_error_popup = bool(state)


def ErrorPopupUtility():
    global _popup_utility_instance
    if not _popup_utility_instance:
        _popup_utility_instance = _ErrorPopupUtility()
    return _popup_utility_instance


class ExampleWidget(QWidget):  # pragma: no cover
    """
    Example widget to demonstrate error handling with the ErrorPopupUtility.

    Warnings -> This example works properly only with PySide6, PyQt6 has a bug with the error handling.
    """

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.init_ui()
        self.warning_utility = WarningPopupUtility(self)

    def init_ui(self):
        self.layout = QVBoxLayout(self)

        # Button to trigger method with error handling
        self.error_button = QPushButton("Trigger Handled Error", self)
        self.error_button.clicked.connect(self.method_with_error_handling)
        self.layout.addWidget(self.error_button)

        # Button to trigger method without error handling
        self.normal_button = QPushButton("Trigger Normal Error", self)
        self.normal_button.clicked.connect(self.method_without_error_handling)
        self.layout.addWidget(self.normal_button)

        # Button to trigger warning popup
        self.warning_button = QPushButton("Trigger Warning", self)
        self.warning_button.clicked.connect(self.trigger_warning)
        self.layout.addWidget(self.warning_button)

    @SafeSlot(popup_error=True)
    def method_with_error_handling(self):
        """This method raises an error and the exception is handled by the decorator."""
        raise ValueError("This is a handled error.")

    @SafeSlot()
    def method_without_error_handling(self):
        """This method raises an error and the exception is not handled here."""
        raise ValueError("This is an unhandled error.")

    @SafeSlot()
    def trigger_warning(self):
        """Trigger a warning using the WarningPopupUtility."""
        self.warning_utility.show_warning(
            title="Warning",
            message="This is a warning message.",
            detailed_text="This is the detailed text of the warning message.",
            widget=self,
        )


if __name__ == "__main__":  # pragma: no cover

    app = QApplication(sys.argv)
    widget = ExampleWidget()
    widget.show()
    sys.exit(app.exec_())
