# standard
from dataclasses import dataclass
from datetime import datetime

# plugin
from oslandia.gitlab_api.issue import Issues
from oslandia.gitlab_graphql.graphql_manager import GraphQLRequestManager


@dataclass
class IssuePermissions:
    update_issue: bool = False
    admin_issue: bool = False
    create_note: bool = False
    reopen_issue: bool = False


class IssuesGraphQLRequestManager(GraphQLRequestManager):
    """
    Fetches issues for a specific GitLab project using the GraphQL API.
    """

    def __init__(self, page_size: int = 5) -> None:
        """Constructor

        :param page_size: page size, defaults to 5
        :type page_size: int, optional
        """
        super().__init__()
        self.page_size = page_size

    def get_issue_user_permission(
        self, project_path: str, issue_iid: int
    ) -> IssuePermissions | None:
        """Get user permission for an issue

        :param project_path: full project path
        :type project_path: str
        :param issue_iid: issue internal id
        :type issue_iid: int
        :return: user permission for issue, None in case of errors
        :rtype: IssuePermissions | None
        """

        query = """
        query ($fullPath: ID!, $issue_iid: String) {
          project(fullPath: $fullPath) {
            issue(iid: $issue_iid) {
              userPermissions {
                  updateIssue
                  adminIssue
                  createNote
                  reopenIssue
              }
            }
          }
        }
        """

        # Define GraphQL variable
        variables = {
            "fullPath": project_path,
            "issue_iid": str(issue_iid),
        }
        user_permission: IssuePermissions | None = None
        if response := self._post_query(query, variables):
            project_data = response["data"]["project"]
            if project_data is not None:
                issue_data = project_data["issue"]
                user_permission_data = issue_data["userPermissions"]
                return IssuePermissions(
                    update_issue=user_permission_data["updateIssue"],
                    admin_issue=user_permission_data["adminIssue"],
                    create_note=user_permission_data["createNote"],
                    reopen_issue=user_permission_data["reopenIssue"],
                )

        return user_permission

    def get_updated_issues_list(
        self,
        project_path: str,
        updated_after: datetime,
        excluded_usernames: list[str] | None = None,
    ) -> list[Issues]:
        """Get issues updated after a datetime

        :param project_path: project full path for GraphQL
        :type project_path: str
        :param updated_after: datetime for update
        :type updated_after: datetime
        :param excluded_usernames: list of username to exclude, Default None
        :type excluded_usernames: Optional[List[str]]
        :return: list of issue updated after datetime
        :rtype: List[Issues]
        """

        issues, after = self.get_updated_issues_list_for_cursor(
            after=None,
            project_path=project_path,
            updated_after=updated_after,
            excluded_usernames=excluded_usernames,
        )

        while after is not None:
            page_issue_list, after = self.get_updated_issues_list_for_cursor(
                after=after,
                project_path=project_path,
                updated_after=updated_after,
                excluded_usernames=excluded_usernames,
            )
            issues.extend(page_issue_list)
        return issues

    def get_updated_issues_list_for_cursor(
        self,
        after: str | None,
        project_path: str,
        updated_after: datetime,
        excluded_usernames: list[str] | None = None,
    ) -> tuple[list[Issues], str | None]:
        """Get issues updated after a datetime for a GraphQL cursor
        :param after : GraphQL cursor, None for first page
        :type after :Optional[str]
        :param project_path: project full path for GraphQL
        :type project_path: str
        :param updated_after: datetime for update
        :type updated_after: datetime
        :param excluded_usernames: list of username to exclude, Default None
        :type excluded_usernames: Optional[List[str]]
        :return: list of issue updated after datetime  and next GraphQL cursor
        :rtype: tuple[list[Issues], str | None]
        """

        # Define GraphQL Query: filter on updatedAfter and get only opened issues
        query = """
        query ($fullPath: ID!, $after: String, $updatedAfter: Time, $pageSize : Int) {
          project(fullPath: $fullPath) {
            id
            issues(first: $pageSize,
                   after: $after,
                   updatedAfter: $updatedAfter,
                   state : opened
            ) {
              pageInfo {
                hasNextPage
                endCursor
              }
              nodes {
                id
                iid
                title
                createdAt
                updatedAt
                discussions {
                  nodes {
                    notes {
                      nodes {
                        createdAt
                        lastEditedAt
                        author{
                          username
                        }
                        lastEditedBy {
                          username
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
        """

        # Define GraphQL variable
        variables = {
            "fullPath": project_path,
            "after": after,
            "updatedAfter": updated_after.isoformat(),
            "pageSize": self.page_size,
        }

        issues: list[Issues] = []
        after = None
        if response := self._post_query(query, variables):
            project_data = response["data"]["project"]
            if project_data is not None:
                # Define project id for REST API from id
                project_id = int(project_data["id"].split("/")[-1])
                issue_data = project_data["issues"]

                for raw in issue_data["nodes"]:
                    # Don't duplicate issue just created
                    if raw["createdAt"] == raw["updatedAt"]:
                        continue

                    issue_update_date_str = raw["updatedAt"]

                    checked_username = None

                    # Define username from discussion
                    for discussion in raw["discussions"]["nodes"]:
                        note_updated = False
                        for note in discussion["notes"]["nodes"]:
                            check_date_str = note["lastEditedAt"]

                            # This note was the reason the issue was updated
                            # This can be a note for issue description update or for comment update/creation
                            if issue_update_date_str == check_date_str:
                                if check_date_str == note["createdAt"]:
                                    checked_username = note["author"]["username"]
                                else:
                                    checked_username = note["lastEditedBy"]["username"]

                                note_updated = True
                                break
                        # Note found for update date, no need to check other discussions
                        if note_updated:
                            break

                    # Filter username
                    if (
                        checked_username is not None
                        and excluded_usernames
                        and checked_username in excluded_usernames
                    ):
                        continue

                    issue = self._issue_from_json(raw, project_id)
                    issues.append(issue)

                after = self._get_next_cursor(issue_data)

        return issues, after

    def get_new_issues_list(
        self,
        project_path: str,
        created_after: datetime,
        excluded_usernames: list[str] | None = None,
    ) -> list[Issues]:
        """Get issues created after a datetime

        :param project_path: project full path for GraphQL
        :type project_path: str
        :param created_after: datetime for creation
        :type created_after: datetime
        :param excluded_usernames: list of username to exclude, Default None
        :type excluded_usernames: Optional[List[str]]
        :return: list of issue created after datetime
        :rtype: List[Issues]
        """

        issues, after = self.get_new_issues_list_for_cursor(
            after=None,
            project_path=project_path,
            created_after=created_after,
            excluded_usernames=excluded_usernames,
        )

        while after is not None:
            page_issue_list, after = self.get_new_issues_list_for_cursor(
                after=after,
                project_path=project_path,
                created_after=created_after,
                excluded_usernames=excluded_usernames,
            )
            issues.extend(page_issue_list)
        return issues

    def get_new_issues_list_for_cursor(
        self,
        after: str | None,
        project_path: str,
        created_after: datetime,
        excluded_usernames: list[str] | None = None,
    ) -> tuple[list[Issues], str | None]:
        """Get issues updated after a datetime for a GraphQL cursor
        :param after : GraphQL cursor, None for first page
        :type after :Optional[str]
        :param project_path: project full path for GraphQL
        :type project_path: str
        :param updated_after: datetime for update
        :type updated_after: datetime
        :param excluded_usernames: list of username to exclude, Default None
        :type excluded_usernames: Optional[List[str]]
        :return: list of issue updated after datetime  and next GraphQL cursor
        :rtype: tuple[list[Issues], str | None]
        """

        # Define GraphQL Query: filter on createdAfter and get only opened issues
        query = """
        query ($fullPath: ID!, $after: String, $createdAfter: Time, $pageSize : Int, $authorUsername : [String!]) {
          project(fullPath: $fullPath) {
            id
            issues(first: $pageSize,
                   after: $after,
                   createdAfter: $createdAfter,
                   state : opened,
                   not : {authorUsername: $authorUsername}) {
              pageInfo {
                hasNextPage
                endCursor
              }
              nodes {
                id
                iid
                title
                createdAt
              }
            }
          }
        }
        """

        # Define GraphQL variable

        variables = {
            "fullPath": project_path,
            "after": after,
            "createdAfter": created_after.isoformat(),
            "pageSize": self.page_size,
            "authorUsername": excluded_usernames,
        }
        issues: list[Issues] = []
        after = None
        if response := self._post_query(query, variables):
            project_data = response["data"]["project"]
            if project_data is not None:
                # Define project id for REST API from id
                project_id = int(project_data["id"].split("/")[-1])
                issue_data = project_data["issues"]

                for raw in issue_data["nodes"]:
                    issue = self._issue_from_json(raw, project_id)
                    issues.append(issue)

                after = self._get_next_cursor(issue_data)

        return issues, after

    @staticmethod
    def _issue_from_json(raw: dict, project_id: int) -> Issues:
        """Create an issue from json value

        :param raw: raw json value
        :type raw: dict
        :param project_id: issue project id (not available in raw data)
        :type project_id: int
        :return: created issue
        :rtype: Issues
        """
        # Define issue id for REST API from id
        issue_id = int(raw["id"].split("/")[-1])
        return Issues(
            id=issue_id,
            iid=int(raw["iid"]),
            project_id=project_id,
            name=raw["title"],
            created_date=datetime.fromisoformat(raw["createdAt"]),
        )
