# Standard
from collections.abc import Callable
from datetime import datetime, timezone

# pyQGIS
from qgis.core import Qgis, QgsApplication, QgsTask
from qgis.gui import QgsMessageBar
from qgis.PyQt.QtCore import QCoreApplication, QObject, QTimer
from qgis.PyQt.QtWidgets import QPushButton
from qgis.utils import iface

# project
from oslandia.custom_exceptions import UnavailableUserError
from oslandia.helpdesk.graphql_issue import IssuesGraphQLRequestManager
from oslandia.helpdesk.issue import Issues
from oslandia.helpdesk.project import Project, ProjectRequestManager
from oslandia.helpdesk.user import UserRequestManager
from oslandia.toolbelt.log_handler import PlgLogger
from oslandia.toolbelt.preferences import PlgOptionsManager


class GitLabIssueChecker(QgsTask):
    def __init__(
        self,
        last_check: datetime,
        projects: list[Project],
        excluded_usernames: list[str] | None = None,
        message_bar: QgsMessageBar | None = None,
        issue_clicked_callable: Callable[[Issues], None] | None = None,
    ):
        """QgsTask to check for new issues in GitLab.

        :param last_check: datetime defining last check
        :type last_check: datetime
        :param projects: list of project to check
        :type projects: List[Project]
        :param excluded_usernames: list of username to exclude, Default None
        :type excluded_usernames: Optional[List[str]]
        :param message_bar: QgsMessageBar used to display result, defaults to None
        :type message_bar: Optional[QgsMessageBar], optional
        :param issue_clicked_callable: Callable used to connect to issue display button, defaults to None
        :type issue_clicked_callable: Optional[Callable[[Issues], None]], optional
        """
        # since a Qt translation is reuired before the super.init, we call the
        # QCoreApplication.translate method diretcly instead of relying on the one
        # coming with the upcoming inherited QObject
        super().__init__(
            description=QCoreApplication.translate(
                "GitLabIssueChecker", "Checking for new issue"
            ),
            flags=QgsTask.Flag.CanCancel | QgsTask.Flag.Silent,
        )
        self.last_check: datetime = last_check
        self.issue_clicked_callable: Callable[[Issues], None] | None = (
            issue_clicked_callable
        )

        self.projects = projects
        self.excluded_usernames = excluded_usernames
        self.new_issues: list[Issues] = []
        self.updated_issues: list[Issues] = []
        self.error_msg = ""
        self.log = PlgLogger().log

        # If no message bar defined used qgis message bar
        if message_bar:
            self.message_bar = message_bar
        else:
            self.message_bar = iface.messageBar()

    def run(self) -> bool:
        """Run QgsTask to get list of new and updated issues

        :return: True if no errors occurs, False otherwise
        :rtype: bool
        """
        try:
            manager = IssuesGraphQLRequestManager()

            # No error
            self.error_msg = ""

            # No new or updated issues
            self.new_issues = []
            self.updated_issues = []

            for project in self.projects:
                self.log(
                    message=f"Check for issue updated for project {project.name} after "
                    f"{self.last_check} with current date {datetime.now(timezone.utc)}",
                    log_level=Qgis.MessageLevel.NoLevel,
                    push=False,
                )

                new_issues = manager.get_new_issues_list(
                    project_path=project.full_path,
                    created_after=self.last_check,
                    excluded_usernames=self.excluded_usernames,
                )
                updated_issues = manager.get_updated_issues_list(
                    project_path=project.full_path,
                    updated_after=self.last_check,
                    excluded_usernames=self.excluded_usernames,
                )

                self.updated_issues.extend(updated_issues)
                self.new_issues.extend(new_issues)
            return True
        except (ConnectionError, ValueError) as e:
            # Error during request or data parsing
            self.error_msg = str(e)
            return False

    def _add_message_bar_widget(self, title: str, issue: Issues) -> None:
        """Add a widget in QgsMessageBar for issue display

        :param title: QgsMessageBar title
        :type title: str
        :param issue: issue to be displayed
        :type issue: Issues
        """
        notification = self.message_bar.createMessage(title=title, text=issue.name)
        # Add button in message to display issue
        if self.issue_clicked_callable:
            widget_button = QPushButton(self.tr("Display issue"))
            # Connect button to specific Callable for issue display
            widget_button.clicked.connect(
                lambda _, issue=issue, issue_clicked=self.issue_clicked_callable: (
                    issue_clicked(issue)
                )
            )
            # notification dismissed when button clicked
            widget_button.clicked.connect(notification.dismiss)
            # Add button to widget
            notification.layout().addWidget(widget_button)

        # No duration indicated, user must click dismiss to remove notification
        self.message_bar.pushWidget(widget=notification, level=Qgis.MessageLevel.Info)

    def finished(self, result: bool):
        """Add found new and updated issue in a QgsMessageBar

        :param result: True if check was OK, False otherwise
        :type result: bool
        """
        if result:
            # Add notifications
            for issue in self.new_issues:
                self._add_message_bar_widget(self.tr("New issue"), issue)
            for issue in self.updated_issues:
                self._add_message_bar_widget(self.tr("Issue updated"), issue)
        else:
            # Display check errors
            self.message_bar.pushWarning(
                self.tr("Notification check error"), self.error_msg
            )


class GitLabNotificationManager(QObject):
    def __init__(
        self,
        parent: QObject,
        refresh_period_in_seconds: int = 5,
        projects: list[Project] | None = None,
        message_bar: QgsMessageBar | None = None,
        issue_clicked_callable: Callable[[Issues], None] | None = None,
    ):
        """Manager for GitLab notification.
        Run periodically check for new and updated issues.

        If no project defined, project use are defined by settings:
        - last used project
        - all available project if no last used project

        :param parent: parent for object delete
        :type parent: QObject
        :param refresh_period_in_seconds: check refresh period in seconds, defaults to 5
        :type refresh_period_in_seconds: int, optional
        :param projects: list of project to check, defaults to None
        :type projects: Optional[List[Project]], optional
        :param message_bar: QgsMessageBar used for notification display, defaults to None
        :type message_bar: Optional[QgsMessageBar], optional
        :param issue_clicked_callable: Callable used to connect to issue display button, defaults to None
        :type issue_clicked_callable: Optional[Callable[[Issues], None]], optional
        """
        super().__init__(parent)

        self.last_check: datetime | None = None

        # Create timer
        self.timer = QTimer()
        self.timer.setInterval(refresh_period_in_seconds * 1000)
        self.timer.timeout.connect(self.check_issues)

        # No task, will be created at first run
        self.task = None

        # Store task parameters
        self.message_bar = message_bar
        self.issue_clicked_callable = issue_clicked_callable

        # Define projects
        self.projects: list[Project] = []

        # Use projects from constructor
        if projects:
            self.projects = projects

        self._define_default_projects()

    def _define_default_projects(self) -> None:
        """Define default projects if no projects already defined"""
        plg_settings = PlgOptionsManager.get_plg_settings()

        # Check if authentification config is defined before settings current project id
        if not plg_settings.authentification_config_id:
            return

        # Last used project
        if len(self.projects) == 0:
            project_manager = ProjectRequestManager()

            project_id = plg_settings.issue_view_last_project_id
            if not project_id:
                project_id = plg_settings.issue_creation_last_project_id

            if project_id:
                if project := project_manager.get_project(project_id=project_id):
                    self.projects = [project]

        # All available project (for now for first selected gitlab group)
        if len(self.projects) == 0:
            projects = project_manager.get_project_list(
                group_id=plg_settings.gitlab_group_list_values[0]
            )

            # Sort project by creation date
            projects.sort(key=lambda p: p.last_activity_at)

            # Only keep 5 projects max
            nb_project = min(5, len(projects))
            self.projects = projects[:nb_project]

    def check_issues(self) -> None:
        """Launch QgsTask to check for new or update issues"""
        plg_settings = PlgOptionsManager.get_plg_settings()

        # Check if authentification config is defined
        if not plg_settings.authentification_config_id:
            return

        try:
            manager = UserRequestManager()
            user = manager.get_current_user()
        except UnavailableUserError:
            return

        excluded_usernames = plg_settings.excluded_username_list_values
        excluded_usernames.append(user.username)

        # Check if notification are enabled
        if plg_settings.enable_notification:
            if self.last_check is None:
                self.last_check = datetime.now(timezone.utc)
            self.task = GitLabIssueChecker(
                self.last_check,
                self.projects,
                excluded_usernames,
                self.message_bar,
                self.issue_clicked_callable,
            )
            QgsApplication.taskManager().addTask(self.task)
            self.last_check = datetime.now(timezone.utc)

        # Check if notification frequency changed
        if plg_settings.notification_frequency != self.timer.interval() * 1000:
            self.timer.setInterval(plg_settings.notification_frequency * 1000)

    def start(self) -> None:
        """Start notifications"""
        self.check_issues()
        self.timer.start()

    def stop(self) -> None:
        """Stop notifications"""
        self.timer.stop()

    def restart(self) -> None:
        """Restart notifications"""
        self.timer.start()
