"""
Base classes for workspace items.

Provides Item (base class) and ItemAccessor (base accessor pattern)
for all Fabric workspace item types.
"""

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

from ...._shared.exceptions import FabiasError, NotFoundError
from ...._shared.runtime import runtime
from ...._shared.utils import GUID

if TYPE_CHECKING:
    from ..._client import FabricClient
    from ..permissions.roles import Role, RoleAccessor


class ItemAccessor:
    """
    Base class for item accessors.

    Provides callable access to list or get items, and create new ones.
    Subclasses should set ITEM_TYPE and ITEM_CLASS.

    Usage:
        accessor()           # List all items of this type
        accessor("name")     # Get specific item by name/ID
        accessor.add("name") # Create new item
    """

    ITEM_TYPE = ""  # e.g., "DataPipeline", "Lakehouse"
    ITEM_CLASS: Optional[type] = None  # Set after class definition to avoid circular refs

    def __init__(self, client: "FabricClient", workspace_id: str):
        self._client = client
        self._workspace_id = workspace_id

    @overload
    def __call__(self, identifier: str) -> "Item": ...

    @overload
    def __call__(self, identifier: None = None) -> List["Item"]: ...

    def __call__(self, identifier: Optional[str] = None) -> Union["Item", List["Item"]]:
        """
        Get existing item by name/ID, or list all items of this type.

        Args:
            identifier: Optional item name or GUID. If None, lists all items.

        Returns:
            If identifier provided: Single item object (Pipeline, Lakehouse, etc.)
            If identifier None: List of item objects
        """
        if identifier is not None:
            # Get specific item
            return self.ITEM_CLASS(self._client, self._workspace_id, identifier)

        # List all items of this type (pagination handled automatically by client)
        endpoint = f"/workspaces/{self._workspace_id}/items?type={self.ITEM_TYPE}"
        response = self._client.get(endpoint)
        data = response.json()

        return [
            fromJson(self._client, self._workspace_id, item_data)
            for item_data in data.get("value", [])
        ]

    def add(
        self, name: str, description: Optional[str] = None, folder_id: Optional[str] = None
    ) -> "Item":
        """Create new item in workspace."""
        payload = {"displayName": name, "type": self.ITEM_TYPE}
        if description is not None:
            payload["description"] = description
        if folder_id is not None:
            payload["folderId"] = folder_id

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

        try:
            response = self._client.post(endpoint, data=payload)

            if response.status_code == 202:
                operation_id = response.headers.get("x-ms-operation-id")
                raise FabiasError(
                    f"Item creation started asynchronously (operation: {operation_id}). "
                    "Polling not yet implemented."
                )

            item_data = response.json()
            # Use module-level fromJson defined later in this file
            return fromJson(self._client, self._workspace_id, item_data)
        except Exception as e:
            # If item 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 item by name and type
                items = self.__call__()
                matches = [item for item in items if item.name == name]
                if matches:
                    return matches[0]
            raise

    def delete(self, identifier: str) -> None:  # pragma: no cover
        """
        Delete an item by name or GUID.

        Warning: This operation is permanent and cannot be undone.

        Args:
            identifier: Item name or GUID

        Examples:
            >>> workspace.pipelines.delete("Old Pipeline")
            >>> workspace.lakehouses.delete("abc-123-guid")
        """
        from ...._shared.exceptions import FabiasError

        # Get the item (this resolves name to ID if needed)
        item = self.ITEM_CLASS(self._client, self._workspace_id, identifier)

        if not item.id:
            raise FabiasError(f"Cannot delete item: ID not resolved for '{identifier}'")

        endpoint = f"/workspaces/{self._workspace_id}/items/{item.id}"
        self._client.delete(endpoint)


class Item:
    """
    Base class for workspace items.

    Provides common functionality for resolving and accessing workspace
    items like pipelines, lakehouses, notebooks, and environments.

    Attributes:
        id (str): Item unique identifier (GUID)
        name (str): Item display name
        description (str): Item description
        folder_id (str): Parent folder GUID if in a folder
    """

    # Override in subclasses
    ITEM_TYPE = ""
    ITEM_TYPE_DISPLAY = ""

    def __init__(
        self,
        client: "FabricClient",
        workspace_id: str,
        identifier: Optional[str] = None,
        item_data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize and resolve the workspace item.

        Args:
            client: Authenticated FabricClient
            workspace_id: Workspace GUID containing the item
            identifier: Item display name or GUID (optional if item_data provided)
            item_data: Pre-populated item data from API (skips resolution)
        """
        self._client = client
        self.workspace_id = workspace_id
        self.id: Optional[str] = None
        self.name: Optional[str] = None
        self.description: Optional[str] = None
        self.folder_id: Optional[str] = None

        # Access roles caching
        self._access_roles_cache: Optional[list] = None
        self._security_enabled_cache: Optional[bool] = None

        if item_data:
            # Pre-populate from provided data
            self.fromJson(item_data)
        elif identifier:
            # Resolve via API or sempy
            self._resolve_item(identifier)
        else:
            raise ValueError("Either identifier or item_data must be provided")

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

        Args:
            data: Item JSON data from API
        """
        self.id = data.get("id")
        self.name = data.get("displayName")
        self.description = data.get("description", "")
        self.folder_id = data.get("folderId")

    def _resolve_item(self, identifier: str) -> None:
        """
        Resolve item identifier to actual item data.

        Args:
            identifier: Item name or GUID
        """
        # Try sempy.fabric first in Fabric environment
        if runtime.isFabric:  # pragma: no cover
            sempy_fabric = runtime.sempy
            if sempy_fabric:
                try:
                    self._resolve_via_sempy(sempy_fabric, identifier)
                    return
                except Exception:
                    pass  # Fall through to API

        # Use REST API
        self._resolve_via_api(identifier)

    def _resolve_via_sempy(self, sempy, identifier: str) -> None:  # pragma: no cover
        """
        Resolve item using sempy.fabric.

        Args:
            sempy: sempy.fabric module
            identifier: Item identifier
        """
        items = sempy.list_items(workspace=self.workspace_id, type=self.ITEM_TYPE)

        if GUID.valid(identifier):
            row = items[items["Id"] == identifier]
        else:
            row = items[items["Display Name"] == identifier]

        if row.empty:
            raise NotFoundError(
                f"{self.ITEM_TYPE_DISPLAY} '{identifier}' not found in workspace",
                resource_type=self.ITEM_TYPE_DISPLAY,
                resource_id=identifier,
            )

        if len(row) > 1:
            raise FabiasError(
                f"Multiple {self.ITEM_TYPE_DISPLAY.lower()}s found with identifier '{identifier}'"
            )

        row_data = row.iloc[0]
        self.id = row_data["Id"]
        self.name = row_data.get("Display Name")
        self.description = row_data.get("Description")
        self.folder_id = row_data.get("Folder Id")

    def _resolve_via_api(self, identifier: str) -> None:
        """
        Resolve item using REST API.

        Args:
            identifier: Item identifier
        """
        endpoint = f"/workspaces/{self.workspace_id}/items"
        if self.ITEM_TYPE:
            endpoint = f"{endpoint}?type={self.ITEM_TYPE}"

        response = self._client.get(endpoint)
        data = response.json()
        items = data.get("value", [])

        # Find matching item
        matches = [
            item
            for item in items
            if item.get("displayName") == identifier or item.get("id") == identifier
        ]

        if not matches:
            raise NotFoundError(
                f"{self.ITEM_TYPE_DISPLAY} '{identifier}' not found in workspace",
                resource_type=self.ITEM_TYPE_DISPLAY,
                resource_id=identifier,
            )

        if len(matches) > 1:
            raise FabiasError(
                f"Multiple {self.ITEM_TYPE_DISPLAY.lower()}s found with identifier '{identifier}'"
            )

        item = matches[0]
        self.id = item.get("id")
        self.name = item.get("displayName")
        self.description = item.get("description")
        # folder_id might be in a nested object
        self.folder_id = item.get("folderId") or item.get("folder", {}).get("id")

    def update(  # pragma: no cover
        self, display_name: Optional[str] = None, description: Optional[str] = None
    ) -> "Item":
        """
        Update item properties.

        Args:
            display_name: New display name for the item
            description: New description for the item

        Returns:
            Self for method chaining

        Example:
            >>> pipeline.update(description="Updated ETL pipeline")
            >>> item.update(display_name="New Name", description="New Description")
        """
        if not self.id:
            raise FabiasError("Cannot update item: ID not resolved")

        payload = {}
        if display_name is not None:
            payload["displayName"] = display_name
        if description is not None:
            payload["description"] = description

        if not payload:
            return self  # Nothing to update

        endpoint = f"/workspaces/{self.workspace_id}/items/{self.id}"
        response = self._client.patch(endpoint, data=payload)

        # Update local attributes with response
        updated = response.json()
        self.fromJson(updated)

        return self

    def delete(self) -> None:  # pragma: no cover
        """
        Delete this item from the workspace.

        Warning: This operation is permanent and cannot be undone.

        Example:
            >>> old_pipeline = workspace.pipeline("deprecated-pipeline")
            >>> old_pipeline.delete()
        """
        if not self.id:
            raise FabiasError("Cannot delete item: ID not resolved")

        endpoint = f"/workspaces/{self.workspace_id}/items/{self.id}"
        self._client.delete(endpoint)

        # Clear local state since item no longer exists
        self.id = None
        self.name = None
        self.description = None
        self.folder_id = None

    @property
    def securityEnabled(self) -> bool:
        """
        Check if OneLake data access security (universal security) is enabled.

        This property attempts to fetch access roles to determine if the feature
        is enabled. The result is cached for subsequent calls.

        Returns:
            bool: True if universal security is enabled, False otherwise

        Examples:
            >>> lakehouse = workspace.lakehouse("Analytics")
            >>> if lakehouse.securityEnabled:
            ...     roles = lakehouse.accessRoles()
            ...     print(f"Found {len(roles)} roles")
            ... else:
            ...     print("Universal security not enabled for this workspace")
        """
        # Return cached result if available
        if self._security_enabled_cache is not None:
            return self._security_enabled_cache

        # Attempt to fetch access roles
        try:
            from ..permissions.roles import RoleAccessor

            if not self.id:
                raise ValueError("Item ID not available for security check")

            accessor = RoleAccessor(self._client, self.workspace_id, self.id)
            roles = accessor()

            # Success - cache roles and return True
            self._access_roles_cache = roles
            self._security_enabled_cache = True
            return True

        except Exception as e:
            # Check if it's the universal security disabled error
            if hasattr(e, "response_body") and isinstance(e.response_body, dict):
                more_details = e.response_body.get("moreDetails", [])
                for detail in more_details:
                    if detail.get("errorCode") == "UniversalSecurityFeatureDisabledForWorkspace":
                        # Cache disabled status and return False
                        self._security_enabled_cache = False
                        return False

            # Other error - don't cache, re-raise
            raise

    @property
    def accessRoles(self) -> "RoleAccessor":
        """
        Accessor for OneLake data access roles (ABAC).

        Provides attribute-based access control for data within this item.
        Supported for: Lakehouse, KQL Database, Warehouse, and other OneLake items.

        Note: Returns cached accessor if roles were already fetched via securityEnabled.
        Use refreshAccessRoles() to force a fresh fetch.

        Returns:
            RoleAccessor: Accessor for listing/creating roles

        Examples:
            >>> # List all roles
            >>> roles = lakehouse.accessRoles()
            >>>
            >>> # Create a new role with rules and members
            >>> from fabias.fabric.items import Role, Rule, Member, ReadWrite, MemberType
            >>> role = lakehouse.accessRoles.add(
            ...     name="SalesReader",
            ...     rules=[
            ...         Rule(
            ...             paths=["Tables/Sales"],
            ...             accessLevel=ReadWrite.READ
            ...         )
            ...     ],
            ...     members=[
            ...         Member(
            ...             principalId="user-guid",
            ...             principalType="User",
            ...             memberType=MemberType.ENTRA
            ...         )
            ...     ]
            ... )
        """
        from ..permissions.roles import RoleAccessor

        if not self.id:
            raise ValueError("Item ID not available for accessing roles")

        # Create accessor with optional cache support
        accessor = RoleAccessor(self._client, self.workspace_id, self.id)

        # If we have cached roles, inject them into the accessor
        if self._access_roles_cache is not None:
            accessor._cached_roles = self._access_roles_cache

        return accessor

    def accessRole(self, name: str) -> "Role":
        """
        Get a specific data access role by name.

        Uses lazy loading - the role data is fetched only when properties are accessed.

        Args:
            name: Role name

        Returns:
            Role: The role object (lazy loaded)

        Examples:
            >>> role = lakehouse.accessRole("DefaultReader")
            >>> print(f"Members: {len(role.members)}")  # Triggers API call
            >>>
            >>> # Update role
            >>> role.members.append(Member(...))
            >>> role.save()
        """
        from ..permissions.roles import Role

        return Role(self._client, self.workspace_id, self.id, name=name)

    def refreshAccessRoles(self) -> "Item":
        """
        Clear cached access roles data and force a fresh fetch on next access.

        Returns:
            Self for method chaining

        Examples:
            >>> lakehouse.refreshAccessRoles()
            >>> roles = lakehouse.accessRoles()  # Fresh fetch from API
        """
        self._access_roles_cache = None
        self._security_enabled_cache = None
        return self


def fromJson(client: "FabricClient", workspace_id: str, item_data: Dict[str, Any]) -> Item:
    """
    Factory method to create appropriate item instance from JSON data.

    Converts API JSON to specific item types (Pipeline, Notebook, etc.) based
    on the 'type' field. Unknown types return base Item.

    Args:
        client: Authenticated FabricClient
        workspace_id: Workspace GUID
        item_data: Item JSON from API (must include 'type' field)

    Returns:
        Item: Specific item type or base Item

    Examples:
        >>> data = {"id": "...", "type": "DataPipeline", "displayName": "ETL"}
        >>> item = fromJson(client, workspace_id, data)
        >>> isinstance(item, Pipeline)
        True
    """
    # Import here to avoid circular dependency
    from ..environment import Environment
    from ..lakehouse import Lakehouse
    from ..notebook import Notebook
    from ..pipeline import Pipeline
    from ..variablelibrary import VariableLibrary

    _TYPE_MAP = {
        "DataPipeline": Pipeline,
        "Lakehouse": Lakehouse,
        "Notebook": Notebook,
        "Environment": Environment,
        "VariableLibrary": VariableLibrary,
    }

    if not item_data or not isinstance(item_data, dict):
        raise ValueError("item_data must be a valid dictionary")

    item_type: Any = item_data.get("type")
    item_class = _TYPE_MAP.get(item_type, Item)

    return item_class(client=client, workspace_id=workspace_id, item_data=item_data)  # type: ignore[return-value]
