"""
Connections management for Microsoft Fabric workspaces.

Provides functionality for managing data connections including
on-premises, virtual network, and cloud connections.
"""

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

from .._shared.utils import GUID
from ._shared.enums import ConnectivityType, PrivacyLevel
from ._shared.role_assignments import BaseRoleAssignment, BaseRoleAssignmentAccessor

if TYPE_CHECKING:
    from ._client import FabricClient


class Connection:
    """
    Represents a Fabric connection.

    Attributes:
        id (str): Connection unique identifier
        name (str): Connection display name
        connection_type (str): Type of connection
        connectivity_type (str): Connectivity type (OnPremises, VirtualNetwork, Cloud)
        privacy_level (str): Privacy level setting
        credential_details (dict): Credential configuration details
    """

    def __init__(
        self,
        client: "FabricClient",
        identifier: Optional[str] = None,
        connection_data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize Connection.

        Args:
            client: Authenticated FabricClient
            identifier: Connection name or GUID (triggers lazy loading or immediate resolution)
            connection_data: Pre-populated connection data from API (skips resolution)
        """
        self._client = client
        self._data_fetched = False

        # Attributes (lazy loaded)
        self._id: Optional[str] = None
        self._name: Optional[str] = None
        self._connection_type: Optional[str] = None
        self._connectivity_type: Optional[str] = None
        self._privacy_level: Optional[str] = None
        self._credential_details: Optional[dict] = None

        # Pre-populated from API response
        if connection_data:
            self._id = connection_data.get("id")
            self._name = connection_data.get("displayName")
            self._connection_type = connection_data.get("connectivityType")
            self._connectivity_type = connection_data.get("connectivityType")
            self._privacy_level = connection_data.get("privacyLevel")
            self._credential_details = connection_data.get("credentialDetails", {})
            self._data_fetched = True
            return

        # GUID provided - store and return (no API call)
        if identifier and GUID.valid(identifier):
            self._id = identifier
            return

        # Name provided - resolve to GUID (API call happens here)
        if identifier:
            self._resolve_identifier(identifier)

    def _resolve_identifier(self, name: str) -> None:
        """
        Resolve connection name to GUID.

        Searches all connections and populates all data since we're making an API call.

        Args:
            name: Connection display name
        """
        # Search by name using Connections handler
        from .connections import Connections

        handler = Connections(self._client)
        matches = handler.list(name=name)

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

            raise NotFoundError(
                f"Connection '{name}' not found", resource_type="Connection", resource_id=name
            )

        # Exact match preferred
        exact = [c for c in matches if c.name == name]
        if len(exact) == 1:
            matched = exact[0]
        elif len(matches) == 1:
            matched = matches[0]
        else:
            from .._shared.exceptions import FabiasError

            raise FabiasError(
                f"Multiple connections found matching '{name}': "
                + ", ".join(c.name for c in matches[:5])
            )

        # Copy all properties from matched connection
        self._id = matched.id
        self._name = matched.name
        self._connection_type = matched.connectionType
        self._connectivity_type = matched.connectivityType
        self._privacy_level = matched.privacyLevel
        self._credential_details = matched.credentialDetails
        self._data_fetched = True

    def _fetch(self) -> None:
        """Lazy load connection data from API if not already fetched."""
        if self._data_fetched or not self._id:
            return

        response = self._client.get(f"/connections/{self._id}")
        data = response.json()

        self._name = data.get("displayName")
        self._connection_type = data.get("connectivityType")
        self._connectivity_type = data.get("connectivityType")
        self._privacy_level = data.get("privacyLevel")
        self._credential_details = data.get("credentialDetails", {})
        self._data_fetched = True

    @property
    def id(self) -> Optional[str]:
        """Connection ID is always available (set in __init__)."""
        return self._id

    @property
    def name(self) -> Optional[str]:
        """Connection display name (lazy loaded)."""
        self._fetch()
        return self._name

    @property
    def connectionType(self) -> Optional[str]:
        """Connection type (lazy loaded)."""
        self._fetch()
        return self._connection_type

    @property
    def connectivityType(self) -> Optional[str]:
        """Connectivity type (lazy loaded)."""
        self._fetch()
        return self._connectivity_type

    @property
    def privacyLevel(self) -> Optional[str]:
        """Privacy level (lazy loaded)."""
        self._fetch()
        return self._privacy_level

    @property
    def credentialDetails(self) -> Optional[dict]:
        """Credential details (lazy loaded)."""
        self._fetch()
        return self._credential_details

    def refresh(self) -> "Connection":
        """
        Force refresh connection data from API.

        Returns:
            Connection: Self for method chaining

        Examples:
            >>> conn = fabric.connection("SQL Server")
            >>> conn.refresh()  # Get latest data from API
        """
        self._data_fetched = False
        self._fetch()
        return self

    def roleAssignment(self, principal_id: str) -> "ConnectionRoleAssignment":
        """
        Get a specific role assignment by principal ID.

        This method accepts a principal ID and looks up the corresponding
        role assignment. If multiple or no assignments are found, raises an error.

        Args:
            principal_id: Principal GUID (user, group, or service principal)

        Returns:
            ConnectionRoleAssignment: The role assignment

        Raises:
            NotFoundError: If no role assignment found for the principal
            FabiasError: If multiple role assignments found (shouldn't happen)

        Examples:
            >>> conn = fabric.connection("SQL Server")
            >>> assignment = conn.roleAssignment("user-guid")
            >>> print(assignment.role)
        """
        # List all assignments to find the one with matching principal_id
        assignments = self.roleAssignments()

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

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

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

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

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

        return cast("ConnectionRoleAssignment", matches[0])

    @property
    def roleAssignments(self) -> "RoleAssignmentAccessor":
        """
        Access role assignments for this connection.

        Returns:
            RoleAssignmentAccessor: Accessor for listing and managing role assignments

        Examples:
            List role assignments:

            >>> conn = fabric.connection("SQL Server")
            >>> for assignment in conn.roleAssignments():
            ...     print(f"{assignment.principalName}: {assignment.role}")

            Add role assignment:

            >>> conn.roleAssignments.add(
            ...     principal_id="user-guid",
            ...     principal_type="User",
            ...     role="Admin"
            ... )
        """
        if not self.id:
            raise ValueError("Connection ID not available")
        return RoleAssignmentAccessor(self._client, self.id)

    def __repr__(self) -> str:
        return f"Connection(id={self.id}, " f"name={self.name}, " f"type={self.connectionType})"


class ConnectionRoleAssignment(BaseRoleAssignment):
    """
    Represents a role assignment for a connection.

    Inherits from BaseRoleAssignment to provide:
    - principal (dict): Principal information (user/group/service principal)
    - role (str): Assigned role (e.g., 'User', 'Admin', 'Owner')
    - principal_id, principal_type, principal_name properties

    Connection-specific roles:
    - User: Can use the connection
    - UserWithReshare: Can use and share the connection
    - Owner: Full control over the connection
    """

    pass


class Connections:
    """
    Manages connections for Microsoft Fabric (tenant-wide).

    Provides methods to:
    - List, create, get, update, and delete connections
    - Manage connection role assignments
    - Get supported connection types

    Note: Connections are tenant-wide resources, not workspace-scoped.

    Examples:
        >>> import fabias.fabric as fabric
        >>> connections = Connections(fabric.client())
        >>>
        >>> # List all connections
        >>> for conn in connections.list():
        ...     print(f"{conn.name}: {conn.connectionType}")
        >>>
        >>> # Create a connection
        >>> conn = connections.create(
        ...     name="MyDataSource",
        ...     connection_type="Sql",
        ...     connectivity_type="OnPremises",
        ...     privacy_level="Organizational"
        ... )
        >>>
        >>> # Get a connection
        >>> conn = connections.get("connection-id")
        >>>
        >>> # Update a connection
        >>> connections.update("connection-id", display_name="NewName")
        >>>
        >>> # Delete a connection
        >>> connections.delete("connection-id")
    """

    def __init__(self, client: "FabricClient"):
        """
        Initialize Connections handler.

        Args:
            client: Authenticated FabricClient
        """
        self._client = client
        self._endpoint = "/connections"

    def list(self, name: Optional[str] = None) -> List[Connection]:
        """
        List all connections (tenant-wide).

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

        Returns:
            List[Connection]: List of connection objects

        Examples:
            >>> connections = Connections(client)
            >>> for conn in connections.list():
            ...     print(conn.name)

            >>> # Search by name
            >>> sql_conns = connections.list(name="SQL")
        """
        response = self._client.get(self._endpoint)
        data = response.json()

        connections = [
            Connection(self._client, connection_data=item) for item in data.get("value", [])
        ]

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

        return connections

    def get(self, connection_id: str) -> Connection:
        """
        Get a specific connection by ID.

        Args:
            connection_id: Connection GUID

        Returns:
            Connection: The connection object

        Examples:
            >>> conn = connections.get("abc-123")
            >>> print(conn.name)
        """
        response = self._client.get(f"{self._endpoint}/{connection_id}")
        data = response.json()

        return Connection(self._client, connection_data=data)

    def create(  # pragma: no cover
        self,
        name: str,
        connection_type: str,
        connectivity_type: Union[ConnectivityType, str] = "Cloud",
        privacy_level: Union[PrivacyLevel, str] = "Organizational",
        **kwargs: Any,
    ) -> Connection:
        """
        Create a new connection.

        Args:
            name: Connection display name
            connection_type: Type of connection (e.g., 'Sql', 'AzureBlob', 'SharePoint')
            connectivity_type: ConnectivityType enum or string ('OnPremises', 'VirtualNetwork', 'Cloud')
            privacy_level: PrivacyLevel enum or string ('None', 'Public', 'Organizational', 'Private')
            **kwargs: Additional connection properties

        Returns:
            Connection: The created connection object

        Examples:
            Using enums (recommended):

            >>> from fabias.fabric.enums import ConnectivityType, PrivacyLevel
            >>> conn = connections.create(
            ...     name="SQL Server",
            ...     connection_type="Sql",
            ...     connectivity_type=ConnectivityType.ON_PREMISES_GATEWAY,
            ...     privacy_level=PrivacyLevel.ORGANIZATIONAL
            ... )

            Using strings:

            >>> conn = connections.create(
            ...     name="SQL Server",
            ...     connection_type="Sql",
            ...     connectivity_type="OnPremises",
            ...     privacy_level="Organizational"
            ... )
        """
        # Convert enums to strings if needed
        connectivity_str = (
            connectivity_type.value
            if isinstance(connectivity_type, ConnectivityType)
            else connectivity_type
        )
        privacy_str = (
            privacy_level.value if isinstance(privacy_level, PrivacyLevel) else privacy_level
        )

        payload = {
            "displayName": name,
            "connectionType": connection_type,
            "connectivityType": connectivity_str,
            "privacyLevel": privacy_str,
            **kwargs,
        }

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

        return Connection(self._client, connection_data=data)

    def update(  # pragma: no cover
        self,
        connection_id: str,
        display_name: Optional[str] = None,
        privacy_level: Optional[Union[PrivacyLevel, str]] = None,
        **kwargs: Any,
    ) -> Connection:
        """
        Update an existing connection.

        Args:
            connection_id: Connection GUID
            display_name: New display name (optional)
            privacy_level: PrivacyLevel enum or string (optional)
            **kwargs: Additional properties to update

        Returns:
            Connection: The updated connection object

        Examples:
            Using enum:

            >>> from fabias.fabric.enums import PrivacyLevel
            >>> conn = connections.update(
            ...     "abc-123",
            ...     display_name="Updated Name",
            ...     privacy_level=PrivacyLevel.PRIVATE
            ... )

            Using string:

            >>> conn = connections.update(
            ...     "abc-123",
            ...     display_name="Updated Name",
            ...     privacy_level="Private"
            ... )
        """
        payload = {**kwargs}

        if display_name is not None:
            payload["displayName"] = display_name
        if privacy_level is not None:
            # Convert enum to string if needed
            privacy_str = (
                privacy_level.value if isinstance(privacy_level, PrivacyLevel) else privacy_level
            )
            payload["privacyLevel"] = privacy_str

        response = self._client.patch(f"{self._endpoint}/{connection_id}", data=payload)
        data = response.json()

        return Connection(self._client, connection_data=data)

    def delete(self, name_or_id: str) -> None:  # pragma: no cover
        """
        Delete a connection by name or GUID.

        Args:
            name_or_id: Connection name or GUID

        Examples:
            >>> fabric.connections.delete("Old Connection")
            >>> fabric.connections.delete("abc-123-guid")
        """
        # Try as GUID first
        if GUID.valid(name_or_id):
            self._get_client().delete(f"{self._endpoint}/{name_or_id}")
            return

        # Resolve name to connection and delete by ID
        from . import connection as get_connection

        conn = get_connection(name_or_id)
        if conn and conn.id:
            self._get_client().delete(f"{self._endpoint}/{conn.id}")

    def supportedTypes(self) -> List[Dict[str, Any]]:
        """
        List supported connection types.

        Returns:
            List[dict]: List of supported connection type configurations

        Examples:
            >>> types = connections.supported_types()
            >>> for conn_type in types:
            ...     print(conn_type.get('name'))
        """
        response = self._client.get(f"{self._endpoint}/supportedTypes")
        data = response.json()

        return data.get("value", [])

    def __repr__(self) -> str:
        return "Connections(tenant-wide)"


class RoleAssignmentAccessor(BaseRoleAssignmentAccessor):
    """
    Accessor for connection role assignment operations.

    Inherits from BaseRoleAssignmentAccessor to provide:
    - __call__(): List all role assignments
    - add(principal_id, principal_type, role): Add role assignment (with 409 handling)
    - update(principal_id, role): Update role assignment
    - delete(principal_id): Delete role assignment
    """

    def __init__(self, client: "FabricClient", connection_id: str):
        """Initialize role assignment accessor for a connection."""
        endpoint = f"/connections/{connection_id}/roleAssignments"
        super().__init__(client, endpoint, ConnectionRoleAssignment)


class ConnectionsAccessor:
    """Accessor for Connection list/create operations."""

    def __init__(self, client_or_getter: Union["FabricClient", Callable[[], "FabricClient"]]):
        """
        Args:
            client_or_getter: FabricClient or callable returning one (for lazy loading)
        """
        if callable(client_or_getter):
            self._client_getter: Optional[Callable[[], "FabricClient"]] = client_or_getter
            self._client: Optional["FabricClient"] = None
        else:
            self._client = client_or_getter
            self._client_getter = None

    def _get_client(self) -> "FabricClient":
        """Get client, lazily loading if needed."""
        if self._client is None:
            assert self._client_getter is not None
            self._client = self._client_getter()
        return self._client

    def __call__(self) -> List[Connection]:
        """
        List all connections.

        Automatically handles pagination via continuation tokens.

        Returns:
            list[Connection]: List of Connection objects

        Examples:
            >>> import fabias.fabric as fabric
            >>> fabric.client(auth=my_auth)
            >>>
            >>> # List all connections
            >>> all_conns = fabric.connections()
        """
        # Get all connections (pagination handled automatically by client)
        endpoint = "/connections"
        response = self._get_client().get(endpoint)
        data = response.json()

        return [
            Connection(self._get_client(), connection_data=item) for item in data.get("value", [])
        ]

    def get(self, connection_id: str) -> Connection:
        """
        Get a connection by ID.

        Args:
            connection_id: Connection GUID

        Returns:
            Connection: The Connection object

        Examples:
            >>> conn = fabric.connections.get("connection-guid")
        """
        return Connection(self._get_client(), identifier=connection_id)

    def add(  # pragma: no cover
        self, name: str, connection_type: str, connectivity_type: str, **kwargs: Any
    ) -> Connection:
        """
        Create a new connection.

        Args:
            name: Connection name
            connection_type: Type of connection (e.g., "Sql", "AzureBlob")
            connectivity_type: "OnPremises", "ShareableCloud", or "NotShareable"
            **kwargs: Additional properties (privacy_level, credential_details, etc.)

        Returns:
            Connection: The created Connection object

        Examples:
            >>> conn = fabric.connections.add(
            ...     name="My SQL Server",
            ...     connection_type="Sql",
            ...     connectivity_type="OnPremises",
            ...     privacy_level="Organizational"
            ... )
        """
        endpoint = "/connections"
        payload = {
            "displayName": name,
            "connectionDetails": {"type": connection_type},
            "connectivityType": connectivity_type,
            **kwargs,
        }

        try:
            response = self._get_client().post(endpoint, data=payload)
            connection_data = response.json()

            return Connection(self._get_client(), connection_data=connection_data)
        except Exception as e:
            # If connection 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()
            ):
                # Search for existing connection by name
                connections = self.__call__()
                matches = [c for c in connections if c.name == name]
                if matches:
                    return matches[0]
            raise
