"""Widget for issue display"""

from datetime import datetime

# standard
from enum import Enum
from pathlib import Path

# PyQGIS
from qgis.core import Qgis, QgsApplication, QgsTask
from qgis.gui import QgsGui
from qgis.PyQt import QtCore, uic
from qgis.PyQt.QtCore import QSize, Qt
from qgis.PyQt.QtGui import QIcon, QMovie, QPixmap
from qgis.PyQt.QtWidgets import QLayout, QWidget

# project
from oslandia.gui.commons.lbl_tag import Tag
from oslandia.gui.helpdesk.mdl_members import MembersListModel
from oslandia.gui.helpdesk.mdl_members_manager import MemberListModelManager
from oslandia.gui.helpdesk.proxy_model_members import MembersProxyModel
from oslandia.gui.helpdesk.wdg_discussion import DiscussionWidget
from oslandia.gui.stylesheet import SPLITTER
from oslandia.helpdesk.discussion import (
    Discussion,
    DiscussionGraphQLRequestManager,
)
from oslandia.helpdesk.graphql_issue import (
    IssuePermissions,
    IssuesGraphQLRequestManager,
)
from oslandia.helpdesk.issue import Issues, IssuesRestRequestManager
from oslandia.helpdesk.labels import LabelsRequestManager
from oslandia.toolbelt.log_handler import PlgLogger
from oslandia.toolbelt.preferences import PlgOptionsManager, PlgSettingsStructure


class IssueWidget(QWidget):
    """QWidget to display issue

    :param parent: dialog parent, defaults to None
    :type parent: Optional[QWidget], optional
    """

    class DisplayMode(Enum):
        READ = 1
        UPDATE = 2
        CREATE = 3

    # Signal to indicate changes in issue displayed
    qsig_issue_changed = QtCore.pyqtSignal()

    # Signal to indicate that the issue was updated
    qsig_issue_updated = QtCore.pyqtSignal(Issues)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        uic.loadUi(Path(__file__).parent / f"{Path(__file__).stem}.ui", self)

        self._user_permission_task: QgsTask | None = None
        self._user_permission: IssuePermissions | None = None

        self.issue = None
        self._project_id = None
        self._project_labels = []

        self.label_manager = LabelsRequestManager()

        self.mdl_members = MembersListModel(self)
        self.mdl_members.sort(MembersListModel.NAME_COL)

        self.proxy_mdl_members = MembersProxyModel(self)
        self.proxy_mdl_members.setSourceModel(self.mdl_members)
        self.proxy_mdl_members.setSortCaseSensitivity(
            Qt.CaseSensitivity.CaseInsensitive
        )
        self.proxy_mdl_members.sort(
            MembersListModel.NAME_COL, Qt.SortOrder.AscendingOrder
        )
        self.cbx_assignee.setModel(self.proxy_mdl_members)

        # QgsCheckableComboBox doesn't support model use

        self.lne_title.textChanged.connect(self.qsig_issue_changed.emit)
        self.txe_description.qsig_text_changed.connect(self.qsig_issue_changed.emit)
        self.cbx_labels.currentIndexChanged.connect(self.qsig_issue_changed.emit)
        self.cbx_assignee.currentIndexChanged.connect(self.qsig_issue_changed.emit)

        self.txe_description.setPlaceholderText(self.tr("Description *"))

        self.wdg_note_creation.qsig_discussion_added.connect(self._add_discussion)

        self.set_display_mode(IssueWidget.DisplayMode.READ)

        self.btn_edit.clicked.connect(self._update_mode)
        self.btn_cancel.clicked.connect(self._cancel_update)
        self.btn_save.clicked.connect(self._update_issue)

        self.btn_edit.setIcon(QgsApplication.getThemeIcon("mActionToggleEditing.svg"))
        self.btn_cancel.setIcon(
            QgsApplication.getThemeIcon("mActionCancelAllEdits.svg")
        )
        self.btn_save.setIcon(QgsApplication.getThemeIcon("mActionSaveAllEdits.svg"))

        # Define icon for button for comment collapse
        self.lbl_comment.setText(self.tr("Comments"))
        icon = QIcon()
        icon.addPixmap(
            QPixmap(":images/themes/default/mIconExpand.svg"),
            mode=QIcon.Mode.Normal,
            state=QIcon.State.Off,
        )
        icon.addPixmap(
            QPixmap(":images/themes/default/mIconCollapse.svg"),
            mode=QIcon.Mode.Normal,
            state=QIcon.State.On,
        )
        self.btn_comment_collapse.setIcon(icon)
        self.btn_comment_collapse.setChecked(True)
        self.scrl_area_comment.setVisible(True)
        self.btn_comment_collapse.clicked.connect(self._update_comment_visibility)

        self._user_permission_task: QgsTask | None = None
        self._discussion_task: QgsTask | None = None
        self._comment_load_movie = QMovie(QgsApplication.iconPath("mIconLoading.gif"))
        self._comment_load_movie.setScaledSize(QSize(24, 24))
        self.lbl_load_comment.setMovie(self._comment_load_movie)
        self.lbl_load_comment.setVisible(False)

        self.scrl_area_comment.viewport().setAutoFillBackground(False)
        self.scrl_area_comment.widget().setAutoFillBackground(False)

        self.splitter.setStyleSheet(SPLITTER)

        instance_gui: QgsGui | None = QgsGui.instance()
        if isinstance(instance_gui, QgsGui):
            instance_gui.optionsChanged.connect(self.update_available_assignee)

    def _update_comment_visibility(self, visible: bool) -> None:
        """Update comment visibility
        - display scroll area content
        - update maximum height for frame

        :param visible: True for comments visible, False otherwise
        :type visible: bool
        """
        self.scrl_area_comment.setVisible(visible)
        margins = self.frm_comment_layout.contentsMargins()
        self.frm_comment.setMaximumHeight(
            16777215
            if visible
            else self.frm_comment_description.geometry().height()
            + margins.top()
            + margins.bottom()
        )

    def update_available_assignee(self) -> None:
        """Update available assignee for issue creation/update"""
        plg_settings = PlgOptionsManager()
        settings: PlgSettingsStructure = plg_settings.get_plg_settings()
        if self.display_mode != IssueWidget.DisplayMode.READ:
            self.proxy_mdl_members.set_excluded_assignee(
                settings.excluded_assignee_list_values
            )
        else:
            self.proxy_mdl_members.set_excluded_assignee(None)

    def _update_mode(self) -> None:
        """Switch to UPDATE mode"""
        self.set_display_mode(IssueWidget.DisplayMode.UPDATE)

    def _cancel_update(self) -> None:
        """Cancel UPDATE mode and restore displayed issue"""
        if self.issue:
            self.set_issue(self.issue)
        self.set_display_mode(IssueWidget.DisplayMode.READ)

    def _update_issue(self) -> None:
        """Update issue with current displayed values"""
        issue = self.get_issue()
        issue_manager = IssuesRestRequestManager()
        issue = issue_manager.update_issue(issue)
        self.issue = issue
        self.set_display_mode(IssueWidget.DisplayMode.READ)
        # Need to update displayed tag from new labels
        self._set_issue_labels(issue.labels)
        # Need to update displayed assignee for new assignee
        self._set_issue_assignee(issue.assignee_id)

        PlgLogger().log(
            message=self.tr("Issue updated."),
            log_level=Qgis.MessageLevel.Success,
            push=True,
            duration=5,
            parent_location=self,
            nb_parent_max_for_message_bar=10,
        )

        self.qsig_issue_updated.emit(issue)

    def is_issue_changed(self) -> bool:
        """Check if displayed issue is changed from input issue

        :return: True if issue was changed, False otherwise
        :rtype: bool
        """
        if self.issue:
            return self.issue != self.get_issue()
        return False

    def get_display_mode(self) -> DisplayMode:
        """Return current display mode

        :return: current display mode
        :rtype: DisplayMode
        """
        return self.display_mode

    def set_display_mode(self, mode: DisplayMode) -> None:
        """Set display mode:
            IssueWidget.DisplayMode.READ : all GUI are read only
            IssueWidget.DisplayMode.UPDATE : user can edit GUI
            IssueWidget.DisplayMode.CREATE : user can edit GUI but comment are not displayed

        :param mode: display mode
        :type mode: DisplayMode
        """
        self.display_mode = mode
        # Read only attribute
        read_only = mode == IssueWidget.DisplayMode.READ
        self.lne_title.setReadOnly(read_only)
        self.txe_description.setReadOnly(read_only)
        self.cbx_labels.setAttribute(
            Qt.WidgetAttribute.WA_TransparentForMouseEvents, read_only
        )
        self.cbx_labels.setFocusPolicy(
            Qt.FocusPolicy.NoFocus if read_only else Qt.FocusPolicy.StrongFocus
        )
        self.cbx_assignee.setAttribute(
            Qt.WidgetAttribute.WA_TransparentForMouseEvents, read_only
        )
        self.cbx_assignee.setFocusPolicy(
            Qt.FocusPolicy.NoFocus if read_only else Qt.FocusPolicy.StrongFocus
        )

        # Display combobox for assignee and tag selection if create mode or update mode and user permission for admin
        can_update = mode == IssueWidget.DisplayMode.CREATE or (
            self._user_permission is not None
            and self._user_permission.admin_issue
            and mode == IssueWidget.DisplayMode.UPDATE
        )

        if can_update:
            self.stacked_assignee.setCurrentWidget(self.page_cbx_assignee)
            self.stacked_labels.setCurrentWidget(self.page_cbx_labels)
        else:
            # Display static tag list widget for read mode
            self.stacked_assignee.setCurrentWidget(self.page_tags_assignee)
            self.stacked_labels.setCurrentWidget(self.page_tags_labels)

        # Don't display comment in CREATE mode
        self.frm_comment.setVisible(mode != IssueWidget.DisplayMode.CREATE)

        self.btn_edit.setVisible(mode == IssueWidget.DisplayMode.READ)
        self.btn_save.setVisible(mode == IssueWidget.DisplayMode.UPDATE)
        self.btn_cancel.setVisible(mode == IssueWidget.DisplayMode.UPDATE)

        # Don't display hints in READ mode
        self.lbl_title_hint.setVisible(mode != IssueWidget.DisplayMode.READ)
        self.lbl_description_hint.setVisible(mode != IssueWidget.DisplayMode.READ)

        # Don't allow selection of excluded assignee if not READ mode
        self.update_available_assignee()

    def set_current_project_id(self, project_id: int) -> None:
        """Define current project to define labels and members available

        :param project_id: _description_
        :type project_id: int
        """
        if self._project_id != project_id:
            # Save project labels for issue label display
            self._project_labels = self.label_manager.get_labels_list(
                project_id=project_id
            )
            self.cbx_labels.clear()
            for label in self._project_labels:
                self.cbx_labels.addItemWithCheckState(
                    label.name, Qt.CheckState.Unchecked
                )

            self.mdl_members = MemberListModelManager.get_model_for_project(project_id)
            self.proxy_mdl_members.setSourceModel(self.mdl_members)

            # No assignee selected by defaults
            self._unselect_assignee()

            self.proxy_mdl_members.sort(
                MembersListModel.NAME_COL, Qt.SortOrder.AscendingOrder
            )
            self.txe_description.set_project_id(project_id)

    def _unselect_assignee(self) -> None:
        """Unselect assigne by selecting None member"""
        self.mdl_members.wait_for_data_load()
        for row in range(self.mdl_members.rowCount()):
            index = self.proxy_mdl_members.index(row, self.mdl_members.NAME_COL)
            member = self.proxy_mdl_members.data(
                index,
                Qt.ItemDataRole.UserRole,
            )
            if member is None:
                self.cbx_assignee.setCurrentIndex(row)
                break

    @staticmethod
    def clear_layout(layout: QLayout) -> None:
        """Clear a layout from all added widget

        :param layout: layout to clear
        :type layout: any Qt type layouy
        """
        if layout is not None:
            while layout.count():
                item = layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
                else:
                    layout.removeItem(item)

    def _set_issue_labels(self, labels: list[str]) -> None:
        """Define issue labels in label tag widget
        - get project label for issue label name
        - use project label color and text color for tag creation

        :param labels: list of label for issue
        :type labels: List[str]
        """
        tags = []
        for label_name in labels:
            project_label = next(
                (label for label in self._project_labels if label.name == label_name),
                None,
            )
            if project_label:
                color = project_label.color
                text_color = project_label.text_color
                tooltip = project_label.description_html
            else:
                color = "transparent"
                text_color = "black"
                tooltip = None
            tags.append(
                Tag(
                    name=label_name, color=color, text_color=text_color, tooltip=tooltip
                )
            )
        self.wdg_label_tags.set_tag_list(tags)

    def _set_issue_assignee(self, assignee_id: int | None) -> None:
        """Define issue assignee in assignee tag widget and assignee combobox

        :param assignee_id: assignee id, None if no assignee
        :type assignee_id: Optional[int]
        """
        tags = []
        self.mdl_members.wait_for_data_load()
        for row in range(self.mdl_members.rowCount()):
            index = self.proxy_mdl_members.index(row, self.mdl_members.NAME_COL)
            member = self.proxy_mdl_members.data(
                index,
                Qt.ItemDataRole.UserRole,
            )
            if assignee_id:
                if member and member.id == assignee_id:
                    tags.append(
                        Tag(
                            name=member.name,
                            color="white",
                            text_color="black",
                            border_color="black",
                            tooltip=member.username,
                        )
                    )
                    self.cbx_assignee.setCurrentIndex(row)
                    break
            elif member is None:
                self.cbx_assignee.setCurrentIndex(row)
                break
        self.wdg_assignee_tags.set_tag_list(tags)

    def set_issue(self, issue: Issues) -> None:
        """Set issue displayed

        :param issue: issue
        :type issue: Issues
        """
        self.issue = issue
        self.set_current_project_id(issue.project_id)

        self.lne_title.setText(issue.name)
        self.txe_description.setText(issue.description)
        self.txe_description.set_project_id(issue.project_id)

        self.cbx_labels.deselectAllOptions()
        self.cbx_labels.setCheckedItems(issue.labels)

        self._set_issue_labels(issue.labels)
        self._set_issue_assignee(issue.assignee_id)

        # Disable edition button
        self.btn_edit.setEnabled(False)
        self.btn_edit.setToolTip(self.tr("Waiting for user permission definition"))

        self._user_permission_task = QgsTask.fromFunction(
            self.tr("Read issue permission for user"),
            self._get_user_permission,
            on_finished=self._user_permission_read,
            flags=QgsTask.Flag.Silent,
        )

        QgsApplication.taskManager().addTask(self._user_permission_task)

        # A discussions as note only if not in CREATE mode
        if self.display_mode != IssueWidget.DisplayMode.CREATE:
            self._refresh_notes()

        self.wdg_note_creation.set_issue(issue)

    def _get_user_permission(self, task: QgsTask | None) -> IssuePermissions | None:
        """Get user permission in a QgsTask

        :param task: task for background call
        :type task: QgsTask | None
        :return: _description_
        :rtype: IssuePermissions | None
        """
        if self.issue is not None and self.issue.project_full_path is not None:
            manager = IssuesGraphQLRequestManager()
            return manager.get_issue_user_permission(
                project_path=self.issue.project_full_path, issue_iid=self.issue.iid
            )
        return None

    def _user_permission_read(
        self, exception: Exception | None, result: IssuePermissions | None = None
    ) -> None:
        """Function call after QgsTask finished for user permission read

        :param exception: exception raised during task, None if no exception raised
        :type exception: Exception | None
        :param result: result of user permission load, None in case of error during request
        :type result: IssuePermissions | None
        """
        self._user_permission = result
        if result is not None:
            self.btn_edit.setEnabled(result.update_issue)
            self.btn_edit.setToolTip(
                ""
                if result.update_issue
                else self.tr("You don't have the permission to edit this issue")
            )
        else:
            self.btn_edit.setEnabled(False)
            self.btn_edit.setToolTip(
                self.tr("Can't define user permission for this issue")
            )

    def _refresh_notes(self) -> None:
        """Refresh notes for current displayed issue"""
        self.clear_layout(self.lyt_notes)
        if self.issue:
            self.lbl_load_comment.setVisible(True)
            self._comment_load_movie.start()

            self._discussion_task = QgsTask.fromFunction(
                self.tr("Read issue discussion"),
                self._get_issue_discussion,
                on_finished=self._issue_discussion_read,
                flags=QgsTask.Flag.Silent,
                after=None,
            )

            QgsApplication.taskManager().addTask(self._discussion_task)

    def _get_issue_discussion(
        self, task: QgsTask | None, after: str | None
    ) -> tuple[list[Discussion], str | None] | None:
        """Get issue discussion for a graphQL cursor in a QgsTask

        :param task: task for background call
        :type task: QgsTask | None
        :param after: graphQL cursor, default None for first page
        :type after: str | None
        :return: tuple of discussion list and next cursor, None in case of errors
        :rtype: tuple[list[Discussion], str | None] | None
        """
        if self.issue is not None and self.issue.project_full_path is not None:
            discussion_manager = DiscussionGraphQLRequestManager()
            return discussion_manager.get_issue_discussions_for_cursor(
                project_path=self.issue.project_full_path,
                issue_iid=self.issue.iid,
                after=after,
            )
        return [], None

    def _issue_discussion_read(
        self,
        exception: Exception | None,
        result: tuple[list[Discussion], str | None] | None,
    ) -> None:
        """Function call after QgsTask finished for issue discussion load

        :param exception: exception raised during task, None if no exception raised
        :type exception: Optional[Exception]
        :param result: result of discussion load, None in case of error during request
        :type result: tuple[list[Discussion], str | None] | None
        """
        if result is not None:
            discussions, after = result
            for discussion in discussions:
                self._add_discussion(discussion)
            if after is not None:
                self._discussion_task = QgsTask.fromFunction(
                    self.tr("Read issue discussion"),
                    self._get_issue_discussion,
                    on_finished=self._issue_discussion_read,
                    flags=QgsTask.Flag.Silent,
                    after=after,
                )
                QgsApplication.taskManager().addTask(self._discussion_task)
            else:
                self.lbl_load_comment.setVisible(False)
                self._comment_load_movie.stop()

    def _add_discussion(self, discussion: Discussion) -> None:
        """Add a discussion to notes layout

        :param discussion: discussion to add
        :type discussion: Discussion
        """
        # For now only display discussion that are not system notes
        not_system_note = [note for note in discussion.notes if not note.system]
        if len(not_system_note) != 0:
            discussion_widget = DiscussionWidget(self)
            discussion_widget.set_discussion(discussion)
            self.lyt_notes.addWidget(discussion_widget)
            self.lbl_comment.setText(
                self.tr("{} comments").format(self.lyt_notes.count())
            )

    def _selected_assignee_id(self) -> int | None:
        """Get selected assignee id

        :return: selected assignee id
        :rtype: Optional[int]
        """
        assignee_current_index = self.cbx_assignee.currentIndex()
        if assignee_current_index != -1:
            member = self.mdl_members.data(
                self.proxy_mdl_members.mapToSource(
                    self.proxy_mdl_members.index(
                        assignee_current_index, MembersListModel.NAME_COL
                    )
                ),
                Qt.ItemDataRole.UserRole,
            )
            if member:
                return member.id
        return None

    def get_issue(self) -> Issues:
        """Get issue displayed

        :return: issue
        :rtype: Issues
        """
        return Issues(
            id=self.issue.id if self.issue else "",
            project_id=self.issue.project_id if self.issue else "",
            name=self.lne_title.text(),
            description=self.txe_description.toPlainText(),
            state=self.issue.state if self.issue else "",
            created_date=self.issue.created_date if self.issue else datetime.now(),
            updated_at=self.issue.updated_at if self.issue else datetime.now(),
            assignee_id=self._selected_assignee_id(),
            labels=self.cbx_labels.checkedItems(),
            web_url=self.issue.web_url if self.issue else "",
            iid=self.issue.iid if self.issue else "",
        )

    def clear_issue(self) -> None:
        """Clear title, description, labels and assignment fields."""
        self.lne_title.setText("")
        self.txe_description.setText("")
        self.cbx_labels.setCheckedItems([])
        self.cbx_assignee.setModel(self.proxy_mdl_members)
