from __future__ import annotations

from typing import Any, BinaryIO, TYPE_CHECKING

import requests

import gitlab
from gitlab import cli
from gitlab import exceptions as exc
from gitlab import types
from gitlab.base import RESTObject, TObjCls
from gitlab.mixins import (
    CreateMixin,
    CRUDMixin,
    DeleteMixin,
    ListMixin,
    NoUpdateMixin,
    ObjectDeleteMixin,
    SaveMixin,
)
from gitlab.types import RequiredOptional

from .access_requests import GroupAccessRequestManager  # noqa: F401
from .audit_events import GroupAuditEventManager  # noqa: F401
from .badges import GroupBadgeManager  # noqa: F401
from .boards import GroupBoardManager  # noqa: F401
from .branches import GroupProtectedBranchManager  # noqa: F401
from .clusters import GroupClusterManager  # noqa: F401
from .container_registry import GroupRegistryRepositoryManager  # noqa: F401
from .custom_attributes import GroupCustomAttributeManager  # noqa: F401
from .deploy_tokens import GroupDeployTokenManager  # noqa: F401
from .epics import GroupEpicManager  # noqa: F401
from .export_import import GroupExportManager, GroupImportManager  # noqa: F401
from .group_access_tokens import GroupAccessTokenManager  # noqa: F401
from .hooks import GroupHookManager  # noqa: F401
from .invitations import GroupInvitationManager  # noqa: F401
from .issues import GroupIssueManager  # noqa: F401
from .iterations import GroupIterationManager  # noqa: F401
from .labels import GroupLabelManager  # noqa: F401
from .member_roles import GroupMemberRoleManager  # noqa: F401
from .members import (  # noqa: F401
    GroupBillableMemberManager,
    GroupMemberAllManager,
    GroupMemberManager,
)
from .merge_request_approvals import GroupApprovalRuleManager
from .merge_requests import GroupMergeRequestManager  # noqa: F401
from .milestones import GroupMilestoneManager  # noqa: F401
from .notification_settings import GroupNotificationSettingsManager  # noqa: F401
from .packages import GroupPackageManager  # noqa: F401
from .projects import GroupProjectManager, SharedProjectManager  # noqa: F401
from .push_rules import GroupPushRulesManager
from .runners import GroupRunnerManager  # noqa: F401
from .service_accounts import GroupServiceAccountManager  # noqa: F401
from .statistics import GroupIssuesStatisticsManager  # noqa: F401
from .variables import GroupVariableManager  # noqa: F401
from .wikis import GroupWikiManager  # noqa: F401

__all__ = [
    "Group",
    "GroupManager",
    "GroupDescendantGroup",
    "GroupDescendantGroupManager",
    "GroupLDAPGroupLink",
    "GroupLDAPGroupLinkManager",
    "GroupSubgroup",
    "GroupSubgroupManager",
    "GroupSAMLGroupLink",
    "GroupSAMLGroupLinkManager",
]


class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
    _repr_attr = "name"

    access_tokens: GroupAccessTokenManager
    accessrequests: GroupAccessRequestManager
    approval_rules: GroupApprovalRuleManager
    audit_events: GroupAuditEventManager
    badges: GroupBadgeManager
    billable_members: GroupBillableMemberManager
    boards: GroupBoardManager
    clusters: GroupClusterManager
    customattributes: GroupCustomAttributeManager
    deploytokens: GroupDeployTokenManager
    descendant_groups: GroupDescendantGroupManager
    epics: GroupEpicManager
    exports: GroupExportManager
    hooks: GroupHookManager
    imports: GroupImportManager
    invitations: GroupInvitationManager
    issues: GroupIssueManager
    issues_statistics: GroupIssuesStatisticsManager
    iterations: GroupIterationManager
    labels: GroupLabelManager
    ldap_group_links: GroupLDAPGroupLinkManager
    member_roles: GroupMemberRoleManager
    members: GroupMemberManager
    members_all: GroupMemberAllManager
    mergerequests: GroupMergeRequestManager
    milestones: GroupMilestoneManager
    notificationsettings: GroupNotificationSettingsManager
    packages: GroupPackageManager
    projects: GroupProjectManager
    shared_projects: SharedProjectManager
    protectedbranches: GroupProtectedBranchManager
    pushrules: GroupPushRulesManager
    registry_repositories: GroupRegistryRepositoryManager
    runners: GroupRunnerManager
    subgroups: GroupSubgroupManager
    variables: GroupVariableManager
    wikis: GroupWikiManager
    saml_group_links: GroupSAMLGroupLinkManager
    service_accounts: GroupServiceAccountManager

    @cli.register_custom_action(cls_names="Group", required=("project_id",))
    @exc.on_http_error(exc.GitlabTransferProjectError)
    def transfer_project(self, project_id: int, **kwargs: Any) -> None:
        """Transfer a project to this group.

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

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabTransferProjectError: If the project could not be transferred
        """
        path = f"/groups/{self.encoded_id}/projects/{project_id}"
        self.manager.gitlab.http_post(path, **kwargs)

    @cli.register_custom_action(cls_names="Group", required=(), optional=("group_id",))
    @exc.on_http_error(exc.GitlabGroupTransferError)
    def transfer(self, group_id: int | None = None, **kwargs: Any) -> None:
        """Transfer the group to a new parent group or make it a top-level group.

        Requires GitLab ≥14.6.

        Args:
            group_id: ID of the new parent group. When not specified,
                the group to transfer is instead turned into a top-level group.
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabGroupTransferError: If the group could not be transferred
        """
        path = f"/groups/{self.encoded_id}/transfer"
        post_data = {}
        if group_id is not None:
            post_data["group_id"] = group_id
        self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)

    @cli.register_custom_action(cls_names="Group", required=("scope", "search"))
    @exc.on_http_error(exc.GitlabSearchError)
    def search(
        self, scope: str, search: str, **kwargs: Any
    ) -> gitlab.GitlabList | list[dict[str, Any]]:
        """Search the group resources matching the provided string.

        Args:
            scope: Scope of the search
            search: Search string
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabSearchError: If the server failed to perform the request

        Returns:
            A list of dicts describing the resources found.
        """
        data = {"scope": scope, "search": search}
        path = f"/groups/{self.encoded_id}/search"
        return self.manager.gitlab.http_list(path, query_data=data, **kwargs)

    @cli.register_custom_action(cls_names="Group")
    @exc.on_http_error(exc.GitlabCreateError)
    def ldap_sync(self, **kwargs: Any) -> None:
        """Sync LDAP groups.

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

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabCreateError: If the server cannot perform the request
        """
        path = f"/groups/{self.encoded_id}/ldap_sync"
        self.manager.gitlab.http_post(path, **kwargs)

    @cli.register_custom_action(
        cls_names="Group",
        required=("group_id", "group_access"),
        optional=("expires_at",),
    )
    @exc.on_http_error(exc.GitlabCreateError)
    def share(
        self,
        group_id: int,
        group_access: int,
        expires_at: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Share the group with a group.

        Args:
            group_id: ID of the group.
            group_access: Access level for the group.
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabCreateError: If the server failed to perform the request

        Returns:
            Group
        """
        path = f"/groups/{self.encoded_id}/share"
        data = {
            "group_id": group_id,
            "group_access": group_access,
            "expires_at": expires_at,
        }
        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="Group", required=("group_id",))
    @exc.on_http_error(exc.GitlabDeleteError)
    def unshare(self, group_id: int, **kwargs: Any) -> None:
        """Delete a shared group link within a group.

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

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabDeleteError: If the server failed to perform the request
        """
        path = f"/groups/{self.encoded_id}/share/{group_id}"
        self.manager.gitlab.http_delete(path, **kwargs)

    @cli.register_custom_action(cls_names="Group")
    @exc.on_http_error(exc.GitlabRestoreError)
    def restore(self, **kwargs: Any) -> None:
        """Restore a  group marked for deletion..

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

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabRestoreError: If the server failed to perform the request
        """
        path = f"/groups/{self.encoded_id}/restore"
        self.manager.gitlab.http_post(path, **kwargs)


class GroupManager(CRUDMixin[Group]):
    _path = "/groups"
    _obj_cls = Group
    _list_filters = (
        "skip_groups",
        "all_available",
        "search",
        "order_by",
        "sort",
        "statistics",
        "owned",
        "with_custom_attributes",
        "min_access_level",
        "top_level_only",
    )
    _create_attrs = RequiredOptional(
        required=("name", "path"),
        optional=(
            "description",
            "membership_lock",
            "visibility",
            "share_with_group_lock",
            "require_two_factor_authentication",
            "two_factor_grace_period",
            "project_creation_level",
            "auto_devops_enabled",
            "subgroup_creation_level",
            "emails_disabled",
            "avatar",
            "mentions_disabled",
            "lfs_enabled",
            "request_access_enabled",
            "parent_id",
            "default_branch_protection",
            "shared_runners_minutes_limit",
            "extra_shared_runners_minutes_limit",
        ),
    )
    _update_attrs = RequiredOptional(
        optional=(
            "name",
            "path",
            "description",
            "membership_lock",
            "share_with_group_lock",
            "visibility",
            "require_two_factor_authentication",
            "two_factor_grace_period",
            "project_creation_level",
            "auto_devops_enabled",
            "subgroup_creation_level",
            "emails_disabled",
            "avatar",
            "mentions_disabled",
            "lfs_enabled",
            "request_access_enabled",
            "default_branch_protection",
            "file_template_project_id",
            "shared_runners_minutes_limit",
            "extra_shared_runners_minutes_limit",
            "prevent_forking_outside_group",
            "shared_runners_setting",
        )
    )
    _types = {"avatar": types.ImageAttribute, "skip_groups": types.ArrayAttribute}

    @exc.on_http_error(exc.GitlabImportError)
    def import_group(
        self,
        file: BinaryIO,
        path: str,
        name: str,
        parent_id: int | str | None = None,
        **kwargs: Any,
    ) -> dict[str, Any] | requests.Response:
        """Import a group from an archive file.

        Args:
            file: Data or file object containing the group
            path: The path for the new group to be imported.
            name: The name for the new group.
            parent_id: ID of a parent group that the group will
                be imported into.
            **kwargs: Extra options to send to the server (e.g. sudo)

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabImportError: If the server failed to perform the request

        Returns:
            A representation of the import status.
        """
        files = {"file": ("file.tar.gz", file, "application/octet-stream")}
        data: dict[str, Any] = {"path": path, "name": name}
        if parent_id is not None:
            data["parent_id"] = parent_id

        return self.gitlab.http_post(
            "/groups/import", post_data=data, files=files, **kwargs
        )


class SubgroupBaseManager(ListMixin[TObjCls]):
    _from_parent_attrs = {"group_id": "id"}
    _list_filters = (
        "skip_groups",
        "all_available",
        "search",
        "order_by",
        "sort",
        "statistics",
        "owned",
        "with_custom_attributes",
        "min_access_level",
    )
    _types = {"skip_groups": types.ArrayAttribute}


class GroupSubgroup(RESTObject):
    pass


class GroupSubgroupManager(SubgroupBaseManager[GroupSubgroup]):
    _path = "/groups/{group_id}/subgroups"
    _obj_cls = GroupSubgroup


class GroupDescendantGroup(RESTObject):
    pass


class GroupDescendantGroupManager(SubgroupBaseManager[GroupDescendantGroup]):
    """
    This manager inherits from GroupSubgroupManager as descendant groups
    share all attributes with subgroups, except the path and object class.
    """

    _path = "/groups/{group_id}/descendant_groups"
    _obj_cls = GroupDescendantGroup


class GroupLDAPGroupLink(RESTObject):
    _repr_attr = "provider"

    def _get_link_attrs(self) -> dict[str, str]:
        # https://docs.gitlab.com/ee/api/groups.html#add-ldap-group-link-with-cn-or-filter
        # https://docs.gitlab.com/ee/api/groups.html#delete-ldap-group-link-with-cn-or-filter
        # We can tell what attribute to use based on the data returned
        data = {"provider": self.provider}
        if self.cn:
            data["cn"] = self.cn
        else:
            data["filter"] = self.filter

        return data

    def delete(self, **kwargs: Any) -> None:
        """Delete the LDAP group link from the server.

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

        Raises:
            GitlabAuthenticationError: If authentication is not correct
            GitlabDeleteError: If the server cannot perform the request
        """
        if TYPE_CHECKING:
            assert isinstance(self.manager, DeleteMixin)
        self.manager.delete(
            self.encoded_id, query_data=self._get_link_attrs(), **kwargs
        )


class GroupLDAPGroupLinkManager(
    ListMixin[GroupLDAPGroupLink],
    CreateMixin[GroupLDAPGroupLink],
    DeleteMixin[GroupLDAPGroupLink],
):
    _path = "/groups/{group_id}/ldap_group_links"
    _obj_cls = GroupLDAPGroupLink
    _from_parent_attrs = {"group_id": "id"}
    _create_attrs = RequiredOptional(
        required=("provider", "group_access"), exclusive=("cn", "filter")
    )


class GroupSAMLGroupLink(ObjectDeleteMixin, RESTObject):
    _id_attr = "name"
    _repr_attr = "name"


class GroupSAMLGroupLinkManager(NoUpdateMixin[GroupSAMLGroupLink]):
    _path = "/groups/{group_id}/saml_group_links"
    _obj_cls = GroupSAMLGroupLink
    _from_parent_attrs = {"group_id": "id"}
    _create_attrs = RequiredOptional(required=("saml_group_name", "access_level"))
