"""
Fabric Environment library management.

Provides functionality for managing custom libraries in Fabric environments,
including upload, delete, and replace operations.
"""

import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional

from ..._shared.exceptions import FabiasError

if TYPE_CHECKING:
    from .._client import FabricClient


class Libraries:
    """
    Manages custom libraries in a Microsoft Fabric Environment.

    Provides methods to:
    - List staging and published libraries
    - Upload new libraries (wheel, py, jar, tar.gz)
    - Delete libraries from staging
    - Replace existing libraries with new versions

    Examples:
        >>> env = workspace.environment("ML Environment")
        >>> libs = env.libraries
        >>>
        >>> # Get current libraries
        >>> libs.refresh()
        >>> print(libs.staging)
        >>>
        >>> # Upload a new library
        >>> libs.upload("/path/to/my_package-1.0.0-py3-none-any.whl")
        >>>
        >>> # Replace an existing library
        >>> libs.replace("my_package", "/path/to/my_package-1.1.0-py3-none-any.whl")
    """

    def __init__(self, client: "FabricClient", workspace_id: str, environment_id: str):
        """
        Initialize library manager.

        Args:
            client: Authenticated FabricClient
            workspace_id: Workspace GUID
            environment_id: Environment GUID
        """
        self._client = client
        self._workspace_id = workspace_id
        self._environment_id = environment_id
        self._endpoint = f"/workspaces/{workspace_id}/environments/{environment_id}"

        # Cached library data
        self.staging: Dict[str, Any] = {}
        self.published: Dict[str, Any] = {}

    def refresh(self) -> "Libraries":
        """
        Refresh library data from API.

        Fetches both staging and published library lists.

        Returns:
            Libraries: Self for method chaining
        """
        staging_data = self._get_libraries(staging=True)
        published_data = self._get_libraries(staging=False)

        self.staging = {
            "environment": staging_data.get("environmentYml"),
            "libraries": self._extract_library_list(staging_data),
        }

        self.published = {
            "environment": published_data.get("environmentYml"),
            "libraries": self._extract_library_list(published_data),
        }

        return self

    def _get_libraries(self, staging: bool = False) -> Dict[str, Any]:
        """
        Fetch libraries from API.

        Args:
            staging: If True, fetch staging libraries, else published

        Returns:
            dict: Library data from API
        """
        path = "staging/libraries" if staging else "libraries"
        response = self._client.get(f"{self._endpoint}/{path}")
        return response.json()

    def _extract_library_list(self, data: Dict[str, Any]) -> List[str]:
        """
        Extract flat library list from API response.

        Args:
            data: API response data

        Returns:
            list: List of library filenames
        """
        custom_libs = data.get("customLibraries", {})

        libraries = []
        libraries.extend(custom_libs.get("wheelFiles", []))
        libraries.extend(custom_libs.get("pyFiles", []))
        libraries.extend(custom_libs.get("jarFiles", []))
        libraries.extend(custom_libs.get("rTarFiles", []))

        return libraries

    def upload(self, file_path: str) -> bool:  # pragma: no cover
        """
        Upload a library file to staging.

        Supports .whl, .py, .jar, and .tar.gz files.

        Args:
            file_path: Path to the library file

        Returns:
            bool: True if upload succeeded

        Raises:
            FabiasError: If file not found or upload fails
        """
        path = Path(file_path)

        if not path.exists():
            raise FabiasError(f"File not found: {file_path}")

        with open(path, "rb") as f:
            file_content = f.read()

        files = {"file": (path.name, file_content, "application/octet-stream")}

        self._client.post_multipart(f"{self._endpoint}/staging/libraries", files=files)

        return True

    def delete(self, filename: str) -> bool:  # pragma: no cover
        """
        Delete a library from staging.

        Args:
            filename: Name of the library file to delete

        Returns:
            bool: True if deletion succeeded
        """
        self._client.delete(f"{self._endpoint}/staging/libraries?libraryToDelete={filename}")

        return True

    def replace(
        self,
        library_pattern: str,
        file_path: str,
        callback: Optional[Callable[[str, Dict], None]] = None,
    ) -> None:
        """
        Replace existing library with a new version.

        Deletes all matching libraries from staging and uploads the new file.

        Args:
            library_pattern: Regex pattern to match library names (e.g., "my_package")
            file_path: Path to the new library file
            callback: Optional progress callback(action, details)

        Examples:
            >>> libs.replace(
            ...     "my_package",
            ...     "/path/to/my_package-2.0.0-py3-none-any.whl",
            ...     callback=lambda action, details: print(f"{action}: {details}")
            ... )
        """
        # Refresh library list if needed
        if not self.staging or not self.staging.get("libraries"):
            if callback:
                callback("getLibrary", {})
            self.refresh()
            if callback:
                callback("getLibrary", self.staging)

        # Find and delete matching libraries
        regex = re.compile(library_pattern)
        matches = [lib for lib in self.staging.get("libraries", []) if regex.search(lib)]

        for match in matches:
            if callback:
                callback("delete", {"file": match, "status": "starting"})

            if self.delete(match):
                if callback:
                    callback("delete", {"file": match, "status": "deleted"})

        # Upload new library
        filename = Path(file_path).name
        if callback:
            callback("upload", {"file": filename, "status": "starting"})

        if self.upload(file_path):
            if callback:
                callback("upload", {"file": filename, "status": "uploaded"})

    def __repr__(self) -> str:
        staging_count = len(self.staging.get("libraries", []))
        published_count = len(self.published.get("libraries", []))
        return f"Libraries(staging={staging_count}, published={published_count})"
