"""Role data model and accessor for OneLake Data Access Security."""

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

from ...._shared.exceptions import ApiError, FabiasError
from .members import Member
from .rules import Rule

if TYPE_CHECKING:
    from ..._client import FabricClient


class Role:
    """
    A data access role defining who can access what in a lakehouse.

    Provides OOP interface for managing roles with lazy loading and CRUD operations.

    Attributes:
        id: Unique role identifier (GUID)
        name: User-friendly role name
        rules: List of access control rules
        members: List of principals with access
        etag: Entity tag for optimistic concurrency
        kind: Role type (default: "Policy")
    """

    def __init__(
        self,
        client: Optional["FabricClient"] = None,
        workspace_id: Optional[str] = None,
        item_id: Optional[str] = None,
        name: Optional[str] = None,
        rules: Optional[List[Rule]] = None,
        members: Optional[List[Member]] = None,
        role_data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize a role object.

        Two initialization modes:

        1. Dry mode (no client): Create a template role for later submission
           Role(name="MyRole", rules=[...], members=[...])

        2. Live mode (with client): Create a role bound to API for CRUD operations
           Role(client, workspace_id, item_id, name="MyRole") or
           Role(client, workspace_id, item_id, role_data={...})

        Args:
            client: Authenticated FabricClient (None for dry mode)
            workspace_id: Workspace GUID (None for dry mode)
            item_id: Item GUID (None for dry mode)
            name: Role name
            rules: List of access control rules (dry mode only)
            members: List of members with access (dry mode only)
            role_data: Pre-populated role data from API (live mode only)
        """
        # Initialize attributes
        self.id: Optional[str] = None
        self.name: Optional[str] = name
        self.rules: List[Rule] = rules or []
        self.members: List[Member] = members or []
        self.etag: Optional[str] = None
        self.kind: str = "Policy"
        self._data_raw: Optional[Dict[str, Any]] = None

        # Dry mode: No client (template role)
        if client is None:
            self._client = None
            self._workspace_id = None
            self._item_id = None
            self._data_fetched = False
            return

        # Live mode: Has client (API-backed role)
        self._client = client
        self._workspace_id = workspace_id
        self._item_id = item_id
        self._data_fetched = False

        # Pre-populated from API response
        if role_data:
            self.fromJson(role_data)
            return

        # Name provided - resolve to full data
        if name:
            self._fetch()

    def fromJson(self, data: Dict[str, Any]) -> None:
        """
        Populate role attributes from JSON data.

        Args:
            data: Role JSON data from API
        """
        self.id = data.get("id")
        self.name = data.get("name")
        self.rules = [Rule.fromJson(rule) for rule in data.get("decisionRules", [])]
        self.members = Member.fromJson(data.get("members", {}))
        self.etag = data.get("etag")
        self.kind = data.get("kind", "Policy")
        self._data_raw = data
        self._data_fetched = True

    def _fetch(self) -> None:
        """Fetch role data from API by name."""
        if self._data_fetched or not self.name or not self._client:
            return

        # Get role directly by name
        endpoint = (
            f"/workspaces/{self._workspace_id}/items/{self._item_id}/dataAccessRoles/{self.name}"
        )
        response = self._client.get(endpoint)
        data = response.json()

        self.fromJson(data)

    def toJson(self) -> Dict[str, Any]:
        """
        Convert role to API JSON format.

        Returns:
            Dict ready for API submission with grouped members structure
        """
        data = {"name": self.name, "decisionRules": [rule.toJson() for rule in self.rules]}

        # Include id if present (for updates)
        if self.id:
            data["id"] = self.id
        if self.members and len(self.members) > 0:
            # Hydrate dry role Fabric Members
            from .members import FabricItem

            for member in self.members:
                if isinstance(member, FabricItem) and not member.path:
                    if self._workspace_id and self._item_id:
                        member.path = f"{self._workspace_id}/{self._item_id}"
            data["members"] = cast(Any, Member.toJson(self.members))

        return data

    def save(self, overwrite: bool = True) -> "Role":
        """
        Save this role (create or update in API).

        Works for both new roles (no ID) and existing roles (has ID).
        Uses the single-role createOrUpdate endpoint.

        Args:
            overwrite: If True (default), overwrites existing role.
                      If False, fails if role already exists (409 conflict).

        Returns:
            Self for method chaining

        Raises:
            FabiasError: If role is not live (no client) or has no name

        Examples:
            >>> # Update existing role
            >>> role = lakehouse.accessRole("SalesReader")
            >>> role.members.append(Member(...))
            >>> role.save()
            >>>
            >>> # Create new role
            >>> role = Role(name="NewRole", rules=[...], members=[...])
            >>> role._client = client  # Hydrated by accessor
            >>> role.save()
        """
        if self._client is None:
            raise FabiasError(
                "Cannot save dry role. Add it to a lakehouse first using accessRoles.add(role)."
            )
        if not self.name:
            raise FabiasError("Cannot save role: name not set")

        # Use single-role createOrUpdate endpoint with conflict policy
        policy = "Overwrite" if overwrite else "Abort"
        endpoint = f"/workspaces/{self._workspace_id}/items/{self._item_id}/dataAccessRoles?preview=true&dataAccessRoleConflictPolicy={policy}"

        # Wrap in array per API requirement
        payload = {"value": [self.toJson()]}
        payload = self.toJson()

        try:
            response = self._client.post(endpoint, data=payload)
            response_data = response.json()
            self.fromJson(response_data)
        except ApiError as e:
            # If role already exists (409) and overwrite=False, gracefully fetch existing
            if e.status_code == 409:
                if not overwrite:
                    # Refresh from API to get current state
                    self._data_fetched = False
                    self._fetch()
                else:
                    # Failover method until overwrite works
                    try:
                        self.delete()
                        response = self._client.post(endpoint, data=payload)
                        response_data = response.json()
                        self.fromJson(response_data)
                    except ApiError as e:
                        raise FabiasError(
                            f"Failed to overwrite existing role '{self.name}': {e}"
                        ) from e
            else:
                raise

        return self

    def delete(self) -> None:
        """
        Delete this role from the item.

        Warning: This operation is permanent and cannot be undone.

        Raises:
            FabiasError: If role is not live (no client) or has no name

        Examples:
            >>> role = lakehouse.accessRole("OldRole")
            >>> role.delete()
        """
        if self._client is None:
            raise FabiasError("Cannot delete dry role. It's not attached to any lakehouse.")
        if not self.name:
            raise FabiasError("Cannot delete role: name not set")

        endpoint = (
            f"/workspaces/{self._workspace_id}/items/{self._item_id}/dataAccessRoles/{self.name}"
        )
        self._client.delete(endpoint)

        # Clear local state
        self.id = None
        self._data_fetched = False

    def refresh(self) -> "Role":
        """
        Refresh role data from API.

        Returns:
            Self for method chaining

        Raises:
            FabiasError: If role is not live (no client)

        Examples:
            >>> role.refresh()
        """
        if self._client is None:
            raise FabiasError("Cannot refresh dry role. It's not attached to any lakehouse.")
        self._data_fetched = False
        self._fetch()
        return self

    def __repr__(self) -> str:
        """String representation for debugging."""
        member_count = len(self.members)
        rule_count = len(self.rules)
        return f"Role(name='{self.name}', rules={rule_count}, members={member_count})"


class RoleAccessor:
    """
    Accessor for OneLake data access roles (ABAC).

    Provides list/create operations for roles.

    Usage:
        accessor()                # List all roles
        accessor.add("name", ...) # Create new role
        accessor.get("name")      # Get specific role
    """

    def __init__(self, client: "FabricClient", workspace_id: str, item_id: str):
        """
        Initialize accessor.

        Args:
            client: Authenticated FabricClient
            workspace_id: Workspace GUID containing the item
            item_id: Item GUID (lakehouse, etc.)
        """
        self._client = client
        self._workspace_id = workspace_id
        self._item_id = item_id
        self._cached_roles: Optional[List[Role]] = None

    def __call__(self, name: Optional[str] = None) -> List[Role]:
        """
        List all roles or filter by name.

        Args:
            name: Optional name filter (partial match, case-insensitive)

        Returns:
            List of Role objects

        Examples:
            >>> roles = lakehouse.accessRoles()
            >>> reader_roles = lakehouse.accessRoles(name="Reader")
        """
        # Use cache if available
        if self._cached_roles is not None:
            roles = self._cached_roles
        else:
            # Get all roles (pagination handled automatically by client)
            endpoint = f"/workspaces/{self._workspace_id}/items/{self._item_id}/dataAccessRoles"
            response = self._client.get(endpoint)
            data = response.json()

            # Create Role objects with client attached
            roles = [
                Role(self._client, self._workspace_id, self._item_id, role_data=role_data)
                for role_data in data.get("value", [])
            ]
            self._cached_roles = roles

        # Filter by name if provided
        if name:
            name_lower = name.lower()
            roles = [r for r in roles if name_lower in (r.name or "").lower()]

        return roles

    def add(self, role: Role, overwrite: bool = False) -> Role:
        """
        Add or update a single data access role.

        Preserves other existing roles. Use replace() for batch operations.

        Args:
            role: Role object to add
            overwrite: Whether to overwrite if role exists (default: False)
                      If False and role exists, raises 409 conflict

        Returns:
            Live Role object with client attached

        Examples:
            >>> # Create and add a dry role
            >>> role = Role(name="Reader", rules=[...], members=[...])
            >>> live_role = lakehouse.accessRoles.add(role)
            >>>
            >>> # Overwrite existing role
            >>> role = Role(name="Reader", rules=[...], members=[...])
            >>> live_role = lakehouse.accessRoles.add(role, overwrite=True)
        """
        # Hydrate dry role with client context
        role._client = self._client
        role._workspace_id = self._workspace_id
        role._item_id = self._item_id

        # Save and return
        role.save(overwrite=overwrite)

        # Clear cache
        self._cached_roles = None

        return role

    def replace(self, roles: List[Role]) -> List[Role]:
        """
        Replace all data access roles with the provided list.

        **WARNING**: This is a destructive operation. All existing roles will be removed
        and replaced with the roles in the provided list. Use add() to preserve existing roles.

        Args:
            roles: List of Role objects to set as the complete role collection

        Returns:
            List of live Role objects with client attached

        Examples:
            >>> # Replace all roles with a new set
            >>> new_roles = [
            ...     Role(name="Reader", rules=[...], members=[...]),
            ...     Role(name="Writer", rules=[...], members=[...])
            ... ]
            >>> lakehouse.accessRoles.replace(new_roles)
            >>>
            >>> # Clear all roles (empty list)
            >>> lakehouse.accessRoles.replace([])
        """
        # Validate all roles have required fields
        for i, role in enumerate(roles):
            if not isinstance(role, Role):
                raise TypeError(f"Item at index {i} is not a Role object")
            if not role.name:
                raise ValueError(f"Role at index {i} missing required field 'name'")

        endpoint = f"/workspaces/{self._workspace_id}/items/{self._item_id}/dataAccessRoles"

        # Build payload - always use Overwrite policy since we're replacing
        payload = {
            "value": [
                {
                    "name": role.name,
                    "kind": role.kind,
                    "decisionRules": [rule.toJson() for rule in role.rules],
                    "members": Member.toJson(role.members) if role.members else {},
                }
                for role in roles
            ]
        }

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

        # Clear cache
        self._cached_roles = None

        # Return list of live Roles
        return [
            Role(self._client, self._workspace_id, self._item_id, role_data=item)
            for item in response_data.get("value", [])
        ]
