"""
Folder management for Microsoft Fabric workspaces.

Provides functionality for managing workspace folders including
creating, listing, moving, renaming, and deleting folders.

Note: This API is part of a Preview release.
"""

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

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

if TYPE_CHECKING:
    from ._client import FabricClient


class Folder:
    """
    Represents a folder in a Fabric workspace.

    Folders organize workspace items into a hierarchical structure.
    Supports lazy loading: when created with a GUID, properties are
    fetched on first access.

    Attributes:
        id (str): Folder unique identifier (GUID)
        name (str): Folder display name
        workspace_id (str): Parent workspace GUID
        parent (str): Parent folder GUID, or None if at workspace root

    Examples:
        Get a folder:

        >>> folder = workspace.folder("Sales")
        >>> print(f"{folder.name} (ID: {folder.id})")

        Get a nested folder by path:

        >>> folder = workspace.folder("Sales/Q1/January")

        List all folders:

        >>> for f in workspace.folders():
        ...     print(f"{f.name} -> parent: {f.parent}")

        Create a subfolder:

        >>> parent = workspace.folder("Sales")
        >>> child = workspace.folders.add("Q1", parent=parent.id)

        Move a folder:

        >>> folder.move(target_folder_id=other_folder.id)

        Rename a folder:

        >>> folder.rename("New Name")
    """

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

        Args:
            client: Authenticated FabricClient
            workspace_id: Workspace GUID containing the folder
            identifier: Folder name, GUID, or slash-separated path
                (e.g. ``"Sales/Q1/January"``)
            folder_data: Pre-populated folder data from API (skips resolution)
        """
        self._client = client
        self._workspace_id = workspace_id
        self._data_fetched = False

        # Attributes (lazy loaded)
        self._id: Optional[str] = None
        self._name: Optional[str] = None
        self._parent: Optional[str] = None

        # Pre-populated from API response
        if folder_data:
            self._populate(folder_data)
            return

        if identifier is None:
            raise ValueError("Either identifier or folder_data must be provided")

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

        # Name provided — resolve immediately
        self._resolve_by_name(identifier)

    def _populate(self, data: Dict[str, Any]) -> None:
        """Populate attributes from API response data."""
        self._id = data.get("id")
        self._name = data.get("displayName")
        self._parent = data.get("parentFolderId")
        # workspaceId may also be in the payload but we already have it
        self._data_fetched = True

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

        response = self._client.get(f"/workspaces/{self._workspace_id}/folders/{self._id}")
        self._populate(response.json())

    def _resolve_by_name(self, name: str) -> None:
        """
        Resolve folder name or path to GUID.

        Supports both simple names and slash-separated paths:
          - ``"Sales"`` — matches any folder named Sales (must be unique)
          - ``"Sales/Q1"`` — matches Q1 under the root-level Sales folder
          - ``"Sales/Q1/January"`` — walks the full path from root

        A single API call fetches all folders; path segments are resolved
        in-memory against the returned tree.
        """
        response = self._client.get(f"/workspaces/{self._workspace_id}/folders")
        all_folders = response.json().get("value", [])

        # Path resolution: split on "/" and walk top-down
        segments = [s for s in name.split("/") if s]

        if len(segments) > 1:
            self._resolve_path(segments, all_folders, name)
            return

        # Simple name (no slash) — exact match, error on ambiguity
        segment = segments[0]
        matches = [f for f in all_folders if f.get("displayName") == segment]

        if not matches:
            raise NotFoundError(
                f"Folder '{name}' not found in workspace",
                resource_type="Folder",
                resource_id=name,
            )

        if len(matches) > 1:
            # Build path hints from the folder tree
            hints = []
            for m in matches:
                hints.append(self._build_path(m, all_folders))
            hint_str = ", ".join(f"'{h}'" for h in hints)
            raise FabiasError(
                f"Multiple folders found with name '{name}'. "
                f"Specify the full path to disambiguate: {hint_str}"
            )

        self._populate(matches[0])

    def _resolve_path(
        self,
        segments: List[str],
        all_folders: List[Dict[str, Any]],
        original: str,
    ) -> None:
        """Walk path segments top-down, resolving each against its parent."""
        parent_id: Optional[str] = None  # None = workspace root

        for i, segment in enumerate(segments):
            matches = [
                f
                for f in all_folders
                if f.get("displayName") == segment and f.get("parentFolderId") == parent_id
            ]

            if not matches:
                partial = "/".join(segments[: i + 1])
                raise NotFoundError(
                    f"Folder path '{original}' not found — "
                    f"no folder named '{segment}' "
                    f"{'at workspace root' if parent_id is None else f'under {segments[i - 1]!r}'}",
                    resource_type="Folder",
                    resource_id=original,
                )

            if len(matches) > 1:
                partial = "/".join(segments[: i + 1])
                raise FabiasError(
                    f"Ambiguous path at '{partial}': multiple folders named "
                    f"'{segment}' at the same level."
                )

            parent_id = matches[0]["id"]

        # Last match is our target
        self._populate(matches[0])  # type: ignore[possibly-undefined]

    @staticmethod
    def _build_path(folder: Dict[str, Any], all_folders: List[Dict[str, Any]]) -> str:
        """Build the full path string for a folder (for error hints)."""
        parts = [folder.get("displayName", "")]
        parent_id = folder.get("parentFolderId")
        seen: set = set()
        while parent_id and parent_id not in seen:
            seen.add(parent_id)
            parent = next((f for f in all_folders if f.get("id") == parent_id), None)
            if not parent:
                break
            parts.append(parent.get("displayName", ""))
            parent_id = parent.get("parentFolderId")
        parts.reverse()
        return "/".join(parts)

    # =========================================================================
    # Properties
    # =========================================================================

    @property
    def id(self) -> Optional[str]:
        """Folder ID (always available after init)."""
        return self._id

    @property
    def name(self) -> Optional[str]:
        """Folder display name. Triggers lazy load if needed."""
        self._fetch()
        return self._name

    @property
    def workspace_id(self) -> str:
        """Workspace ID this folder belongs to."""
        return self._workspace_id

    @property
    def parent(self) -> Optional[str]:
        """Parent folder ID (None if at workspace root). Triggers lazy load if needed."""
        self._fetch()
        return self._parent

    # =========================================================================
    # Methods
    # =========================================================================

    def refresh(self) -> "Folder":
        """
        Force refresh folder data from API.

        Returns:
            Folder: self (for method chaining)
        """
        self._data_fetched = False
        self._fetch()
        return self

    def rename(self, name: str) -> "Folder":
        """
        Rename this folder.

        Args:
            name: New folder name

        Returns:
            Folder: self (updated with new name)

        Examples:
            >>> folder.rename("New Folder Name")
        """
        response = self._client.patch(
            f"/workspaces/{self._workspace_id}/folders/{self._id}",
            data={"displayName": name},
        )
        self._populate(response.json())
        return self

    def move(self, target_folder_id: Optional[str] = None) -> "Folder":
        """
        Move this folder within the workspace.

        Args:
            target_folder_id: Destination folder GUID. If None, moves to
                workspace root.

        Returns:
            Folder: self (updated with new parent)

        Raises:
            FabiasError: If move would create circular hierarchy

        Examples:
            Move into another folder:

            >>> folder.move(target_folder_id=other_folder.id)

            Move to workspace root:

            >>> folder.move()
        """
        payload: Dict[str, Any] = {}
        if target_folder_id is not None:
            payload["targetFolderId"] = target_folder_id

        response = self._client.post(
            f"/workspaces/{self._workspace_id}/folders/{self._id}/move",
            data=payload,
        )
        self._populate(response.json())
        return self

    def delete(self) -> None:
        """
        Delete this folder.

        The folder must be empty (no items or nested folders).

        Raises:
            FabiasError: If folder is not empty

        Examples:
            >>> folder.delete()
        """
        self._client.delete(f"/workspaces/{self._workspace_id}/folders/{self._id}")

    def __repr__(self) -> str:
        name = self._name or "(not loaded)"
        return f"Folder(id={self._id}, name={name!r})"

    def __str__(self) -> str:
        return self.name or self._id or "Folder"


class FolderAccessor:
    """
    Accessor for workspace folder operations.

    Provides callable access to list folders, get a specific folder,
    and create new folders within a workspace.

    Usage:
        accessor()                    # List all folders (recursive)
        accessor("name")              # Get folder by name/ID
        accessor.add("name")          # Create folder at workspace root
        accessor.add("name", parent)  # Create nested folder
    """

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

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

    @overload
    def __call__(
        self,
        identifier: None = None,
        *,
        root_folder_id: Optional[str] = None,
        recursive: bool = True,
    ) -> List[Folder]: ...

    def __call__(
        self,
        identifier: Optional[str] = None,
        *,
        root_folder_id: Optional[str] = None,
        recursive: bool = True,
    ) -> Union[Folder, List[Folder]]:
        """
        Get folder by name/ID, or list folders in the workspace.

        Args:
            identifier: Folder name or GUID. If provided, returns single Folder.
            root_folder_id: Filter to folders under this parent. Ignored when
                identifier is provided.
            recursive: If True (default), lists folders in nested subfolders too.
                If False, only direct children. Ignored when identifier is provided.

        Returns:
            Folder or list[Folder]

        Examples:
            List all folders recursively:

            >>> folders = workspace.folders()

            List only direct children of a folder:

            >>> children = workspace.folders(root_folder_id=parent.id, recursive=False)

            Get specific folder:

            >>> folder = workspace.folders("Sales")
        """
        if identifier is not None:
            return Folder(self._client, self._workspace_id, identifier)

        # Build query params
        params: Dict[str, str] = {}
        if root_folder_id is not None:
            params["rootFolderId"] = root_folder_id
        if not recursive:
            params["recursive"] = "False"

        endpoint = f"/workspaces/{self._workspace_id}/folders"
        if params:
            qs = "&".join(f"{k}={v}" for k, v in params.items())
            endpoint = f"{endpoint}?{qs}"

        response = self._client.get(endpoint)
        data = response.json()

        return [
            Folder(self._client, self._workspace_id, folder_data=item)
            for item in data.get("value", [])
        ]

    def add(
        self,
        name: str,
        parent: Optional[str] = None,
    ) -> Folder:
        """
        Create a new folder in the workspace.

        Args:
            name: Folder name
            parent: Parent folder GUID. If None, the folder is
                created at the workspace root.

        Returns:
            Folder: The newly created folder

        Raises:
            FabiasError: If name is already in use or max depth/count exceeded

        Examples:
            Create at workspace root:

            >>> folder = workspace.folders.add("Sales")

            Create nested folder:

            >>> child = workspace.folders.add("Q1", parent=parent.id)
        """
        payload: Dict[str, Any] = {"displayName": name}
        if parent is not None:
            payload["parentFolderId"] = parent

        response = self._client.post(
            f"/workspaces/{self._workspace_id}/folders",
            data=payload,
        )
        return Folder(self._client, self._workspace_id, folder_data=response.json())

    def delete(self, identifier: str) -> None:
        """
        Delete a folder by name or GUID.

        The folder must be empty (no items or nested folders).

        Args:
            identifier: Folder name or GUID

        Examples:
            >>> workspace.folders.delete("Old Folder")
        """
        folder = Folder(self._client, self._workspace_id, identifier)

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

        self._client.delete(f"/workspaces/{self._workspace_id}/folders/{folder.id}")
