"""Schema models for index field definitions (2026-01.alpha API)."""

from __future__ import annotations

from typing import Any

from pinecone.models._base import BaseModel


class DenseVectorField(BaseModel, tag="dense_vector", tag_field="type"):
    """Dense vector field definition.

    Dense vectors are the traditional fixed-length vectors used for
    similarity search. Each dimension has a floating-point value.

    Attributes:
        dimension: Number of dimensions in the vector (1-20000). Optional to handle evolving API responses.
        metric: Distance metric ("cosine", "euclidean", or "dotproduct"). Optional to handle evolving API responses.
        description: Optional human-readable description

    Note:
        The "type" field is automatically set to "dense_vector" by msgspec's
        tagged union system and should not be included in the struct definition.

        During API schema evolution, some fields may be missing from responses.
        The SDK is intentionally flexible to parse and display whatever data exists.

    Example:
        >>> field = DenseVectorField(
        ...     dimension=1536, metric="cosine", description="OpenAI ada-002 embeddings"
        ... )
    """

    dimension: int | None = None
    metric: str | None = None  # cosine, euclidean, dotproduct
    description: str | None = None


class SparseVectorField(BaseModel, tag="sparse_vector", tag_field="type"):
    """Sparse vector field definition.

    Sparse vectors have most values as zero and are represented as
    (indices, values) pairs. Useful for keyword-based search (e.g., BM25).

    Attributes:
        metric: Distance metric (typically "dotproduct"). Optional to handle evolving API responses.
        description: Optional human-readable description

    Note:
        The "type" field is automatically set to "sparse_vector" by msgspec's
        tagged union system and should not be included in the struct definition.

        During API schema evolution, some fields may be missing from responses.
        The SDK is intentionally flexible to parse and display whatever data exists.

    Example:
        >>> field = SparseVectorField(metric="dotproduct", description="BM25 sparse embeddings")
    """

    metric: str | None = None  # typically dotproduct
    description: str | None = None


class SemanticTextField(BaseModel, tag="semantic_text", tag_field="type"):
    """Semantic text field with integrated embedding.

    Semantic text fields automatically embed text using a specified model,
    eliminating the need to generate embeddings separately. Text is stored
    and embedded using Pinecone's integrated inference.

    Attributes:
        model: Embedding model name (e.g., "multilingual-e5-large"). Optional to handle evolving API responses.
        metric: Distance metric (typically "cosine"). Optional to handle evolving API responses.
        description: Optional human-readable description
        read_parameters: Parameters for read operations (e.g., {"input_type": "query"})
        write_parameters: Parameters for write operations (e.g., {"input_type": "passage"})
        dimension: Vector dimension (may be included in some API responses)

    Note:
        The "type" field is automatically set to "semantic_text" by msgspec's
        tagged union system and should not be included in the struct definition.

        During API schema evolution, some fields may be missing from responses.
        The SDK is intentionally flexible to parse and display whatever data exists.

    Example:
        >>> field = SemanticTextField(
        ...     model="multilingual-e5-large",
        ...     metric="cosine",
        ...     description="Product descriptions",
        ...     write_parameters={"input_type": "passage"},
        ...     read_parameters={"input_type": "query", "truncate": "NONE"},
        ... )
    """

    model: str | None = None  # e.g., "multilingual-e5-large"
    metric: str | None = None  # cosine
    dimension: int | None = None  # May be present in some API responses
    description: str | None = None
    read_parameters: dict[str, Any] | None = None
    write_parameters: dict[str, Any] | None = None


class StringField(BaseModel, tag="string", tag_field="type"):
    """String field for metadata or full-text search.

    String fields can be used for filtering (filterable) or full-text
    search (full_text_searchable). They store text data as metadata.

    Attributes:
        description: Optional human-readable description
        filterable: Whether field can be used in metadata filters
        full_text_searchable: Whether field supports full-text search
        language: Language code for full-text search (e.g., "en", "es")
        stemming: Whether to apply stemming for full-text search
        lowercase: Whether to convert text to lowercase for full-text search
        max_term_len: Maximum term length for full-text search indexing

    Note:
        The "type" field is automatically set to "string" by msgspec's
        tagged union system and should not be included in the struct definition.

        Full-text search parameters (language, stemming, lowercase, max_term_len)
        are only applicable when full_text_searchable=True.

    Example:
        >>> # Filterable string
        >>> category = StringField(filterable=True, description="Product category")
        >>>
        >>> # Full-text searchable string with language
        >>> content = StringField(
        ...     full_text_searchable=True, language="en", description="Document content"
        ... )
    """

    description: str | None = None
    filterable: bool = False
    full_text_searchable: bool = False
    language: str | None = None
    stemming: bool | None = None
    lowercase: bool | None = None
    max_term_len: int | None = None


class IntegerField(BaseModel, tag="integer", tag_field="type"):
    """Integer field for numerical metadata filtering.

    Integer fields store whole numbers and can be used for range filtering
    (e.g., year >= 2020, price < 100).

    Attributes:
        description: Optional human-readable description
        filterable: Whether field can be used in metadata filters

    Note:
        The "type" field is automatically set to "integer" by msgspec's
        tagged union system and should not be included in the struct definition.

    Example:
        >>> field = IntegerField(filterable=True, description="Publication year")
    """

    description: str | None = None
    filterable: bool = False


# Union of all field types
SchemaField = DenseVectorField | SparseVectorField | SemanticTextField | StringField | IntegerField


class Schema(BaseModel):
    """Index schema definition.

    The schema defines all fields in the index, including vector fields,
    text fields, and metadata fields. Each field has a specific type and
    properties that determine how it's indexed and queried.

    Attributes:
        fields: Dictionary mapping field names to field definitions

    Example:
        >>> schema = Schema(
        ...     fields={
        ...         "embedding": DenseVectorField(
        ...             type="dense_vector", dimension=1536, metric="cosine"
        ...         ),
        ...         "title": StringField(type="string", full_text_searchable=True),
        ...         "category": StringField(type="string", filterable=True),
        ...         "year": IntegerField(type="integer", filterable=True),
        ...     }
        ... )
    """

    fields: dict[str, SchemaField]


class SchemaBuilder:
    """Build index schemas with type-safe factory methods.

    SchemaBuilder provides IDE autocomplete, early error detection, and
    future extensibility for creating index schemas. Use this instead of
    manually constructing nested dictionaries.

    The builder returns a plain dict that can be passed to both sync and
    async `indexes.create()` methods. You can also mutate the returned
    dict before passing it to create() if needed.

    Attributes:
        _fields: Internal dict of field definitions

    Examples:
        Basic schema with vector and metadata fields:

        >>> from pinecone import Pinecone, SchemaBuilder
        >>>
        >>> schema = (
        ...     SchemaBuilder()
        ...     .add_dense_vector_field("embedding", dimension=768, metric="cosine")
        ...     .add_string_field("title", filterable=True)
        ...     .add_integer_field("year", filterable=True)
        ...     .build()
        ... )
        >>>
        >>> pc = Pinecone(api_key="...")
        >>> index = pc.indexes.create(name="my-index", schema=schema)

        Full-text search with language processing:

        >>> schema = (
        ...     SchemaBuilder()
        ...     .add_string_field(
        ...         "content",
        ...         full_text_searchable=True,
        ...         language="en",
        ...         stemming=True,
        ...         lowercase=True,
        ...     )
        ...     .build()
        ... )

        Using with async client:

        >>> from pinecone import AsyncPinecone, SchemaBuilder
        >>>
        >>> schema = (
        ...     SchemaBuilder()
        ...     .add_dense_vector_field("vec", dimension=384, metric="cosine")
        ...     .build()
        ... )
        >>> pc = AsyncPinecone(api_key="...")
        >>> index = await pc.indexes.create(name="my-index", schema=schema)

        Future API parameters via **kwargs:

        >>> schema = (
        ...     SchemaBuilder()
        ...     .add_string_field(
        ...         "title",
        ...         full_text_searchable=True,
        ...         case_sensitive=True,  # Hypothetical future parameter
        ...     )
        ...     .build()
        ... )

        Custom/experimental fields:

        >>> schema = (
        ...     SchemaBuilder()
        ...     .add_custom_field("experimental", {"type": "string", "experimental_option": True})
        ...     .build()
        ... )

        Mutating after build:

        >>> schema = SchemaBuilder().add_string_field("title").build()
        >>> schema["fields"]["title"]["description"] = "Article title"
        >>> pc.indexes.create(name="my-index", schema=schema)

    Note:
        If you add multiple fields with the same name, the last one wins
        (later calls overwrite earlier ones). This is intentional to allow
        replacing field definitions.
    """

    def __init__(self) -> None:
        self._fields: dict[str, Any] = {}

    def add_string_field(
        self,
        name: str,
        *,
        full_text_searchable: bool = False,
        language: str | None = None,
        stemming: bool | None = None,
        lowercase: bool | None = None,
        max_term_len: int | None = None,
        filterable: bool = False,
        description: str | None = None,
        **additional_options: Any,
    ) -> SchemaBuilder:
        """Add a string field for text content or metadata.

        String fields can be configured for full-text search with language-specific
        processing, or used as filterable metadata. Full-text search options
        (language, stemming, lowercase, max_term_len) only apply when
        full_text_searchable is True.

        Args:
            name: Field name. If a field with this name already exists, it will
                be replaced.
            full_text_searchable: Enable full-text search on this field. When True,
                you can search this field using text queries in the Documents API.
            language: Language code for text processing (e.g., "en", "es", "fr").
                Only applies when full_text_searchable=True.
            stemming: Enable word stemming (e.g., "running" matches "run").
                Only applies when full_text_searchable=True.
            lowercase: Convert text to lowercase before indexing.
                Only applies when full_text_searchable=True.
            max_term_len: Maximum length of indexed terms. Longer terms are truncated.
                Only applies when full_text_searchable=True.
            filterable: Enable filtering on this field using metadata filters.
            description: Human-readable description of this field.
            **additional_options: Additional options for future API parameters.
                These are passed through to the API without validation, allowing
                you to use new API features before the SDK officially supports them.

        Returns:
            self for method chaining

        Examples:
            Full-text searchable field with language processing:

            >>> builder = SchemaBuilder()
            >>> builder.add_string_field(
            ...     "title", full_text_searchable=True, language="en", stemming=True, lowercase=True
            ... )

            Filterable metadata field:

            >>> builder.add_string_field("category", filterable=True)

            Using future API parameters:

            >>> builder.add_string_field(
            ...     "title",
            ...     full_text_searchable=True,
            ...     case_sensitive=True,  # Future API param via **additional_options
            ... )
        """
        field: dict[str, Any] = {
            "type": "string",
            "full_text_searchable": full_text_searchable,
            "filterable": filterable,
        }
        # Only include optional params if set
        if language is not None:
            field["language"] = language
        if stemming is not None:
            field["stemming"] = stemming
        if lowercase is not None:
            field["lowercase"] = lowercase
        if max_term_len is not None:
            field["max_term_len"] = max_term_len
        if description is not None:
            field["description"] = description

        field.update(additional_options)  # Support future params
        self._fields[name] = field
        return self

    def add_dense_vector_field(
        self,
        name: str,
        *,
        dimension: int,
        metric: str,
        description: str | None = None,
        **additional_options: Any,
    ) -> SchemaBuilder:
        """Add a dense vector field for similarity search.

        Dense vector fields store fixed-dimension embeddings and support
        similarity search using various distance metrics.

        Args:
            name: Field name. If a field with this name already exists, it will
                be replaced.
            dimension: Vector dimension (e.g., 768 for many embedding models).
                Must be a positive integer.
            metric: Distance metric for similarity search. Common values:
                "cosine", "euclidean", "dotproduct".
            description: Human-readable description of this field.
            **additional_options: Additional options for future API parameters.

        Returns:
            self for method chaining

        Examples:
            Standard embedding field:

            >>> builder = SchemaBuilder()
            >>> builder.add_dense_vector_field(
            ...     "embedding",
            ...     dimension=768,
            ...     metric="cosine",
            ...     description="OpenAI text-embedding-3-small",
            ... )
        """
        field: dict[str, Any] = {
            "type": "dense_vector",
            "dimension": dimension,
            "metric": metric,
        }
        if description is not None:
            field["description"] = description

        field.update(additional_options)
        self._fields[name] = field
        return self

    def add_sparse_vector_field(
        self,
        name: str,
        *,
        metric: str = "dotproduct",
        description: str | None = None,
        **additional_options: Any,
    ) -> SchemaBuilder:
        """Add a sparse vector field for keyword-based search.

        Sparse vector fields store vectors with mostly zero values, typically
        used for keyword search (e.g., BM25, SPLADE) or hybrid search.

        Args:
            name: Field name. If a field with this name already exists, it will
                be replaced.
            metric: Distance metric. Defaults to "dotproduct".
            description: Human-readable description of this field.
            **additional_options: Additional options for future API parameters.

        Returns:
            self for method chaining

        Examples:
            Sparse vector for keyword search:

            >>> builder = SchemaBuilder()
            >>> builder.add_sparse_vector_field(
            ...     "sparse_embedding", metric="dotproduct", description="BM25 sparse vectors"
            ... )
        """
        field: dict[str, Any] = {
            "type": "sparse_vector",
            "metric": metric,
        }
        if description is not None:
            field["description"] = description

        field.update(additional_options)
        self._fields[name] = field
        return self

    def add_semantic_text_field(
        self,
        name: str,
        *,
        model: str,
        metric: str = "cosine",
        read_parameters: dict[str, Any] | None = None,
        write_parameters: dict[str, Any] | None = None,
        description: str | None = None,
        **additional_options: Any,
    ) -> SchemaBuilder:
        """Add a semantic text field with integrated embedding.

        Semantic text fields automatically embed text using Pinecone Inference
        without requiring you to manage embeddings separately.

        Args:
            name: Field name. If a field with this name already exists, it will
                be replaced.
            model: Embedding model to use (e.g., "multilingual-e5-large").
            metric: Distance metric for similarity search. Defaults to "cosine".
            read_parameters: Model parameters for query-time embedding.
            write_parameters: Model parameters for index-time embedding.
            description: Human-readable description of this field.
            **additional_options: Additional options for future API parameters.

        Returns:
            self for method chaining

        Examples:
            Semantic text field with Pinecone's embedding model:

            >>> builder = SchemaBuilder()
            >>> builder.add_semantic_text_field(
            ...     "content", model="multilingual-e5-large", metric="cosine"
            ... )
        """
        field: dict[str, Any] = {
            "type": "semantic_text",
            "model": model,
            "metric": metric,
        }
        if read_parameters is not None:
            field["read_parameters"] = read_parameters
        if write_parameters is not None:
            field["write_parameters"] = write_parameters
        if description is not None:
            field["description"] = description

        field.update(additional_options)
        self._fields[name] = field
        return self

    def add_integer_field(
        self,
        name: str,
        *,
        filterable: bool = True,
        description: str | None = None,
        **additional_options: Any,
    ) -> SchemaBuilder:
        """Add an integer field for numeric metadata.

        Integer fields store whole numbers and support filtering with
        comparison operators ($gt, $gte, $lt, $lte, $eq, $ne).

        Args:
            name: Field name. If a field with this name already exists, it will
                be replaced.
            filterable: Enable filtering on this field. Defaults to True.
            description: Human-readable description of this field.
            **additional_options: Additional options for future API parameters.

        Returns:
            self for method chaining

        Examples:
            Integer field for year filtering:

            >>> builder = SchemaBuilder()
            >>> builder.add_integer_field("year", filterable=True, description="Publication year")
        """
        field: dict[str, Any] = {
            "type": "integer",
            "filterable": filterable,
        }
        if description is not None:
            field["description"] = description

        field.update(additional_options)
        self._fields[name] = field
        return self

    def add_custom_field(
        self,
        name: str,
        field_definition: dict[str, Any],
    ) -> SchemaBuilder:
        """Add a field with a custom dictionary definition.

        Use this escape hatch when you need options not yet supported
        by the typed factory methods, or for experimental API features.

        Args:
            name: Field name
            field_definition: Complete field definition dict

        Returns:
            self for method chaining

        Examples:
            >>> builder.add_custom_field("custom", {"type": "string", "experimental_option": True})
        """
        self._fields[name] = field_definition
        return self

    def build(self) -> dict[str, Any]:
        """Build and return the schema dictionary.

        Returns a plain dict that can be passed to both sync and async
        `indexes.create()` methods. The returned dict can be further
        modified before passing to create() if needed.

        Returns:
            Schema dict with "fields" key. If no fields were added,
            returns {"fields": {}}.

        Examples:
            Build and use with sync client:

            >>> schema = (
            ...     SchemaBuilder()
            ...     .add_dense_vector_field("vec", dimension=768, metric="cosine")
            ...     .build()
            ... )
            >>> pc.indexes.create(name="my-index", schema=schema)

            Build and use with async client:

            >>> schema = (
            ...     SchemaBuilder()
            ...     .add_dense_vector_field("vec", dimension=768, metric="cosine")
            ...     .build()
            ... )
            >>> await async_pc.indexes.create(name="my-index", schema=schema)

            Build and mutate:

            >>> schema = SchemaBuilder().add_string_field("title").build()
            >>> schema["fields"]["title"]["description"] = "Document title"
            >>> pc.indexes.create(name="my-index", schema=schema)
        """
        return {"fields": self._fields}


__all__ = [
    "DenseVectorField",
    "IntegerField",
    "Schema",
    "SchemaBuilder",
    "SchemaField",
    "SemanticTextField",
    "SparseVectorField",
    "StringField",
]
