"""
Base classes for role assignment pattern.

Provides reusable abstractions for resources that follow the
principal+role assignment model (Connections, Workspaces, etc.).
"""

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

from .enums import PrincipalType

if TYPE_CHECKING:
    from ._client import FabricClient


class BaseRoleAssignment:
    """
    Base class for role assignments with principal+role structure.

    This class provides the common structure and behavior for role assignments
    across different resource types (connections, workspaces, etc.).

    Attributes:
        principal (dict): Principal information with id, type, and optional displayName
        role (str): Assigned role (resource-specific enum)
    """

    def __init__(self, data: Dict[str, Any], client: Optional["FabricClient"] = None):
        """
        Initialize from API response data.

        Args:
            data: Role assignment response from API
            client: Optional FabricClient for resolving principal names
        """
        self.id = data.get("id")  # Role assignment ID (for delete operations)
        self.principal = data.get("principal", {})
        self.role = data.get("role")
        self._client = client
        self._principal_name_cached = None

    @property
    def principalId(self) -> str:
        """Get the principal's ID."""
        return self.principal.get("id")

    @property
    def principalType(self) -> str:
        """Get the principal type (User, Group, ServicePrincipal, etc.)."""
        return self.principal.get("type")

    @property
    def principalName(self) -> Optional[str]:
        """
        Get the principal's display name.

        Resolves the name via Microsoft Graph API if not cached.
        Returns None if resolution fails or requires elevated permissions.
        """
        # Check if displayName is already in the response
        if "displayName" in self.principal:
            return self.principal["displayName"]

        # Check cache
        if self._principal_name_cached is not None:
            return self._principal_name_cached

        # Resolve via Graph API if client is available
        if self._client and self.principalId and self.principalType:
            from .._shared.graph import GraphHelper

            graph = GraphHelper(self._client._auth)
            self._principal_name_cached = graph.getPrincipalName(
                self.principalId, self.principalType
            )
            return self._principal_name_cached

        return None

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"id={self.id}, "
            f"principal={self.principalName or self.principalId}, "
            f"role={self.role})"
        )


class BaseRoleAssignmentAccessor:
    """
    Base accessor for role assignment CRUD operations.

    Provides standard list, add, update, delete operations for resources
    that follow the principal+role assignment pattern.
    """

    def __init__(self, client: "FabricClient", endpoint: str, assignment_class: type):
        """
        Initialize role assignment accessor.

        Args:
            client: Authenticated FabricClient
            endpoint: API endpoint for role assignments (e.g., "/connections/{id}/roleAssignments")
            assignment_class: Class to instantiate for role assignments
        """
        self._client = client
        self._endpoint = endpoint
        self._assignment_class = assignment_class

    def __call__(self) -> List[BaseRoleAssignment]:
        """
        List all role assignments.

        Returns:
            List[BaseRoleAssignment]: List of all role assignment objects
        """
        # Get all role assignments (pagination handled automatically by client)
        response = self._client.get(self._endpoint)
        data = response.json()

        return [self._assignment_class(item, client=self._client) for item in data.get("value", [])]

    def add(  # pragma: no cover
        self, principal_id: str, principal_type: Union[PrincipalType, str], role: str
    ) -> BaseRoleAssignment:
        """
        Add a role assignment.

        If the role assignment already exists (HTTP 409), returns the existing assignment.

        Args:
            principal_id: Principal GUID (user, group, or service principal)
            principal_type: Type - can be PrincipalType enum or string:
                - PrincipalType.USER or "User"
                - PrincipalType.GROUP or "Group"
                - PrincipalType.SERVICE_PRINCIPAL or "ServicePrincipal"
            role: Role to assign (resource-specific)

        Returns:
            BaseRoleAssignment: The created (or existing) role assignment

        Examples:
            >>> # Using enum (recommended)
            >>> from fabias.fabric.enums import PrincipalType, WorkspaceRole
            >>> ws.roleAssignments.add(
            ...     principal_id="user-guid",
            ...     principal_type=PrincipalType.USER,
            ...     role=WorkspaceRole.ADMIN
            ... )

            >>> # Using strings (also supported)
            >>> ws.roleAssignments.add(
            ...     principal_id="user-guid",
            ...     principal_type="User",
            ...     role="Admin"
            ... )
        """
        # Convert enum to string value if needed
        principal_type_value = (
            principal_type.value if isinstance(principal_type, PrincipalType) else principal_type
        )

        payload = {"principal": {"id": principal_id, "type": principal_type_value}, "role": role}

        try:
            response = self._client.post(self._endpoint, data=payload)
            data = response.json()
            return self._assignment_class(data, client=self._client)
        except Exception as e:
            # If role assignment already exists (409), fetch and return it
            from .._shared.exceptions import ApiError

            if (
                (isinstance(e, ApiError) and e.status_code == 409)
                or "409" in str(e)
                or "already exists" in str(e).lower()
            ):
                # Fetch the existing assignment
                endpoint = f"{self._endpoint}/{principal_id}"
                response = self._client.get(endpoint)
                data = response.json()
                return self._assignment_class(data, client=self._client)
            raise

    def update(self, principal_id: str, role: str) -> BaseRoleAssignment:  # pragma: no cover
        """
        Update a role assignment.

        Args:
            principal_id: Principal GUID
            role: New role to assign

        Returns:
            BaseRoleAssignment: The updated role assignment
        """
        endpoint = f"{self._endpoint}/{principal_id}"
        payload = {"role": role}

        response = self._client.patch(endpoint, data=payload)
        data = response.json()

        return self._assignment_class(data, client=self._client)

    def delete(self, principal_id: str) -> None:  # pragma: no cover
        """
        Delete a role assignment by principal ID.

        This method accepts a principal ID (user/group/service principal GUID),
        looks up the corresponding role assignment ID, and deletes it.

        Args:
            principal_id: Principal GUID to remove

        Raises:
            NotFoundError: If no role assignment found for the principal

        Note:
            The API requires the role assignment ID (not principal ID) for deletion.
            This method automatically handles the lookup for convenience.
        """
        # List all assignments to find the one with matching principal_id
        assignments = self.__call__()

        # Find assignment with matching principal ID
        matching = [a for a in assignments if a.principalId == principal_id]

        if not matching:
            from .._shared.exceptions import NotFoundError

            raise NotFoundError(
                f"No role assignment found for principal {principal_id}",
                resource_type="RoleAssignment",
                resource_id=principal_id,
            )

        if len(matching) > 1:
            from .._shared.exceptions import FabiasError

            raise FabiasError(
                f"Multiple role assignments found for principal {principal_id}. "
                "This should not happen."
            )

        # Use the role assignment ID for deletion
        role_assignment_id = matching[0].id
        endpoint = f"{self._endpoint}/{role_assignment_id}"
        self._client.delete(endpoint)
