"""Storage abstraction layer for screenshot artifacts.

This module provides Protocol-based interfaces for storage backends,
enabling dependency injection and testability.
"""

import logging
from collections.abc import Iterator
from pathlib import Path
from typing import Any, Protocol, runtime_checkable

logger = logging.getLogger(__name__)


@runtime_checkable
class ArtifactStorage(Protocol):
    """Protocol for storage backends that handle screenshot artifacts.

    This protocol defines the interface for downloading and uploading
    screenshot results to/from various storage backends (Azure Blob,
    S3, local filesystem, etc.).
    """

    def download_file(self, blob_name: str, dest: Path) -> bool:
        """Download a single file from storage.

        Args:
            blob_name: Name/path of the blob to download.
            dest: Local destination path. Parent directories created automatically.

        Returns:
            True if download succeeded, False otherwise.

        Example:
            >>> storage = AzureBlobStorage(client)
            >>> success = storage.download_file("batch-123/job-1/screenshot.png", Path("output/screenshot.png"))
        """
        ...

    def download_prefix(self, prefix: str, dest_dir: Path) -> bool:
        """Download all files with a given prefix to a directory.

        Args:
            prefix: Blob name prefix (e.g., "batch-123/job-1/").
            dest_dir: Local directory to download files into.

        Returns:
            True if all downloads succeeded, False if any failed.

        Example:
            >>> storage.download_prefix("batch-123/", Path("output/batch-123"))
        """
        ...

    def upload_file(self, src: Path, blob_path: str) -> str:
        """Upload a file to storage.

        Args:
            src: Local file path to upload.
            blob_path: Destination blob name/path in storage.

        Returns:
            Public URL of the uploaded blob.

        Raises:
            IOError: If upload fails.

        Example:
            >>> url = storage.upload_file(Path("results.json"), "batch-123/results.json")
            >>> print(url)  # https://storage.example.com/batch-123/results.json
        """
        ...

    def list_blobs(self, prefix: str) -> Iterator[str]:
        """List all blob names with a given prefix.

        Args:
            prefix: Blob name prefix to filter by.

        Yields:
            Blob names/paths matching the prefix.

        Example:
            >>> for blob in storage.list_blobs("batch-123/"):
            ...     print(blob)  # batch-123/job-1/screenshot.png
        """
        ...


class NullStorage:
    """No-op storage implementation for when storage is disabled.

    This class provides a safe fallback when download/upload functionality
    is not needed or not configured.
    """

    def download_file(self, blob_name: str, dest: Path) -> bool:
        """Always returns False (no download performed)."""
        return False

    def download_prefix(self, prefix: str, dest_dir: Path) -> bool:
        """Always returns False (no download performed)."""
        return False

    def upload_file(self, src: Path, blob_path: str) -> str:
        """Raises RuntimeError (upload not supported)."""
        raise RuntimeError("NullStorage does not support uploads")

    def list_blobs(self, prefix: str) -> Iterator[str]:
        """Yields no blobs."""
        return
        yield  # Make this a generator


class LocalFileStorage:
    """Local filesystem storage implementation for testing.

    This implementation stores files in a local directory, mirroring
    the structure of blob storage for testing purposes.

    Attributes:
        base_dir: Root directory for all storage operations.
    """

    def __init__(self, base_dir: Path) -> None:
        """Initialize local file storage.

        Args:
            base_dir: Base directory for file storage.
        """
        self.base_dir = Path(base_dir)
        self.base_dir.mkdir(parents=True, exist_ok=True)

    def download_file(self, blob_name: str, dest: Path) -> bool:
        """Copy file from base_dir to dest."""
        src = self.base_dir / blob_name
        if not src.exists():
            return False
        dest.parent.mkdir(parents=True, exist_ok=True)
        try:
            import shutil

            shutil.copy2(src, dest)
            return True
        except Exception:
            return False

    def download_prefix(self, prefix: str, dest_dir: Path) -> bool:
        """Copy all files matching prefix to dest_dir."""
        src_dir = self.base_dir / prefix
        if not src_dir.exists():
            return False
        dest_dir.mkdir(parents=True, exist_ok=True)
        try:
            import shutil

            shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True)
            return True
        except Exception:
            return False

    def upload_file(self, src: Path, blob_path: str) -> str:
        """Copy file to base_dir under blob_path."""
        dest = self.base_dir / blob_path
        dest.parent.mkdir(parents=True, exist_ok=True)
        import shutil

        shutil.copy2(src, dest)
        return f"file://{dest.absolute()}"

    def list_blobs(self, prefix: str) -> Iterator[str]:
        """List all files under prefix."""
        prefix_dir = self.base_dir / prefix
        if not prefix_dir.exists():
            return
        for path in prefix_dir.rglob("*"):
            if path.is_file():
                yield str(path.relative_to(self.base_dir))


class AzureBlobStorage:
    """Azure Blob Storage implementation of ArtifactStorage.

    This adapter wraps an Azure storage client from infra_core.azure.storage
    and implements the ArtifactStorage protocol.

    Attributes:
        client: Azure storage client instance.
    """

    def __init__(self, client: Any, *, container_name: str | None = None) -> None:
        """Initialize Azure blob storage adapter.

        Args:
            client: Azure storage client from infra_core.azure.storage.
        """
        self._client = client
        self.container_name = container_name

    def download_file(self, blob_name: str, dest: Path) -> bool:
        """Download a single blob to local file."""
        if not self._client.is_configured():
            logger.debug("Azure storage client not configured")
            return False

        container = self._client.container()
        if not container:
            logger.debug("Azure storage container not available")
            return False

        try:
            downloader = container.download_blob(blob_name)
        except Exception:
            logger.debug(
                "Failed to download blob %s from Azure storage",
                blob_name,
                exc_info=True,
            )
            return False

        try:
            dest.parent.mkdir(parents=True, exist_ok=True)
            dest.write_bytes(downloader.readall())
            return True
        except Exception:
            logger.debug(
                "Failed to write downloaded blob %s to %s",
                blob_name,
                dest,
                exc_info=True,
            )
            return False

    def download_prefix(self, prefix: str, dest_dir: Path) -> bool:
        """Download all blobs with given prefix."""
        if not self._client.is_configured():
            return False

        container = self._client.container()
        if not container:
            return False

        try:
            blobs = list(container.list_blobs(name_starts_with=prefix))
            if not blobs:
                return False

            success_count = 0
            for blob in blobs:
                blob_name = blob.name
                relative_path = blob_name[len(prefix) :].lstrip("/")
                dest_path = dest_dir / relative_path
                if self.download_file(blob_name, dest_path):
                    success_count += 1

            return success_count > 0
        except Exception:
            logger.debug(
                "Failed to download prefix %s from Azure storage",
                prefix,
                exc_info=True,
            )
            return False

    def upload_file(self, src: Path, blob_path: str) -> str:
        """Upload file to Azure blob storage."""
        if not src.exists():
            raise OSError(f"Source file does not exist: {src}")

        if not self._client.is_configured():
            raise RuntimeError("Azure storage client not configured")

        # Use the client's upload_file method
        self._client.upload_file(src, blob_path=blob_path)

        # Construct blob URL
        container = self._client.container()
        if container:
            return f"{container.url}/{blob_path}"
        return blob_path

    def list_blobs(self, prefix: str) -> Iterator[str]:
        """List all blobs with given prefix."""
        if not self._client.is_configured():
            return

        container = self._client.container()
        if not container:
            return

        try:
            for blob in container.list_blobs(name_starts_with=prefix):
                yield blob.name
        except Exception:
            logger.debug(
                "Failed to list blobs with prefix %s",
                prefix,
                exc_info=True,
            )
            return


def create_azure_storage(
    connection_string: str | None = None,
    container: str | None = None,
) -> AzureBlobStorage | None:
    """Create Azure blob storage adapter from environment or parameters.

    Args:
        connection_string: Azure storage connection string. If None, reads from env.
        container: Container name. If None, uses default.

    Returns:
        AzureBlobStorage instance if Azure SDK is available and configured,
        None otherwise.

    Example:
        >>> storage = create_azure_storage()
        >>> if storage:
        ...     storage.download_file("batch-123/result.json", Path("result.json"))
    """
    try:
        from infra_core.azure import storage as azure_storage
    except ImportError:
        logger.debug("infra_core.azure.storage not available")
        return None

    import os

    conn = connection_string or os.getenv("SCREENSHOT_AZURE_STORAGE_CONNECTION_STRING")
    cont = container or os.getenv("SCREENSHOT_AZURE_STORAGE_CONTAINER") or "screenshot-output"

    if not conn:
        logger.debug("No Azure storage connection string configured")
        return None

    settings = azure_storage.AzureStorageSettings(
        container=cont,
        connection_string=conn,
    )

    client = azure_storage.get_client(settings=settings)
    return AzureBlobStorage(client, container_name=cont)
