from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union

import requests

from gitlab import cli, client
from gitlab import exceptions as exc
from gitlab import types
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
    CreateMixin,
    CRUDMixin,
    DeleteMixin,
    ListMixin,
    ObjectDeleteMixin,
    ParticipantsMixin,
    RetrieveMixin,
    SaveMixin,
    SubscribableMixin,
    TimeTrackingMixin,
    TodoMixin,
    UserAgentDetailMixin,
)
from gitlab.types import RequiredOptional

from .award_emojis import ProjectIssueAwardEmojiManager  # noqa: F401
from .discussions import ProjectIssueDiscussionManager  # noqa: F401
from .events import (  # noqa: F401
    ProjectIssueResourceIterationEventManager,
    ProjectIssueResourceLabelEventManager,
    ProjectIssueResourceMilestoneEventManager,
    ProjectIssueResourceStateEventManager,
    ProjectIssueResourceWeightEventManager,
)
from .notes import ProjectIssueNoteManager  # noqa: F401

__all__ = [
    "Issue",
    "IssueManager",
    "GroupIssue",
    "GroupIssueManager",
    "ProjectIssue",
    "ProjectIssueManager",
    "ProjectIssueLink",
    "ProjectIssueLinkManager",
]


class Issue(RESTObject):
    _url = "/issues"
    _repr_attr = "title"


class IssueManager(RetrieveMixin, RESTManager):
    _path = "/issues"
    _obj_cls = Issue
    _list_filters = (
        "state",
        "labels",
        "milestone",
        "scope",
        "author_id",
        "iteration_id",
        "assignee_id",
        "my_reaction_emoji",
        "iids",
        "order_by",
        "sort",
        "search",
        "created_after",
        "created_before",
        "updated_after",
        "updated_before",
    )
    _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}

    def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Issue:
        return cast(Issue, super().get(id=id, lazy=lazy, **kwargs))


class GroupIssue(RESTObject):
    pass


class GroupIssueManager(ListMixin, RESTManager):
    _path = "/groups/{group_id}/issues"
    _obj_cls = GroupIssue
    _from_parent_attrs = {"group_id": "id"}
    _list_filters = (
        "state",
        "labels",
        "milestone",
        "order_by",
        "sort",
        "iids",
        "author_id",
        "iteration_id",
        "assignee_id",
        "my_reaction_emoji",
        "search",
        "created_after",
        "created_before",
        "updated_after",
        "updated_before",
    )
    _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}


class ProjectIssue(
    UserAgentDetailMixin,
    SubscribableMixin,
    TodoMixin,
    TimeTrackingMixin,
    ParticipantsMixin,
    SaveMixin,
    ObjectDeleteMixin,
    RESTObject,
):
    _repr_attr = "title"
    _id_attr = "iid"

    awardemojis: ProjectIssueAwardEmojiManager
    discussions: ProjectIssueDiscussionManager
    links: "ProjectIssueLinkManager"
    notes: ProjectIssueNoteManager
    resourcelabelevents: ProjectIssueResourceLabelEventManager
    resourcemilestoneevents: ProjectIssueResourceMilestoneEventManager
    resourcestateevents: ProjectIssueResourceStateEventManager
    resource_iteration_events: ProjectIssueResourceIterationEventManager
    resource_weight_events: ProjectIssueResourceWeightEventManager

    @cli.register_custom_action(cls_names="ProjectIssue", required=("to_project_id",))
    @exc.on_http_error(exc.GitlabUpdateError)
    def move(self, to_project_id: int, **kwargs: Any) -> None:
        """Move the issue to another project.

        Args:
            to_project_id: ID of the target project
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabUpdateError: If the issue could not be moved
        """
        path = f"{self.manager.path}/{self.encoded_id}/move"
        data = {"to_project_id": to_project_id}
        server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
        if TYPE_CHECKING:
            assert isinstance(server_data, dict)
        self._update_attrs(server_data)

    @cli.register_custom_action(
        cls_names="ProjectIssue", required=("move_after_id", "move_before_id")
    )
    @exc.on_http_error(exc.GitlabUpdateError)
    def reorder(
        self,
        move_after_id: Optional[int] = None,
        move_before_id: Optional[int] = None,
        **kwargs: Any,
    ) -> None:
        """Reorder an issue on a board.

        Args:
            move_after_id: ID of an issue that should be placed after this issue
            move_before_id: ID of an issue that should be placed before this issue
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabUpdateError: If the issue could not be reordered
        """
        path = f"{self.manager.path}/{self.encoded_id}/reorder"
        data: Dict[str, Any] = {}

        if move_after_id is not None:
            data["move_after_id"] = move_after_id
        if move_before_id is not None:
            data["move_before_id"] = move_before_id

        server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs)
        if TYPE_CHECKING:
            assert isinstance(server_data, dict)
        self._update_attrs(server_data)

    @cli.register_custom_action(cls_names="ProjectIssue")
    @exc.on_http_error(exc.GitlabGetError)
    def related_merge_requests(
        self, **kwargs: Any
    ) -> Union[client.GitlabList, List[Dict[str, Any]]]:
        """List merge requests related to the issue.

        Args:
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabGetErrot: If the merge requests could not be retrieved

        Returns:
            The list of merge requests.
        """
        path = f"{self.manager.path}/{self.encoded_id}/related_merge_requests"
        result = self.manager.gitlab.http_list(path, **kwargs)
        if TYPE_CHECKING:
            assert not isinstance(result, requests.Response)
        return result

    @cli.register_custom_action(cls_names="ProjectIssue")
    @exc.on_http_error(exc.GitlabGetError)
    def closed_by(
        self, **kwargs: Any
    ) -> Union[client.GitlabList, List[Dict[str, Any]]]:
        """List merge requests that will close the issue when merged.

        Args:
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabGetErrot: If the merge requests could not be retrieved

        Returns:
            The list of merge requests.
        """
        path = f"{self.manager.path}/{self.encoded_id}/closed_by"
        result = self.manager.gitlab.http_list(path, **kwargs)
        if TYPE_CHECKING:
            assert not isinstance(result, requests.Response)
        return result


class ProjectIssueManager(CRUDMixin, RESTManager):
    _path = "/projects/{project_id}/issues"
    _obj_cls = ProjectIssue
    _from_parent_attrs = {"project_id": "id"}
    _list_filters = (
        "iids",
        "state",
        "labels",
        "milestone",
        "scope",
        "author_id",
        "iteration_id",
        "assignee_id",
        "my_reaction_emoji",
        "order_by",
        "sort",
        "search",
        "created_after",
        "created_before",
        "updated_after",
        "updated_before",
    )
    _create_attrs = RequiredOptional(
        required=("title",),
        optional=(
            "description",
            "confidential",
            "assignee_ids",
            "assignee_id",
            "milestone_id",
            "labels",
            "created_at",
            "due_date",
            "merge_request_to_resolve_discussions_of",
            "discussion_to_resolve",
        ),
    )
    _update_attrs = RequiredOptional(
        optional=(
            "title",
            "description",
            "confidential",
            "assignee_ids",
            "assignee_id",
            "milestone_id",
            "labels",
            "state_event",
            "updated_at",
            "due_date",
            "discussion_locked",
        ),
    )
    _types = {"iids": types.ArrayAttribute, "labels": types.CommaSeparatedListAttribute}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectIssue:
        return cast(ProjectIssue, super().get(id=id, lazy=lazy, **kwargs))


class ProjectIssueLink(ObjectDeleteMixin, RESTObject):
    _id_attr = "issue_link_id"


class ProjectIssueLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
    _path = "/projects/{project_id}/issues/{issue_iid}/links"
    _obj_cls = ProjectIssueLink
    _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"}
    _create_attrs = RequiredOptional(required=("target_project_id", "target_issue_iid"))

    @exc.on_http_error(exc.GitlabCreateError)
    # NOTE(jlvillal): Signature doesn't match CreateMixin.create() so ignore
    # type error
    def create(  # type: ignore
        self, data: Dict[str, Any], **kwargs: Any
    ) -> Tuple[RESTObject, RESTObject]:
        """Create a new object.

        Args:
            data: parameters to send to the server to create the
                         resource
            **kwargs: Extra options to send to the server (e.g. sudo)

        Returns:
            The source and target issues

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabCreateError: If the server cannot perform the request
        """
        self._create_attrs.validate_attrs(data=data)
        if TYPE_CHECKING:
            assert self.path is not None
        server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs)
        if TYPE_CHECKING:
            assert isinstance(server_data, dict)
            assert self._parent is not None
        source_issue = ProjectIssue(self._parent.manager, server_data["source_issue"])
        target_issue = ProjectIssue(self._parent.manager, server_data["target_issue"])
        return source_issue, target_issue
