from __future__ import annotations

import datetime as dt
import typing as T
import pydantic


class PydanticVO(pydantic.BaseModel):
    model_config = pydantic.ConfigDict(
        frozen=True, arbitrary_types_allowed=False, use_enum_values=True, extra="forbid"
    )


class VOBlobURI(PydanticVO):
    """
    Immutable blob identifier using URI-like syntax with path/name separation.

    Provides a unified way to identify blobs across different storage backends
    with support for hierarchical paths and optional versioning.

    Examples:
        - Root level file: BlobUri(path="", name="model.pt")
        - Nested file: BlobUri(path="models/v1", name="weights.bin")
        - With version: BlobUri(path="data", name="dataset.csv", version="abc123")
        - Parse from string: BlobUri.parse("models/v1/weights.bin:abc123")
    """

    VERSION_DELIMITER: T.ClassVar[str] = "_"

    logical_path: str = pydantic.Field(
        description="Virtual path to blob directory (empty string for root level)",
        examples=["", "models/v1", "training/data", "experiments/2025-01"],
    )

    logical_name: str = pydantic.Field(
        description="Blob filename with extension",
        examples=["model.pt", "dataset.parquet", "config.json", "weights.bin"],
    )

    version: str = pydantic.Field(
        default="latest",
        description="version identifier. Often a content hash. Underscore NOT allowed. See validator for more details. If none given, uses `latest`",
        examples=["abc123def456", "v1", "20250118-143022"],
    )

    # VALIDATION

    # @pydantic.field_validator("version")
    # def validate_version(cls, v):
    #     illegal_chars = [".", "_", ":", "/", "\\"]
    #     if v is not None and any(c in v for c in illegal_chars):
    #         raise ValueError(
    #             f"Version cannot contain illegal characters: {illegal_chars}"
    #         )
    #     return v

    @pydantic.field_validator("logical_name")
    def validate_name_no_slash(cls, v):
        illegal_chars = ["/", "\\"]
        if any(c in v for c in illegal_chars):
            raise ValueError(f"Name cannot contain illegal characters: {illegal_chars}")
        return v

    @pydantic.field_validator("logical_name")
    def validate_name_has_filetype(cls, v):
        if "." not in v:
            raise ValueError(f"Name must contain a file extension: {v}")
        return v


class VOLocalBlobURI(VOBlobURI):
    # DERIVED ATTRIBUTES
    @property
    def storage_path(self) -> str:
        """
        Uses filename versioning for storage
        """
        return self._get_full_path(use_versioned_filename=True)

    @property
    def versioned_filename(self) -> str:
        """Adds version to prefix"""
        # split by filename and filetype
        filename, filetype = self.logical_name.rsplit(".", maxsplit=1)
        versioned_filename = (
            f"{filename}{self.__class__.VERSION_DELIMITER}{self.version}.{filetype}"
        )
        return versioned_filename

    def _get_full_path(self, *, use_versioned_filename: bool = True) -> str:
        """Get full path, optionally including version"""
        if use_versioned_filename:
            return f"{self.logical_path}/{self.versioned_filename}"
        return f"{self.logical_path}/{self.logical_name}"

    @classmethod
    def parse_versioned_filename(
        cls, versioned_filename: str, *, version_delimiter: T.Optional[str] = None
    ) -> T.Tuple[str, str]:
        """
        Parse a versioned filename to return filename, version

        Returns:
            filename, version

        Example:
            - "model_v1.pt" -> ("model.pt", "v1")
            - "data/dataset_abc123.csv" -> ("data/dataset.csv", "abc123")
        """
        version_delimiter = version_delimiter or cls.VERSION_DELIMITER
        filename, filetype = versioned_filename.rsplit(".", maxsplit=1)
        filename, version = filename.rsplit(version_delimiter, maxsplit=1)
        return f"{filename}.{filetype}", version

    @classmethod
    def parse_versioned_filepath(
        cls,
        versioned_filepath: str,
    ) -> T.Tuple[str, str, str]:
        """
        Parse a versioned filepath to return path, filename, version
        """
        if "/" in versioned_filepath:
            filepath, filename = versioned_filepath.rsplit("/", maxsplit=1)
        else:
            filepath = ""
            filename = versioned_filepath
        filename, version = cls.parse_versioned_filename(filename)
        return filepath, filename, version

    @classmethod
    def from_versioned_path(
        cls,
        versioned_path: str,
    ) -> VOBlobURI:
        """
        Parse a versioned path to return VOBlob object
        """
        path, filename, version = cls.parse_versioned_filepath(versioned_path)
        return cls(logical_path=path, logical_name=filename, version=version)


class VOBlobMetadata(PydanticVO):
    """
    Immutable blob metadata using BlobUri as canonical identifier.

    Contains all essential information about a stored blob including size,
    timestamps, content type, and user-defined tags. The BlobUri provides
    a unified way to identify and reference the blob.

    All attributes are index-aligned with the versions list.

    """

    uri: VOBlobURI = pydantic.Field(
        description="Canonical blob identifier with path and name. Version is defaulted to latest.",
    )

    size_bytes: int = pydantic.Field(
        description="Size of blob content in bytes",
        examples=[1048576, 2048, 1073741824],  # 1MB, 2KB, 1GB
    )
    created_at: dt.datetime = pydantic.Field(
        description="Timestamp when blob was created (UTC)",
        examples=[dt.datetime(2025, 1, 18, 14, 30, 22)],
    )
    tags: T.Dict[str, str] = pydantic.Field(
        description="User-defined metadata tags for filtering and organization",
        examples=[
            {"experiment_id": "exp_001", "model_type": "transformer"},
            {"dataset": "production", "version": "v2.1"},
            {"project": "airport_sim", "env": "staging"},
        ],
    )

    @pydantic.field_validator("created_at", mode="before")
    def validate_created_at(cls, v):
        """
        # DT: 1) check if in iso string. if yes, then cast to datetime.
        # DT: 2) if datetime tz naive, then convert to utc
        # DT: 3) if datetime tz aware, then convert to utc
        # DT: 4) if datetime tz aware and utc, then do nothing
        # DT: 5) if datetime tz aware and not utc, then convert to utc
        # DT: 6) if datetime tz aware and utc, then do nothing

        """
        if isinstance(v, str):
            return dt.datetime.fromisoformat(v)
        elif isinstance(v, dt.datetime):
            if v.tzinfo is None:
                return v.replace(tzinfo=dt.timezone.utc)
            elif v.tzinfo != dt.timezone.utc:
                return v.astimezone(dt.timezone.utc)
            else:
                return v
        else:
            raise ValueError(
                "created_at must be a datetime object or an ISO 8601 string"
            )

    def matches_tags(self, tag_filters: T.Dict[str, str]) -> bool:
        """Check if blob matches tag filters"""
        return all(self.tags.get(k) == v for k, v in tag_filters.items())
