"""
Provide error handling for the Graphical User Interface.
"""

from __future__ import annotations

import pickle
from asyncio import CancelledError
from logging import getLogger
from traceback import format_exception
from typing import TypeVar, Generic, ParamSpec, TYPE_CHECKING

from PyQt6.QtCore import QMetaObject, Qt, Q_ARG, QObject
from PyQt6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QPushButton,
    QScrollArea,
    QFrame,
)
from typing_extensions import override

from betty.error import UserFacingError
from betty.gui.text import Code, Text
from betty.gui.window import BettyMainWindow
from betty.locale import Str, Localizable

if TYPE_CHECKING:
    from PyQt6.QtGui import QCloseEvent
    from betty.app import App
    from types import TracebackType

_T = TypeVar("_T")
_P = ParamSpec("_P")

_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException)


class ExceptionCatcher(Generic[_P, _T]):
    """
    Catch any exception and show an error window instead.
    """

    _SUPPRESS_EXCEPTION_TYPES = (CancelledError,)

    def __init__(
        self,
        parent: QObject,
        *,
        close_parent: bool = False,
    ):
        self._parent = parent
        self._close_parent = close_parent

    def __enter__(self) -> None:
        pass

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> bool | None:
        return self._catch(exc_type, exc_val)

    async def __aenter__(self) -> None:
        pass

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> bool | None:
        return self._catch(exc_type, exc_val)

    def _catch(
        self,
        exception_type: type[_BaseExceptionT] | None,
        exception: _BaseExceptionT | None,
    ) -> bool | None:
        from betty.gui import BettyApplication

        if exception_type is None or exception is None:
            return None

        if isinstance(exception, self._SUPPRESS_EXCEPTION_TYPES):
            return None

        if isinstance(exception, UserFacingError):
            QMetaObject.invokeMethod(
                BettyApplication.instance(),
                "_show_user_facing_error",
                Qt.ConnectionType.QueuedConnection,
                Q_ARG(type, exception_type),
                Q_ARG(bytes, pickle.dumps(exception)),
                Q_ARG(QObject, self._parent),
                Q_ARG(bool, self._close_parent),
            )
        else:
            getLogger(__name__).exception(exception)
            QMetaObject.invokeMethod(
                BettyApplication.instance(),
                "_show_unexpected_exception",
                Qt.ConnectionType.QueuedConnection,
                Q_ARG(type, exception_type),
                Q_ARG(str, str(exception)),
                Q_ARG(str, "".join(format_exception(exception))),
                Q_ARG(QObject, self._parent),
                Q_ARG(bool, self._close_parent),
            )
        return True


class Error(BettyMainWindow):
    """
    An error window.
    """

    window_height = 300
    window_width = 500

    def __init__(
        self,
        app: App,
        message: Localizable,
        *,
        parent: QObject,
        close_parent: bool = False,
    ):
        super().__init__(app, parent=parent)
        self._message_localizable = message
        if close_parent and not isinstance(parent, QWidget):
            raise ValueError("If `close_parent` is true, `parent` must be `QWidget`.")
        self._close_parent = close_parent
        self.setWindowModality(Qt.WindowModality.WindowModal)

        central_widget = QWidget()
        self._central_layout = QVBoxLayout()
        central_widget.setLayout(self._central_layout)
        self.setCentralWidget(central_widget)

        self._message = Text()
        self._central_layout.addWidget(self._message)

        self._controls = QHBoxLayout()
        self._central_layout.addLayout(self._controls)

        self._dismiss = QPushButton()
        self._dismiss.released.connect(self.close)
        self._controls.addWidget(self._dismiss)

    @override
    @property
    def window_title(self) -> Localizable:
        return Str.plain("{error} - Betty", error=Str._("Error"))

    @override
    def _set_translatables(self) -> None:
        super()._set_translatables()
        self._message.setText(self._message_localizable.localize(self._app.localizer))
        self._dismiss.setText(self._app.localizer._("Close"))

    @override
    def closeEvent(self, a0: QCloseEvent | None) -> None:
        if self._close_parent:
            parent = self.parent()
            if isinstance(parent, QWidget):
                parent.close()
        super().closeEvent(a0)


_ErrorT = TypeVar("_ErrorT", bound=Error)


class ExceptionError(Error):
    """
    An error window for a specific exception.
    """

    def __init__(
        self,
        app: App,
        message: Localizable,
        error_type: type[BaseException],
        *,
        parent: QObject,
        close_parent: bool = False,
    ):
        super().__init__(app, message, parent=parent, close_parent=close_parent)
        self.error_type = error_type


_ExceptionErrorT = TypeVar("_ExceptionErrorT", bound=ExceptionError)


class _UnexpectedExceptionError(ExceptionError):
    """
    An error window for a specific unexpected exception.
    """

    def __init__(
        self,
        app: App,
        error_type: type[Exception],
        error_message: str,
        error_traceback: str,
        *,
        parent: QObject,
        close_parent: bool = False,
    ):
        super().__init__(
            app,
            Str._(
                'An unexpected error occurred and Betty could not complete the task. Please <a href="{report_url}">report this problem</a> and include the following details, so the team behind Betty can address it.',
                report_url="https://github.com/bartfeenstra/betty/issues",
            ),
            error_type,
            parent=parent,
            close_parent=close_parent,
        )

        if error_message:
            self._exception_message = Code(error_message)
            self._central_layout.addWidget(self._exception_message)

        self._exception_details = QScrollArea()
        self._exception_details.setFrameShape(QFrame.Shape.NoFrame)
        self._exception_details.setWidget(
            Code(error_traceback + error_traceback + error_traceback + error_traceback)
        )
        self._exception_details.setWidgetResizable(True)
        self._central_layout.addWidget(self._exception_details)
