from __future__ import annotations

import abc
import datetime as dt
import logging
import pathlib
import tempfile
import typing as T

from . import general as util
from .valueobjects import *
from .entities import *


# Set the logger
logger = logging.getLogger(__name__)

TStorageObject = T.TypeVar("TStorageObject", bound=T.Any)


class BlobStoragePort(T.Generic[TStorageObject], abc.ABC):
    """
    Generic port for blob storage operations using URI-based design.

    Provides a clean, consistent interface for blob storage across different
    backends (Azure, Google Cloud, local filesystem, etc.) using BlobUri
    as the canonical identifier for all operations.

    All operations are idempotent where possible and support content-based
    versioning for data integrity and deduplication.
    """

    # =================================================================
    # ABSTRACT METHODS (Backend-Specific: f_B and g_B)
    # =================================================================
    @abc.abstractmethod
    def _build_storage_path(
        self,
        uri: VOBlobURI,
        *,
        use_versioned_filename: bool = True,
    ) -> str:
        """
        Build backend-specific storage path from domain URI.

        Pure function - deterministic path construction.
        """
        pass

    @abc.abstractmethod
    def _parse_storage_object_to_metadata(
        self,
        storage_object: TStorageObject,
        target_logical_path: str,
        target_logical_name: str,
    ) -> T.Optional[VOBlobMetadata]:
        """
        Parse backend-specific storage object into domain metadata.

        Returns None if object doesn't match target pattern.
        Pure function - no side effects.
        """
        pass

    @abc.abstractmethod
    def _list_storage_objects_by_path(self, prefix: str) -> T.List[TStorageObject]:
        """List all storage objects with a given path. Used to get blob files independent of versions"""
        pass

    @abc.abstractmethod
    def _execute_file_upload(
        self,
        versioned_uri: VOBlobURI,
        source_path: pathlib.Path,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        """Execute the actual upload to storage"""
        pass

    @abc.abstractmethod
    def _execute_memory_upload(
        self,
        versioned_uri: VOBlobURI,
        content: bytes,
        upload_metadata: T.Dict[str, str],
    ) -> None:
        """Execute the actual upload to storage from memory"""
        pass

    @abc.abstractmethod
    def _check_blob_exists(self, versioned_uri: VOBlobURI) -> bool:
        """Check if blob exists"""
        pass

    @abc.abstractmethod
    def _infer_content_type(self, storage_object: TStorageObject) -> str:
        """Infer content type from storage object"""
        pass

    @abc.abstractmethod
    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"""
        pass

    @abc.abstractmethod
    def _resolve_versioned_uri(self, source: VOBlobURI) -> VOBlobURI:
        """Resolve versioned URI from source URI.
        If latest is passed it, gets the latest version
        """
        pass

    @abc.abstractmethod
    def _execute_download_to_file(
        self,
        versioned_uri: VOBlobURI,
        destination_path: pathlib.Path,
    ) -> pathlib.Path:
        """
        Backend specific download implementation

        Raises:
            FileNotFoundError: If blob doesn't exist
            PermissionError: if insufficient permissions
        """
        pass

    @abc.abstractmethod
    def _execute_download_to_memory(
        self,
        versioned_uri: VOBlobURI,
        max_size_bytes: int,
    ) -> bytes:
        """
        Backend specific download implementation

        Raises:
            FileNotFoundError: If blob doesn't exist
            ValueError: If blob exceeds max_size_bytes
        """
        pass

    @abc.abstractmethod
    def _execute_download_stream(
        self,
        versioned_uri: VOBlobURI,
        chunk_size: int,
    ) -> T.Iterator[bytes]:
        """Backend specific streaming download implementation"""
        pass

    @abc.abstractmethod
    def _extract_logical_info_from_storage_object(
        self,
        storage_object: TStorageObject,
        *,
        delimiter: str = "_",
    ) -> T.Tuple[T.Union[str, None], T.Union[str, None], T.Union[str, None]]:
        """
        Given a storage object, extract the logical path, name, and version.
        Pure function - no side effects.

        Example:
            "models/v1/model_abc123.pt" -> ("models/v1", "model.pt", "abc123")
        """
        pass

    def build_entity_from_storageobjects(
        self,
        storage_objects: T.List[TStorageObject],
        target_logical_path: str,
        target_logical_name: str,
        *,
        tag_filters: T.Optional[T.Dict[str, str]] = None,
    ) -> ENTBlobLifecycle:
        """
        Template method: h(g_B*(𝒮*)) → ℒ

        Pure domain aggregation - identical logic across backends.
        Delegates parsing to backend-specific _parse_storage_object_to_metadata.
        """
        metadata_list = []
        for obj in storage_objects:
            metadata = self._parse_storage_object_to_metadata(
                obj, target_logical_path, target_logical_name
            )
            # Filter
            if metadata is None:
                continue
            metadata_list.append(metadata)

        if not metadata_list:
            raise FileNotFoundError(
                f"No valid versions found for {target_logical_path}/{target_logical_name}"
            )
        return ENTBlobLifecycle.create(metadata_list)

    # =================================================================
    # SHARED INVARIANT METHODS (Storage-Agnostic: h and domain operations)
    # Implemented as stateless as possible.
    # Organized in class to enable downstream overrides required for any adaptor
    # =================================================================

    @staticmethod
    def _validate_uri_for_download(
        uri: VOBlobURI,
    ) -> None:
        if not uri.logical_name:
            raise ValueError("Logical name cannot be empty")

    @staticmethod
    def _generate_default_download_path(uri: VOBlobURI) -> pathlib.Path:
        # Generate a temp folder in sys temp
        base_dir = pathlib.Path(tempfile.gettempdir()) / "blob_downloads"
        base_dir.mkdir(parents=True, exist_ok=True)

        # use logical path as subdirectory strucutre
        download_dir = base_dir if not uri.logical_path else base_dir / uri.logical_path
        download_dir.mkdir(parents=True, exist_ok=True)

        # use logical name as file name without version
        return download_dir / uri.logical_name

    @staticmethod
    def _validate_uri_for_upload(uri: VOBlobURI) -> None:
        """
        Pure domain validation - identical across all backends.

        Mathematical constraint: uri ∈ Valid(𝒰)
        """
        if not uri.logical_name.strip():
            raise ValueError("Logical name cannot be empty")

        if not uri.version.strip():
            raise ValueError("Version cannot be empty")

        # Universal illegal characters for blob storage
        illegal_chars = ["\\", ":", "*", "?", '"', "<", ">", "|"]
        for char in illegal_chars:
            if char in uri.logical_path or char in uri.logical_name:
                raise ValueError(f"Illegal character '{char}' in URI")

    @staticmethod
    def _matches_tag_filters(
        metadata: VOBlobMetadata,
        tag_filters: T.Dict[str, str],
    ) -> bool:
        """
        Pure function: ℳ × TagFilters → Boolean

        Domain logic for tag matching - identical across backends.
        """
        if not tag_filters:
            return True

        for k, v in tag_filters.items():
            if k not in metadata.tags or metadata.tags[k] != v:
                return False

        return True

    @staticmethod
    def _normalize_datetime_to_utc(dt_obj: T.Optional[dt.datetime]) -> dt.datetime:
        """Normalize datetime to UTC"""
        if dt_obj is None:
            return dt.datetime.now(dt.timezone.utc)

        if dt_obj.tzinfo is None:
            return dt_obj.replace(tzinfo=dt.timezone.utc)

        return dt_obj.astimezone(dt.timezone.utc)

    @staticmethod
    def _generate_base_upload_tags(blob: VOBlobURI, version: str) -> T.Dict[str, str]:
        """Generate base tags for a blob upload"""
        now_iso = dt.datetime.now(dt.timezone.utc).isoformat()

        # More comprehensive metadata
        return {
            # Temporal metadata
            "date_modified": now_iso,
            "date_created": now_iso,  # For new blobs
            # Metadata
            "logical_path": blob.logical_path,
            "logical_name": blob.logical_name,
            "version": version,
        }

    # =================================================================
    # PUBLIC TEMPLATE METHODS (Compose f_B, g_B, h)
    # =================================================================

    def upload_from_file(
        self,
        destination: VOBlobURI,
        source_path: pathlib.Path,
        *,
        tags: T.Dict[str, str] = {},
    ) -> ENTBlobLifecycle:
        """
        Template method for file upload workflow.

        Orchestates:
        1. Validation
        2. Content Hashing if destination version is arbitrary "latest"
        3. Versioned URI Construction
        4. Existence Check
        5. Metadata Construction
        6. Backend-Specific Upload Execution
        7. Lifecycle Entity Construction
        """
        # VALIDATION
        self._validate_uri_for_upload(destination)
        if not source_path.exists():
            raise FileNotFoundError(f"Source file not found: {source_path}")

        # CONTENT HASH AND VERSIONING
        content_hash = util.generate_content_hash(source_path)
        versioned_uri = (
            destination.model_copy(update={"version": content_hash})
            if destination.version == "latest"
            else destination
        )

        # EXISTENCE CHECK
        if self._check_blob_exists(versioned_uri):
            logger.info(f"Blob already exists: {versioned_uri}")
            return self.get_lifecycle_entity(versioned_uri)

        # UPLOAD METADATA
        upload_metadata = self._build_upload_metadata(versioned_uri, content_hash, tags)

        # EXECUTE BACKEND SPECIFIC UPLOAD
        self._execute_file_upload(versioned_uri, source_path, upload_metadata)

        return self.get_lifecycle_entity(versioned_uri)

    def upload_from_memory(
        self,
        destination: VOBlobURI,
        content: bytes,
        *,
        tags: T.Dict[str, str] = {},
    ) -> ENTBlobLifecycle:
        """
        Template method for memory upload workflow.

        Orchaestrates:
        1. Validation
        2. Content Hashing if destination version is arbitrary "latest"
        3. Versioned URI Construction
        4. Existence Check
        5. Metadata Construction
        6. Backend-Specific Upload Execution
        7. Lifecycle Entity Construction
        """
        # VALIDATION
        self._validate_uri_for_upload(destination)

        # CONTENT HASH AND VERSIONING
        content_hash = util.generate_content_hash(content)
        versioned_uri = (
            destination.model_copy(update={"version": content_hash})
            if destination.version == "latest"
            else destination
        )

        # EXISTENCE CHECK
        if self._check_blob_exists(versioned_uri):
            logger.info(f"Blob already exists: {versioned_uri}")
            return self.get_lifecycle_entity(versioned_uri)

        # UPLOAD METADATA
        upload_metadata = self._build_upload_metadata(versioned_uri, content_hash, tags)

        # EXECUTE BACKEND SPECIFIC UPLOAD
        self._execute_memory_upload(versioned_uri, content, upload_metadata)

        return self.get_lifecycle_entity(versioned_uri)

    def download_to_file(
        self,
        source: VOBlobURI,
        destination_path: T.Optional[pathlib.Path] = None,
    ) -> pathlib.Path:
        """
        Templated method for file download workflow

        Orchestrates:
        1. Validation
        2. Version Resolution
        3. Backend-Specific Download Execution

        Raises:
            FileNotFoundError: If blob doesn't exist
            ValueError: If source URI is invalid
            PermissionError: If insufficient storage permissions
        """
        try:
            # Validate
            self._validate_uri_for_download(source)

            # Version
            versioned_uri = self._resolve_versioned_uri(source)
            destination_path = destination_path or self._generate_default_download_path(
                versioned_uri
            )
            destination_path.parent.mkdir(parents=True, exist_ok=True)

            # Execute
            self._execute_download_to_file(versioned_uri, destination_path)

            return destination_path

        except Exception as e:
            logger.error(f"Failed to download {source} to {destination_path}: {e}")
            raise

    def download_to_memory(
        self,
        source: VOBlobURI,
        *,
        max_size_bytes: int = 100 * 1024 * 1024,  # 100mb
    ) -> bytes:
        """
        Download blob content from storage as bytes.

        Orchestrates:
        1. Validation
        2. Version Resolution
        3. Size Check
        4. Backend-Specific Download Execution
        """
        try:
            # VALIDATE
            self._validate_uri_for_download(source)

            # RESOLVE VERSION
            versioned_uri = self._resolve_versioned_uri(source)

            # CHECK SIZE
            metadata = self.get_lifecycle_entity(versioned_uri)
            size = metadata.get_version(versioned_uri.version).size_bytes
            if size > max_size_bytes:
                raise ValueError(
                    f"Blob too large: {size} bytes > {max_size_bytes} bytes. Use download_as_stream, download_as_file or increase max_size_bytes."
                )

            # EXECUTE
            return self._execute_download_to_memory(versioned_uri, max_size_bytes)

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

    def download_as_stream(
        self,
        source: VOBlobURI,
        chunk_size: int = 8192,
    ) -> T.Iterator[bytes]:
        """Streaming for memory-efficient processing of large files"""
        try:
            # VALIDATE
            self._validate_uri_for_download(source)

            # RESOLVE VERSION
            versioned_uri = self._resolve_versioned_uri(source)

            # EXECUTE
            return self._execute_download_stream(versioned_uri, chunk_size)

        except Exception as e:
            logger.error(f"Failed to stream download {source}: {e}")
            raise

    @abc.abstractmethod
    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.

        Args:
            prefix: path prefix to filter results (matches against BlobUri.full_path). If none, given, lists blobs in root
            tags: Optional tag filters (all specified tags must match)

        Returns:
            List of blob URIs for matching blobs. Only the latest version is returned.

        Examples:
            - list_blobs() -> All blobs
            - list_blobs(prefix="models/") -> All blobs under models/ path
            - list_blobs(tags={"env": "prod"}) -> All blobs tagged with env=prod
        """
        pass

    @abc.abstractmethod
    def delete(self, uri: VOBlobURI):
        """
        Delete blob(s) from storage.

        Ignores uri.version as it deletes/archives the entire blob.

        Args:
            uri: BlobUri identifying what to delete


        Examples:
            - delete(BlobUri(path="models", name="weights.bin")) -> Delete all versions
            - delete(BlobUri(path="models", name="weights.bin", version="abc123")) -> Delete specific version
        """
        pass

    def get_lifecycle_entity(
        self,
        uri: VOBlobURI,
        *,
        tag_filters: T.Optional[T.Dict[str, str]] = None,
    ) -> ENTBlobLifecycle:
        """
        Template method for lifecycle retreival.
        Ignores version in URI.

        Orchestrates:
        - prefix
        - list
        - parse & aggregate
        """
        try:
            # Get path
            logical_path = uri.logical_path

            # LIST STORAGE OBJECTS
            # Get all storage objects in path and filter by logical name
            storage_objects = self._list_storage_objects_by_path(logical_path)

            filtered_storage_objects = list(
                filter(
                    lambda obj: self._extract_logical_info_from_storage_object(obj)[1]
                    == uri.logical_name,
                    storage_objects,
                )
            )
            if not filtered_storage_objects:
                raise FileNotFoundError(f"No files found for {uri}")

            # PARSE AND AGGREGATE
            # h(g_B*(objects)) → lifecycle
            return self.build_entity_from_storageobjects(
                filtered_storage_objects,
                uri.logical_path,
                uri.logical_name,
                tag_filters=tag_filters,
            )

        except Exception as e:
            logger.error(f"Failed to get lifecycle for {uri}: {e}")
            raise
