from __future__ import annotations

import datetime as dt
import json
import logging
import mimetypes
import pathlib
import shutil
import typing as T
from uuid import uuid4

import azure.core.exceptions
import azure.storage.blob
from azure.mgmt.storage import StorageManagementClient

from .ports import *


class AzureBlobAdaptor(BlobStoragePort[azure.storage.blob.BlobProperties]):
    VERSION_DELIMITER: T.ClassVar[str] = "_"

    def __init__(
        self,
        container_name: str,
        blob_client: azure.storage.blob.BlobServiceClient,
    ):
        """
        Containers are the equivalent of buckets or root folders. It is the
        logical grouping of blobs. Blobs are the actual files stored in the
        container. Blobs are addressed by their path within the container.

        Data isolation between organizations is achieved through containers.
        """
        self.blob_service_client = blob_client
        self.container_name = container_name
        self.container_service_client = self._provision_container_client(container_name)
        self._validate_soft_delete_enabled(self.blob_service_client, self.container_name)

    # =============================================================================
    # BACKEND-SPECIFIC CLIENT MANAGEMENT
    # ==============================================================================

    def _validate_soft_delete_enabled(self, blob_client: azure.storage.blob.BlobServiceClient, container_name: str):
        """Validates that storage account has soft delete enabled"""
        
        blob_client_properties = blob_client.get_service_properties()
        logger.debug(f"Blob Client Properties: {blob_client_properties}")

        is_soft_delete_enabled = blob_client_properties.get(
            "delete_retention_policy", {}
        ).enabled

        if not is_soft_delete_enabled:
            raise EnvironmentError(
                f"Storage account {blob_client.account_name} must have soft delete enabled. "
            )

    def _provision_container_client(
        self, container_name: str
    ) -> azure.storage.blob.ContainerClient:
        """Ensure the container exists, create if it doesn't"""
        # Get container client
        client = self.blob_service_client.get_container_client(container_name)

        # Check if container exists
        if client.exists():
            logger.debug(f"Using existing container: {container_name}")
            return client

        # Container doesn't exist, create it
        try:
            client = self.blob_service_client.create_container(container_name)
            logger.info(f"Created new container: {container_name}")
            return client
        except Exception as e:
            logger.error(f"Failed to create container: {e}")
            raise

    def _get_blob_client(
        self,
        blob_path: str,
        *,
        container_client: T.Optional[azure.storage.blob.ContainerClient] = None,
        version: T.Optional[str] = None,
    ) -> azure.storage.blob.BlobClient:
        """Get blob client for a specific path"""
        container_client = container_client or self.container_service_client
        return container_client.get_blob_client(blob=blob_path, version_id=version)

    # =============================================================================
    # CONCRETE HOOKS FOR PUBLIC METHODS
    # =============================================================================

    def _build_storage_path(
        self,
        uri: VOBlobURI,
        *,
        use_versioned_filename: bool = True,
    ) -> str:
        """
        f^path_azure
        Calcuation, uri -> storage_path in azure
        """
        return (
            f"{uri.logical_path}/{uri.logical_name}"
            if uri.logical_path
            else uri.logical_name
        )

    def _get_blob_properties(
        self, storage_path: str, version: T.Optional[str] = None
    ) -> dict:
        try:
            blob_client = self._get_blob_client(blob_path=storage_path, version=version)
            return blob_client.get_blob_properties()
        except azure.core.exceptions.ResourceNotFoundError:
            raise azure.core.exceptions.ResourceNotFoundError(
                f"Failed to get blob properties for {storage_path} {version}: Blob does not exist."
            )
        except Exception as e:
            raise azure.core.exceptions.AzureError(
                f"Failed to get blob properties for {storage_path} {version}: {str(e)}"
            )

    def _parse_storage_object_to_metadata(
        self,
        storage_object: azure.storage.blob.BlobProperties,
        target_logical_path: str,
        target_logical_name: str,
    ) -> T.Optional[VOBlobMetadata]:
        """
        Convert native object to our metadata
        """
        try:
            # GET BLOB NAME, VALIDATE FORMAT
            blob_name = storage_object.name

            # GET LOGICAL PATH, NAME, AND VERSION. VALIDATE STRINGS
            path, logical_name, logical_version = (
                self._extract_logical_info_from_storage_object(
                    storage_object,
                )
                or (None, None, None)
            )
            if logical_name != target_logical_name or path != target_logical_path:
                return None
            if logical_name is None or logical_version is None:
                return None

            # GET TAGS
            # NOTE user metadata must be retrieved by calling blob properties. list_blobs() does not cut it.
            blob_properties = self._get_blob_properties(
                storage_path=blob_name, version=storage_object.version_id
            )
            user_tags = blob_properties.get("metadata", {})

            return VOBlobMetadata(
                uri=VOBlobURI(
                    logical_path=target_logical_path,
                    logical_name=target_logical_name,
                    version=logical_version,
                ),
                size_bytes=storage_object.size,
                created_at=storage_object.creation_time,
                tags=user_tags,
            )

        except Exception as e:
            logger.debug(
                f"Failed to parse Azure blob {getattr(storage_object, 'name', 'unknown')}: {e}"
            )
            raise

    def _list_storage_objects_by_path(
        self,
        prefix: str,
    ) -> T.List[azure.storage.blob.BlobProperties]:
        """List azure blobs with prefix"""

        logging.debug(
            f'All versions: {list(self.container_service_client.list_blobs(name_starts_with=prefix, include="versions"))}'
        )
        versions = self.container_service_client.list_blobs(
            name_starts_with=prefix, include="versions"
        )
        storage_objects = [
            blob for blob in versions if self._check_azblob_exists_by_path(blob.name)
        ]
        
        self._validate_versioning_enabled(storage_objects)
        
        return sorted(storage_objects, key=lambda blob: blob.version_id, reverse=True)
    
    def _validate_versioning_enabled(self, storage_objects:  T.List[azure.storage.blob.BlobProperties]): 
        """Validate that blobs have versioning enabled. 
        
        Raises EnvironmentError if storage account does not have versioning enabled. 

        We do this here instead of calling `azure.mgmt.storage.StorageManagementClient.blob_services.get_service_properties()` 
        during `init()`, so that we do not need dependencies like subscription ID, resource group name, etc.
        """

        if any(blob.version_id is None for blob in storage_objects): 
            raise EnvironmentError(f"Storage account {self.blob_service_client.account_name} must have versioning enabled.")

    def _build_upload_metadata(
        self,
        uri: VOBlobURI,
        content_hash: str,
        user_tags: T.Dict[str, str],
    ) -> T.Dict[str, str]:
        """Build azure metadata for upload"""
        base_tags = self._generate_base_upload_tags(uri, content_hash)
        metadata = {
            "logical_path": uri.logical_path,
            "logical_name": uri.logical_name,
            "content_hash": content_hash,
            "upload_time": dt.datetime.now(dt.timezone.utc).isoformat(),
            **base_tags,
            **user_tags,
        }
        return metadata

    def _execute_file_upload(
        self,
        versioned_uri: VOBlobURI,
        source_path: pathlib.Path,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        storage_path = self._build_storage_path(versioned_uri)
        blob_client = self._get_blob_client(storage_path)

        with open(source_path, "rb") as data:
            blob_client.upload_blob(
                data=data,
                metadata=upload_metadata,
                tags=upload_metadata,
                overwrite=True,
            )

    def _execute_memory_upload(
        self,
        versioned_uri: VOBlobURI,
        content: bytes,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        storage_path = self._build_storage_path(versioned_uri)
        blob_client = self._get_blob_client(storage_path)

        blob_client.upload_blob(
            data=content,
            metadata=upload_metadata,
            tags=upload_metadata,
            overwrite=True,
        )

    def _check_blob_exists(
        self,
        versioned_uri: VOBlobURI,
    ) -> bool:
        try:
            storage_path = self._build_storage_path(versioned_uri)
            blob_properties = self._get_blob_properties(storage_path=storage_path)

            return blob_properties.version_id == versioned_uri.version
        except azure.core.exceptions.ResourceNotFoundError:
            return False

    def _check_azblob_exists_by_path(
        self,
        storage_path: str,
    ) -> bool:
        try:
            self._get_blob_properties(storage_path=storage_path)
            return True
        except azure.core.exceptions.ResourceNotFoundError:
            return False

    def _check_azblob_version_exists_by_path(
        self,
        storage_path: str,
        version: str,
    ) -> bool:
        try:
            blob_properties = self._get_blob_properties(
                storage_path=storage_path, version=version
            )
            return blob_properties.version_id == version
        except azure.core.exceptions.ResourceNotFoundError:
            return False

    def _resolve_versioned_uri(
        self,
        source: VOBlobURI,
    ) -> VOBlobURI:
        """Resolve versioned URI from source URI.
        If latest is passed it, gets the latest version
        """
        if source.version != "latest":
            return source

        try:
            lifecycle = self.get_lifecycle_entity(source)
            if not lifecycle.versions:
                raise FileNotFoundError(f"No versions found for {source}")
            return lifecycle.get_latest().uri

        except Exception as e:
            logger.error(f"Failed to resolve versioned URI for {source}: {e}")
            raise

    def _execute_download_to_file(
        self,
        versioned_uri: VOBlobURI,
        destination_path: pathlib.Path,
    ) -> pathlib.Path:
        """
        Azure-specific file download implementation
        """

        try:
            # Get blob client
            storage_path = self._build_storage_path(versioned_uri)
            blob_client = self._get_blob_client(storage_path)

            # Ensure destination directory exists
            destination_path.parent.mkdir(parents=True, exist_ok=True)

            # Download to file
            # Download to temporary file first, then replace (atomic operation)
            temp_path = destination_path.with_suffix(".tmp")
            try:
                with open(temp_path, "wb") as file:
                    download_stream = blob_client.download_blob()
                    download_stream.readinto(file)  # readinto is faster than write

                temp_path.replace(destination_path)

            except Exception as e:
                # CLEANUP
                if temp_path.exists():
                    temp_path.unlink()
                raise e

            return destination_path

        except azure.core.exceptions.ResourceNotFoundError:
            raise FileNotFoundError(f"Blob not found: {versioned_uri}")
        except azure.core.exceptions.ClientAuthenticationError:
            raise PermissionError(f"Permission denied: {versioned_uri}")
        except Exception as e:
            logger.error(
                f"Azure failed to download {versioned_uri} to {destination_path}: {e}"
            )
            raise

    def _execute_download_to_memory(
        self,
        versioned_uri: VOBlobURI,
        max_size_bytes: int,
    ) -> bytes:
        try:
            # Get clients
            storage_path = self._build_storage_path(versioned_uri)
            blob_client = self._get_blob_client(storage_path)

            # Download Files
            download_stream = blob_client.download_blob(max_concurrency=4)
            return download_stream.readall()

        except azure.core.exceptions.ResourceNotFoundError:
            raise FileNotFoundError(f"Blob not found: {versioned_uri}")
        except azure.core.exceptions.ClientAuthenticationError:
            raise PermissionError(f"Permission denied: {versioned_uri}")
        except Exception as e:
            logger.error(f"Failed to download {versioned_uri}: {e}")
            raise

    def _execute_download_stream(
        self,
        versioned_uri: VOBlobURI,
        chunk_size: int,
    ) -> T.Iterator[bytes]:
        try:
            # Get clients
            storage_path = self._build_storage_path(versioned_uri)
            blob_client = self._get_blob_client(storage_path)

            # Download Files
            download_stream = blob_client.download_blob(max_concurrency=4)
            for chunk in download_stream.chunks():
                yield chunk

        except azure.core.exceptions.ResourceNotFoundError:
            raise FileNotFoundError(f"Blob not found: {versioned_uri}")
        except azure.core.exceptions.ClientAuthenticationError:
            raise PermissionError(f"Permission denied: {versioned_uri}")
        except Exception as e:
            logger.error(f"Failed to download {versioned_uri}: {e}")
            raise

    # =============================================================================
    # CONCRETE UTLITIES
    # =============================================================================

    def _extract_logical_info_from_storage_object(
        self,
        storage_object: azure.storage.blob.BlobProperties,
        *,
        delimiter: str = "_",
    ) -> T.Tuple[T.Union[str, None], T.Union[str, None], T.Union[str, None]]:
        """
        Extract logical path, name, and version from Azure blob object.

        Azure blob naming convention: {logical_path}/{logical_name}_{version}.{ext}

        Examples:
            "models/v1/weights_abc123.pt" -> ("models/v1", "weights.pt", "abc123")
            "data/dataset_def456.csv" -> ("data", "dataset.csv", "def456")
            "config_xyz789.json" -> ("", "config.json", "xyz789")

        Returns:
            Tuple of (logical_path, logical_name, version) or (None, None, None) if parsing fails
        """
        try:
            blob_name = storage_object.name

            # Split path and filename
            if "/" in blob_name:
                path_parts = blob_name.split("/")
                logical_path = "/".join(path_parts[:-1])
                logical_name = path_parts[-1]
            else:
                logical_path = ""
                logical_name = blob_name

            version = storage_object.version_id

            # Validate extracted components
            if not logical_name or not version:
                return (None, None, None)

            return (logical_path, logical_name, version)

        except Exception as e:
            logger.debug(
                f"Failed to extract logical info from blob {getattr(storage_object, 'name', 'unknown')}: {e}"
            )
            return (None, None, None)

    def _infer_content_type(
        self, storage_object: azure.storage.blob.BlobProperties
    ) -> str:
        """Extract content type from azure blob"""
        if (
            storage_object.content_settings
            and storage_object.content_settings.content_type
            and storage_object.content_settings.content_type
            != "application/octet-stream"
        ):
            return storage_object.content_settings.content_type
        return "application/octet-stream"

    # =============================================================================
    # CONCRETE PUBLIC METHODS
    # =============================================================================

    def list_blobs(
        self,
        path: str = "",
        *,
        tags: T.Optional[T.Dict[str, str]] = None,
    ) -> T.List[VOBlobURI]:
        """
        List blobs with optional filtering by path prefix and tags.

        Returns latest version of each blob by default.
        """
        prefix = f"{path}/" if path else ""
        blobs = self._list_storage_objects_by_path(prefix=prefix)

        uri_list = []
        seen_logical_blobs = set()

        for blob in blobs:
            try:
                # VALIDATE BLOB NAME
                logical_path, logical_name, version = (
                    self._extract_logical_info_from_storage_object(blob)
                    or (None, None, None)
                )
                if logical_name is None or version is None or logical_path is None:
                    # User may have other blobs container with different naming convention
                    logging.warning(
                        f"Invalid blob name: {blob.name}. Skipping file and continuing"
                    )
                    continue

                # VALIDATE LOGICAL MATCH
                logical_key = (
                    f"{logical_path}/{logical_name}" if logical_path else logical_name
                )
                if logical_key in seen_logical_blobs:
                    continue

                # VALIDATE TAG MATCH
                if tags:
                    blob_properties = self._get_blob_properties(
                        storage_path=logical_key, version=version
                    )
                    blob_tags = blob_properties.metadata or {}
                    if not all(blob_tags.get(k) == v for k, v in tags.items()):
                        continue

                uri_list.append(
                    VOBlobURI(
                        logical_path=logical_path,
                        logical_name=logical_name,
                        version=version,
                    )
                )
                seen_logical_blobs.add(logical_key)

            except Exception as e:
                logger.debug(
                    f"Failed to parse Azure blob {getattr(blob, 'name', 'unknown')}: {e}"
                )
                raise

        return uri_list

    def delete(
        self,
        uri: VOBlobURI,
    ):
        try:
            lifecycle = self.get_lifecycle_entity(uri)
            storage_path = self._build_storage_path(uri)

            # Delete root blob containing the latest version.
            blob_client = self._get_blob_client(
                blob_path=storage_path,
            )
            blob_client.delete_blob(delete_snapshots="include")

            # Delete all versions, starting from the oldest.
            for metadata in lifecycle.versions[::-1]:
                blob_client = self._get_blob_client(
                    blob_path=storage_path, version=metadata.uri.version
                )
                try:
                    blob_client.delete_blob()
                except azure.core.exceptions.ResourceNotFoundError:
                    logger.warning(f"Blob not found for deletion: {storage_path}")
                    raise

        except Exception as e:
            logger.warning(f"Error in delete for {uri}: {e}")
            raise

    def restore(self, uri: VOBlobURI) -> ENTBlobLifecycle:
        """
        Restore a soft-deleted blob and all its previous versions.
        """
        try:
            storage_path = self._build_storage_path(uri=uri)
            container_client = self.container_service_client
            blob_client = self._get_blob_client(blob_path=storage_path)
            blob_list = container_client.list_blobs(
                name_starts_with=storage_path, include=["deleted", "versions"]
            )
            blob_list = sorted(
                list(blob_list), key=lambda x: x.version_id, reverse=True
            )

            try:
                # Ref: https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-overview#restoring-a-soft-deleted-version
                # Restore previous versions
                # NOTE all versions get restored when any version is undeleted
                latest_version = blob_list[0].version_id
                blob_client = self._get_blob_client(
                    blob_path=storage_path, version=latest_version
                )
                blob_client.undelete_blob()

                # Handle restoration of root blob
                # Restore the latest version by copying it to the base blob (undelete does not work on the latest version, see ref above)
                # NOTE This creates an additional version. Better to have one additional copy than any missing.
                versioned_blob_url = f"{blob_client.url}?versionId={latest_version}"
                blob_client.start_copy_from_url(versioned_blob_url)
            except azure.core.exceptions.ResourceNotFoundError:
                logger.warning(f"Blob not found for deletion: {storage_path}")
                raise

            return self.get_lifecycle_entity(uri)
        except:
            raise


# =============================================================================
# LOCAL FILESYSTEM ADAPTOR
# =============================================================================


class LocalFilesystemAdaptor(BlobStoragePort[pathlib.Path]):
    """
    Local filesystem implementation of BlobStoragePort for development and testing.

    Provides a fully functional local storage solution that mirrors the Azure blob
    storage interface, enabling development and testing without cloud dependencies.

    Uses pathlib.Path as TStorageObject and follows the same patterns as AzureBlobAdaptor.
    """

    VERSION_DELIMITER: T.ClassVar[str] = "_"
    REGISTRY_FILE: T.ClassVar[str] = ".blob_registry.json"

    def __init__(self, root_path: pathlib.Path):
        """
        Initialize local filesystem adaptor.

        Args:
            root_path: Base directory for all blob storage operations
        """
        self.root_path = pathlib.Path(root_path)
        self.root_path.mkdir(parents=True, exist_ok=True)
        self.registry_path = self.root_path / self.REGISTRY_FILE
        self._ensure_registry_exists()

    def _ensure_registry_exists(self) -> None:
        """Ensure the blob registry file exists"""
        if not self.registry_path.exists():
            self._save_registry({})

    def _load_registry(self) -> T.Dict[str, T.Any]:
        """Load blob registry from JSON file"""
        try:
            with open(self.registry_path, "r") as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            return {}

    def _save_registry(self, registry: T.Dict[str, T.Any]) -> None:
        """Save blob registry to JSON file atomically"""
        temp_path = self.registry_path.with_suffix(".tmp")
        try:
            with open(temp_path, "w") as f:
                json.dump(registry, f, indent=2, default=str)
            temp_path.replace(self.registry_path)
        except Exception as e:
            if temp_path.exists():
                temp_path.unlink()
            raise e

    def _validate_path(self, path: str) -> None:
        """Validate path to prevent directory traversal attacks"""
        if ".." in path or path.startswith("/") or "\\" in path:
            raise ValueError(f"Invalid path: {path}")

    # =============================================================================
    # ABSTRACT METHOD IMPLEMENTATIONS
    # =============================================================================

    def _build_storage_path(
        self,
        uri: VOBlobURI,
        *,
        use_versioned_filename: bool = True,
    ) -> str:
        """
        Build filesystem storage path from domain URI.

        Format: {logical_path}/{logical_name}_{version}.{ext}
        """
        if use_versioned_filename:
            filename = uri.versioned_filename
        else:
            filename = uri.logical_name

        if uri.logical_path:
            return f"{uri.logical_path}/{filename}"
        return filename

    def _extract_logical_info_from_storage_object(
        self,
        storage_object: pathlib.Path,
        *,
        delimiter: str = "_",
    ) -> T.Tuple[T.Union[str, None], T.Union[str, None], T.Union[str, None]]:
        """
        Extract logical path, name, and version from filesystem Path object.

        Filesystem naming convention: {logical_path}/{logical_name}_{version}.{ext}

        Examples:
            Path("models/v1/weights_abc123.pt") -> ("models/v1", "weights.pt", "abc123")
            Path("data/dataset_def456.csv") -> ("data", "dataset.csv", "def456")
            Path("config_xyz789.json") -> ("", "config.json", "xyz789")

        Returns:
            Tuple of (logical_path, logical_name, version) or (None, None, None) if parsing fails
        """
        try:
            # Get relative path from root
            relative_path = storage_object.relative_to(self.root_path)
            path_str = str(relative_path)

            # Check if path contains version delimiter
            if delimiter not in path_str:
                return (None, None, None)

            # Split path and filename
            if "/" in path_str:
                path_parts = path_str.split("/")
                logical_path = "/".join(path_parts[:-1])
                filename_with_version = path_parts[-1]
            else:
                logical_path = ""
                filename_with_version = path_str

            # Extract version from filename
            # Format: {name}_{version}.{ext}
            if delimiter not in filename_with_version:
                return (None, None, None)

            # Split by last delimiter to separate name from version+extension
            name_part, version_ext = filename_with_version.rsplit(delimiter, 1)

            # Split version from extension
            if "." not in version_ext:
                # No extension, version is the whole part
                logical_name = f"{name_part}"
                version = version_ext
            else:
                # Has extension, split version from extension
                version, ext = version_ext.split(".", 1)
                logical_name = f"{name_part}.{ext}"

            # Validate extracted components
            if not logical_name or not version:
                return (None, None, None)

            return (logical_path, logical_name, version)

        except Exception as e:
            logger.debug(
                f"Failed to extract logical info from path {storage_object}: {e}"
            )
            return (None, None, None)

    def _parse_storage_object_to_metadata(
        self,
        storage_object: pathlib.Path,
        target_logical_path: str,
        target_logical_name: str,
    ) -> T.Optional[VOBlobMetadata]:
        """
        Convert filesystem Path to domain metadata.
        """
        try:
            # Extract logical info from path
            path, logical_name, logical_version = (
                self._extract_logical_info_from_storage_object(storage_object)
                or (None, None, None)
            )

            # Validate matches target
            if logical_name != target_logical_name or path != target_logical_path:
                return None
            if logical_name is None or logical_version is None:
                return None

            # Get file stats
            stat = storage_object.stat()

            # Load metadata from registry (for user tags)
            registry = self._load_registry()
            blob_key = (
                f"{target_logical_path}/{target_logical_name}"
                if target_logical_path
                else target_logical_name
            )
            user_tags = {}

            if blob_key in registry:
                for version_info in registry[blob_key]:
                    if version_info["version"] == logical_version:
                        user_tags = version_info.get("tags", {})
                        break

            return VOBlobMetadata(
                uri=VOLocalBlobURI(
                    logical_path=target_logical_path,
                    logical_name=target_logical_name,
                    version=logical_version,
                ),
                size_bytes=stat.st_size,
                created_at=dt.datetime.fromtimestamp(stat.st_ctime, tz=dt.timezone.utc),
                tags=user_tags,
            )

        except Exception as e:
            logger.debug(f"Failed to parse filesystem object {storage_object}: {e}")
            return None

    def _list_storage_objects_by_path(
        self,
        prefix: str,
    ) -> T.List[pathlib.Path]:
        """List filesystem objects with path prefix"""
        try:
            if prefix:
                search_dir = self.root_path / prefix
            else:
                search_dir = self.root_path

            if not search_dir.exists():
                return []

            # Find all files in directory and subdirectories
            files = []
            for file_path in search_dir.rglob("*"):
                if file_path.is_file() and file_path.name != self.REGISTRY_FILE:
                    files.append(file_path)

            return files

        except Exception as e:
            logger.debug(f"Failed to list objects with prefix {prefix}: {e}")
            return []

    def _execute_file_upload(
        self,
        versioned_uri: VOBlobURI,
        source_path: pathlib.Path,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        """Execute file upload to local filesystem"""
        storage_path = self._build_storage_path(versioned_uri)
        dest_path = self.root_path / storage_path

        # Create directory if needed
        dest_path.parent.mkdir(parents=True, exist_ok=True)

        # Check if file exists
        if dest_path.exists():
            raise FileExistsError(f"File already exists: {dest_path}")

        # Copy file
        shutil.copy2(source_path, dest_path)

        # Update registry with metadata
        self._update_registry(versioned_uri, upload_metadata)

    def _execute_memory_upload(
        self,
        versioned_uri: VOBlobURI,
        content: bytes,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        """Execute memory upload to local filesystem"""
        storage_path = self._build_storage_path(versioned_uri)
        dest_path = self.root_path / storage_path

        # Create directory if needed
        dest_path.parent.mkdir(parents=True, exist_ok=True)

        # Check if file exists
        if dest_path.exists():
            raise FileExistsError(f"File already exists: {dest_path}")

        # Write content to file
        with open(dest_path, "wb") as f:
            f.write(content)

        # Update registry with metadata
        self._update_registry(versioned_uri, upload_metadata)

    def _execute_download_to_file(
        self,
        versioned_uri: VOBlobURI,
        destination_path: pathlib.Path,
    ) -> pathlib.Path:
        """
        Download blob from local filesystem to a file.
        """
        storage_path = self._build_storage_path(versioned_uri)
        file_path = self.root_path / storage_path

        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {versioned_uri}")

        destination_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(file_path, destination_path)
        return destination_path

    def _execute_download_to_memory(
        self,
        versioned_uri: VOBlobURI,
        max_size_bytes: int,
    ) -> bytes:
        """Execute download from local filesystem to memory"""
        storage_path = self._build_storage_path(versioned_uri)
        file_path = self.root_path / storage_path

        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {versioned_uri}")

        # Check file size
        file_size = file_path.stat().st_size
        if file_size > max_size_bytes:
            raise ValueError(f"File size {file_size} exceeds max size {max_size_bytes}")

        # Read file content
        with open(file_path, "rb") as f:
            return f.read()

    def _execute_download_stream(
        self,
        versioned_uri: VOBlobURI,
        chunk_size: int,
    ) -> T.Iterator[bytes]:
        """
        Stream blob content from local filesystem in chunks.
        """
        storage_path = self._build_storage_path(versioned_uri)
        file_path = self.root_path / storage_path

        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {versioned_uri}")

        with open(file_path, "rb") as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                yield chunk

    def _check_blob_exists(self, versioned_uri: VOBlobURI) -> bool:
        """Check if blob exists in local filesystem"""
        storage_path = self._build_storage_path(
            versioned_uri, use_versioned_filename=True
        )
        file_path = self.root_path / storage_path
        return file_path.exists()

    def _infer_content_type(self, storage_object: pathlib.Path) -> str:
        """Infer content type from file extension"""
        content_type, _ = mimetypes.guess_type(str(storage_object))
        return content_type or "application/octet-stream"

    def _build_upload_metadata(
        self,
        uri: VOBlobURI,
        content_hash: str,
        user_tags: T.Dict[str, str],
    ) -> T.Dict[str, str]:
        """Build metadata for upload operation"""
        metadata = {
            "logical_path": uri.logical_path,
            "logical_name": uri.logical_name,
            "content_hash": content_hash,
            "upload_time": dt.datetime.now(dt.timezone.utc).isoformat(),
            **user_tags,
        }
        return metadata

    def _resolve_versioned_uri(self, source: VOBlobURI) -> VOBlobURI:
        """Resolve versioned URI from source URI. If latest, gets the latest version"""
        if source.version != "latest":
            return source

        # Find latest version from registry
        registry = self._load_registry()
        blob_key = (
            f"{source.logical_path}/{source.logical_name}"
            if source.logical_path
            else source.logical_name
        )

        if blob_key not in registry or not registry[blob_key]:
            raise FileNotFoundError(f"No versions found for {source}")

        # Get latest version (first in sorted list)
        versions = sorted(
            registry[blob_key], key=lambda x: x["created_at"], reverse=True
        )
        latest_version = versions[0]["version"]

        return source.model_copy(update={"version": latest_version})

    # =============================================================================
    # HELPER METHODS
    # =============================================================================

    def _update_registry(self, uri: VOBlobURI, metadata: T.Dict[str, str]) -> None:
        """Update registry with blob metadata"""
        registry = self._load_registry()
        blob_key = (
            f"{uri.logical_path}/{uri.logical_name}"
            if uri.logical_path
            else uri.logical_name
        )

        if blob_key not in registry:
            registry[blob_key] = []

        # Check if version already exists
        for i, version_info in enumerate(registry[blob_key]):
            if version_info["version"] == uri.version:
                # Update existing version
                registry[blob_key][i] = {
                    "version": uri.version,
                    "size_bytes": (self.root_path / self._build_storage_path(uri))
                    .stat()
                    .st_size,
                    "created_at": dt.datetime.now(dt.timezone.utc).isoformat(),
                    "tags": {
                        k: v
                        for k, v in metadata.items()
                        if k
                        not in [
                            "logical_path",
                            "logical_name",
                            "content_hash",
                            "upload_time",
                        ]
                    },
                }
                self._save_registry(registry)
                return

        # Add new version
        registry[blob_key].append(
            {
                "version": uri.version,
                "size_bytes": (self.root_path / self._build_storage_path(uri))
                .stat()
                .st_size,
                "created_at": dt.datetime.now(dt.timezone.utc).isoformat(),
                "tags": {
                    k: v
                    for k, v in metadata.items()
                    if k
                    not in [
                        "logical_path",
                        "logical_name",
                        "content_hash",
                        "upload_time",
                    ]
                },
            }
        )

        self._save_registry(registry)

    def list_blobs(
        self,
        path: str = "",
        *,
        tags: T.Optional[T.Dict[str, str]] = None,
    ) -> T.List[VOBlobURI]:
        """
        List blobs with optional filtering by path prefix and tags.

        Returns latest version of each blob by default.
        """
        try:
            registry = self._load_registry()
            uri_list = []
            seen_logical_blobs = set()

            for blob_key, versions in registry.items():
                if not versions:
                    continue

                # Apply path filter
                if path and not blob_key.startswith(path):
                    continue

                # Get latest version (first in sorted list)
                versions_sorted = sorted(
                    versions, key=lambda x: x["created_at"], reverse=True
                )
                latest_version = versions_sorted[0]

                # Apply tag filter if specified
                if tags:
                    blob_tags = latest_version.get("tags", {})
                    if not all(blob_tags.get(k) == v for k, v in tags.items()):
                        continue

                # Parse blob_key back to path and name
                if "/" in blob_key:
                    parts = blob_key.split("/")
                    logical_name = parts[-1]
                    logical_path = "/".join(parts[:-1])
                else:
                    logical_name = blob_key
                    logical_path = ""

                # Create BlobURI
                uri = VOLocalBlobURI(
                    logical_path=logical_path,
                    logical_name=logical_name,
                    version=latest_version["version"],
                )
                uri_list.append(uri)
                seen_logical_blobs.add(blob_key)

            return uri_list

        except Exception as e:
            logger.error(f"Failed to list blobs: {e}")
            raise

    def delete(self, uri: VOBlobURI):
        """
        Delete blob(s) from local filesystem.

        Deletes all versions of the blob and removes from registry.
        """
        try:
            # Get all versions from lifecycle
            lifecycle = self.get_lifecycle_entity(uri)

            # Delete all version files
            for metadata in lifecycle.versions:
                storage_path = self._build_storage_path(metadata.uri)
                file_path = self.root_path / storage_path

                if file_path.exists():
                    file_path.unlink()
                    logger.debug(f"Deleted file: {file_path}")

            # Remove from registry
            registry = self._load_registry()
            blob_key = (
                f"{uri.logical_path}/{uri.logical_name}"
                if uri.logical_path
                else uri.logical_name
            )

            if blob_key in registry:
                del registry[blob_key]
                self._save_registry(registry)
                logger.info(f"Deleted blob from registry: {blob_key}")

        except FileNotFoundError:
            logger.warning(f"Blob not found for deletion: {uri}")
            raise
        except Exception as e:
            logger.error(f"Error in delete for {uri}: {e}")
            raise
