"""Core Pydantic models for Grizabella schema definitions and instances."""
from datetime import datetime, timezone
from enum import Enum  # Standard library import first
from typing import Any, Optional
from uuid import UUID, uuid4
from decimal import Decimal

from pydantic import BaseModel, ConfigDict, Field, field_validator


# --- Enums ---
class PropertyDataType(str, Enum):
    """Enumeration of supported data types for object and relation properties.

    This enum defines the set of primitive data types that can be used when
    defining properties for `ObjectTypeDefinition` and `RelationTypeDefinition`.
    These types guide data validation and storage.

    Attributes:
        TEXT: A string of text.
        INTEGER: A whole number.
        FLOAT: A floating-point number.
        BOOLEAN: A true or false value.
        DATETIME: A date and time value, typically stored in UTC.
        BLOB: Binary Large Object, for storing raw binary data.
        JSON: A JSON object or array, for semi-structured data.
        UUID: A universally unique identifier.

    """

    TEXT = "TEXT"
    INTEGER = "INTEGER"
    FLOAT = "FLOAT"
    BOOLEAN = "BOOLEAN"
    DATETIME = "DATETIME"
    BLOB = "BLOB"
    JSON = "JSON"
    UUID = "UUID" # For explicit UUID type properties

# --- Base Metadata Model ---
class MemoryInstance(BaseModel):
    """Base model for all storable instances, providing common metadata.

    This model includes fields that are common to all persistent entities
    within Grizabella, such as a unique identifier, a weight for ranking or
    relevance, and an upsert timestamp.

    Attributes:
        id (UUID): :noindex: A unique identifier for the instance, generated by default.

        weight (condecimal): :noindex: A decimal value between 0 and 10 (inclusive)
            representing the importance or relevance of the instance.
            Used for ranking or filtering. Defaults to 1.0.

        upsert_date (datetime): :noindex: The timestamp (UTC) when the instance was
            last created or updated. Automatically set on creation/update.

    
    """

    id: UUID = Field(default_factory=uuid4)
    weight: Decimal = Field(default=Decimal("1.0"), ge=0, le=10)
    upsert_date: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc),
    )

    model_config = ConfigDict( # pylint: disable=R0903
        validate_assignment=True, # Ensure fields are validated on assignment
        arbitrary_types_allowed=True, # To allow condecimal
    )

# --- Definition Models ---
class PropertyDefinition(BaseModel):
    """Defines a single property within an `ObjectTypeDefinition` or `RelationTypeDefinition`.

    This model specifies the characteristics of a property, such as its name,
    data type, and constraints (e.g., nullable, unique, indexed).

    Attributes:
        name (str): :noindex: The name of the property (e.g., 'title', 'age').
            This name is used to access the property's value in instances.

        data_type (PropertyDataType): :noindex: The data type of the property.

        is_primary_key (bool): :noindex: Indicates if this property serves as a primary
            key for its ``ObjectTypeDefinition``. Defaults to False.
            An ``ObjectTypeDefinition`` can have at most one primary key.

        is_nullable (bool): :noindex: Specifies whether this property can have a null
            value. Defaults to True.

        is_indexed (bool): :noindex: Indicates if this property should be indexed by
            supporting database layers to speed up queries. Defaults to False.

        is_unique (bool): :noindex: Specifies whether values for this property must be
            unique across all instances of its ``ObjectTypeDefinition``.
            Defaults to False.

        description (Optional[str]): :noindex: An optional human-readable description
            of the property.

    
    """

    name: str = Field(
        ...,
        description="Name of the property (e.g., 'title', 'age').",
    )
    data_type: PropertyDataType = Field(
        ...,
        description="Data type of the property.",
    )
    is_primary_key: bool = Field(
        default=False,
        description="Is this property a primary key for the object type?",
    )
    is_nullable: bool = Field(
        default=True,
        description="Can this property be null?",
    )
    is_indexed: bool = Field(
        default=False,
        description="Should this property be indexed in supporting databases?",
    )
    is_unique: bool = Field(
        default=False,
        description="Does this property require unique values?",
    )
    description: Optional[str] = Field(
        default=None,
        description="Optional description of the property.",
    )

class ObjectTypeDefinition(BaseModel):
    """Defines the schema for a type of object (e.g., a node in a graph, a table row).

    An ``ObjectTypeDefinition`` specifies the structure for a category of data
    entities. It includes a unique name, an optional description, and a list
    of ``PropertyDefinition``s that define its attributes.

    Attributes:
        name (str): :noindex: A unique name for the object type (e.g., 'Document',
            'Person'). Conventionally, PascalCase is used.

        description (Optional[str]): :noindex: An optional human-readable description
            of what this object type represents.

        properties (List[PropertyDefinition]): :noindex: A list of properties that
            define the attributes of this object type.

    
    """

    name: str = Field(
        ...,
        description=(
            "Unique name for the object type (e.g., 'Document', 'Person'). "
            "Convention: PascalCase."
        ),
    )
    description: Optional[str] = Field(
        default=None,
        description="Optional description of the object type.",
    )
    properties: list[PropertyDefinition] = Field(
        ...,
        description="List of properties defining this object type.",
    )
    # _created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    # _updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    @field_validator("properties")
    @classmethod
    def check_primary_key_once(
        cls, v: list[PropertyDefinition],
    ) -> list[PropertyDefinition]:
        """Ensures that an ObjectTypeDefinition has at most one primary key.

        While the `id` field from `MemoryInstance` serves as a unique identifier
        for `ObjectInstance`s, an `ObjectTypeDefinition` can optionally define
        one of its properties as a primary key for domain-specific identification
        or for database layers that require an explicit primary key column.

        Args:
            v (List[PropertyDefinition]): The list of properties being validated.

        Returns:
            List[PropertyDefinition]: The validated list of properties.

        Raises:
            ValueError: If more than one property is marked as `is_primary_key`.

        """
        pk_count = sum(1 for p in v if p.is_primary_key)
        if pk_count > 1:
            msg = "An ObjectTypeDefinition can have at most one primary key property."
            raise ValueError(
                msg,
            )
        # If no explicit PK, 'id' from MemoryInstance will serve this role for ObjectInstance
        return v

class EmbeddingDefinition(BaseModel):
    """Defines how an embedding should be generated and stored for an ``ObjectTypeDefinition``.

    This model specifies the configuration for creating vector embeddings from
    the content of objects. It links an object type and one of its properties
    to an embedding model.

    Attributes:
        name (str): :noindex: A unique name for this embedding configuration (e.g.,
            'content_embedding_v1'). Conventionally, snake_case is used.

        object_type_name (str): :noindex: The name of the ``ObjectTypeDefinition`` this
            embedding applies to.

        source_property_name (str): :noindex: The name of the property within the
            specified ``ObjectTypeDefinition`` whose content will be used to
            generate the embedding.

        embedding_model (str): :noindex: An identifier for the embedding model to be
            used (e.g., a Hugging Face model name like
            'huggingface/colbert-ir/colbertv2.0').

        dimensions (Optional[int]): :noindex: The expected dimensionality of the
            embedding vector. If None, the system may attempt to infer it
            from the model.

        description (Optional[str]): :noindex: An optional human-readable description
            of this embedding definition.

    
    """

    name: str = Field(
        ...,
        description=(
            "Unique name for this embedding configuration "
            "(e.g., 'content_embedding_v1'). Convention: snake_case."
        ),
    )
    object_type_name: str = Field(
        ...,
        description="Name of the ObjectTypeDefinition this embedding applies to.",
    )
    source_property_name: str = Field(
        ...,
        description=(
            "Name of the property within the ObjectTypeDefinition whose content will be "
            "embedded."
        ),
    )
    embedding_model: str = Field(
        default="huggingface/colbert-ir/colbertv2.0",
        description="Identifier for the embedding model to use.",
    )
    dimensions: Optional[int] = Field(
        default=None,
        description="Expected dimensions of the embedding vector. If None, inferred from model.",
    )
    description: Optional[str] = Field(
        default=None,
        description="Optional description of this embedding definition.",
    )
    # _created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    # _updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

class RelationTypeDefinition(BaseModel):
    """Defines the schema for a type of relation between objects (e.g., an edge in a graph).

    A ``RelationTypeDefinition`` specifies the structure for relationships
    that can exist between instances of ``ObjectTypeDefinition``s. It includes
    a unique name, allowed source and target object types, and any properties
    specific to the relation itself (edge properties).

    Attributes:
        name (str): :noindex: A unique name for the relation type (e.g., 'HAS_AUTHOR',
            'REFERENCES'). Conventionally, UPPER_SNAKE_CASE is used.

        description (Optional[str]): :noindex: An optional human-readable description
            of what this relation type represents.

        source_object_type_names (List[str]): :noindex: A list of names of
            ``ObjectTypeDefinition``s that are allowed as the source (or "from" side)
            of this relation.

        target_object_type_names (List[str]): :noindex: A list of names of
            ``ObjectTypeDefinition``s that are allowed as the target (or "to" side)
            of this relation.

        properties (List[PropertyDefinition]): :noindex: A list of ``PropertyDefinition``s that
            belong to the relation itself (often called edge properties).
            Defaults to an empty list if the relation has no properties.

    
    """

    name: str = Field(
        ...,
        description=(
            "Unique name for the relation type (e.g., 'HAS_AUTHOR', 'REFERENCES'). "
            "Convention: UPPER_SNAKE_CASE."
        ),
    )
    description: Optional[str] = Field(
        default=None,
        description="Optional description of the relation type.",
    )
    source_object_type_names: list[str] = Field(
        ...,
        description="List of names of allowed source ObjectTypeDefinitions.",
    )
    target_object_type_names: list[str] = Field(
        ...,
        description="List of names of allowed target ObjectTypeDefinitions.",
    )
    properties: list[PropertyDefinition] = Field(
        default_factory=list,
        description="Properties of the relation itself (edge properties).",
    )
    # _created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    # _updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

# --- Instance Models ---
class ObjectInstance(MemoryInstance):
    """Represents a concrete instance of an ``ObjectTypeDefinition``.

    This model holds the actual data for an individual object, conforming to
    the schema defined by its ``ObjectTypeDefinition``. It inherits common
    metadata fields from ``MemoryInstance``.

    Attributes:
        object_type_name (str): :noindex: The name of the ``ObjectTypeDefinition`` that
            this instance conforms to.

        properties (Dict[str, Any]): :noindex: A dictionary containing the actual data
            for the instance, mapping property names (as defined in the
            ``ObjectTypeDefinition``) to their corresponding values.

    
    """

    object_type_name: str = Field(
        ...,
        description="Name of the ObjectTypeDefinition this instance conforms to.",
    )
    properties: dict[str, Any] = Field(
        ...,
        description="Actual data for the instance, mapping property names to values.",
    )

class EmbeddingInstance(MemoryInstance):
    """Represents an instance of an embedding, linked to an ``ObjectInstance``.

    This model stores a vector embedding generated from a specific property of
    an ``ObjectInstance``, according to an ``EmbeddingDefinition``. It inherits
    common metadata fields from ``MemoryInstance``.

    Attributes:
        object_instance_id (UUID): :noindex: The ID of the ``ObjectInstance`` to which
            this embedding belongs.

        embedding_definition_name (str): :noindex: The name of the ``EmbeddingDefinition``
            that was used to generate this embedding.

        vector (List[float]): :noindex: The actual embedding vector, represented as a
            list of floating-point numbers.

        source_text_preview (Optional[str]): :noindex: A truncated preview of the source
            text that was used to generate the embedding. This can be useful
            for quick inspection or debugging.

    
    """

    object_instance_id: UUID = Field(
        ...,
        description="ID of the ObjectInstance this embedding belongs to.",
    )
    embedding_definition_name: str = Field(
        ...,
        description="Name of the EmbeddingDefinition used to generate this embedding.",
    )
    vector: list[float] = Field(
        ...,
        description="The embedding vector.",
    )
    source_text_preview: Optional[str] = Field(
        default=None,
        description="A truncated preview of the source text used for embedding.",
    )
    # Note: Pydantic v2 might handle max_length differently,
    # consider custom validator if needed or rely on application logic.

class RelationInstance(MemoryInstance):
    """Represents a concrete instance of a ``RelationTypeDefinition``, linking two ``ObjectInstance``s.

    This model captures a specific relationship between two objects, conforming
    to the schema defined by its ``RelationTypeDefinition``. It inherits common
    metadata fields from ``MemoryInstance``.

    Attributes:
        relation_type_name (str): :noindex: The name of the ``RelationTypeDefinition``
            that this instance conforms to.

        source_object_instance_id (UUID): :noindex: The ID of the ``ObjectInstance`` that
            is the source (or "from" side) of this relation.

        target_object_instance_id (UUID): :noindex: The ID of the ``ObjectInstance`` that
            is the target (or "to" side) of this relation.

        properties (Dict[str, Any]): :noindex: A dictionary containing the actual data
            for the relation's own properties (edge properties), if any are
            defined in its ``RelationTypeDefinition``. Defaults to an empty dict.

    
    """

    relation_type_name: str = Field(
        ...,
        description="Name of the RelationTypeDefinition this instance conforms to.",
    )
    source_object_instance_id: UUID = Field(
        ...,
        description="ID of the source ObjectInstance.",
    )
    target_object_instance_id: UUID = Field(
        ...,
        description="ID of the target ObjectInstance.",
    )
    properties: dict[str, Any] = Field(
        default_factory=dict,
        description="Actual data for the relation's properties.",
    )

class RelationInstanceList(BaseModel):
    """A container for a list of RelationInstance objects."""
    relations: list[RelationInstance]
