"""Elasticsearch field utilities and factory functions for search models."""

from enum import Enum
from typing import Any

from pydantic.fields import FieldInfo, Field

from common.utils import NoValue


def _parse_field(
    field: FieldInfo | NoValue | Any,
    default: Any = NoValue(),
    default_factory: Any = NoValue(),
) -> FieldInfo:
    """
    Parse field input into a FieldInfo object.

    Args:
        field: The field specification (FieldInfo, ..., or NoValue)
        default: Default value for the field
        default_factory: Callable that returns the default value

    Returns:
        FieldInfo: A properly configured field info object
    """
    # Can't specify both default and default_factory
    if not isinstance(default, NoValue) and not isinstance(default_factory, NoValue):
        raise ValueError("Cannot specify both default and default_factory")

    # If field is already a FieldInfo, just return it
    if isinstance(field, FieldInfo):
        assert isinstance(default, NoValue) and isinstance(
            default_factory, NoValue
        ), "Cannot specify default or default_factory with FieldInfo"
        return field

    # If field is Ellipsis (...), create new FieldInfo
    if field is ...:
        if isinstance(default, NoValue) and isinstance(default_factory, NoValue):
            return Field(...)
        if not isinstance(default, NoValue):
            return Field(default=default)
        return Field(default_factory=default_factory)

    # If field is NoValue, handle default/default_factory
    if isinstance(field, NoValue):
        if isinstance(default, NoValue) and isinstance(default_factory, NoValue):
            return Field()
        if not isinstance(default, NoValue):
            return Field(default=default)
        return Field(default_factory=default_factory)

    raise ValueError(f"Invalid field specification: {field}")


def SearchField(
    field: FieldInfo | NoValue | Any = NoValue(),
    *,
    default: Any = NoValue(),
    default_factory: Any = NoValue(),
    es_ignore: bool = False,
    es_type: str | None = None,
    es_dynamic: bool | str = None,
    es_null_value: str | None = None,
    es_analyzer: str | None = None,
    es_fields: dict | None = None,
    es_properties: dict | None = None,
) -> Any:
    """Create a search field with Elasticsearch mapping properties.

    Args:
        field: The base field specification
        default: Default value for the field
        default_factory: Callable that returns the default value
        es_ignore: Whether to ignore this field in ES mapping
        es_type: Elasticsearch field type
        es_dynamic: Dynamic mapping behavior
        es_null_value: Value to use for null values
        es_analyzer: Text analyzer to use
        es_fields: Multi-fields configuration
        es_properties: Additional ES properties

    Returns:
        Configured field with ES metadata
    """
    json_schema_update = {}

    field = _parse_field(field=field, default=default, default_factory=default_factory)

    if es_ignore:
        json_schema_update["x-es-ignore"] = es_ignore
        assert es_type is None, "Cannot specify es_type when es_ignore is True"
        assert (
            es_properties is None
        ), "Cannot specify es_properties when es_ignore is True"

    es_properties = es_properties or {}
    if es_type is not None:
        es_properties["type"] = es_type

    if es_dynamic is not None:
        es_properties["dynamic"] = es_dynamic

    if es_null_value is not None:
        es_properties["null_value"] = es_null_value

    if es_analyzer is not None:
        if es_properties.get("type") is None:
            # Default to text if type not specified
            es_properties["type"] = "text"
        es_properties["analyzer"] = es_analyzer

    if es_fields is not None:
        es_properties["fields"] = es_fields

    if es_properties:
        json_schema_update["x-es-properties"] = es_properties

    if isinstance(field.json_schema_extra, dict):
        field.json_schema_extra.update(json_schema_update)
    else:
        assert (
            field.json_schema_extra is None
        ), f"Unexpected json_schema_extra: {field.json_schema_extra}"
        field.json_schema_extra = json_schema_update

    return field


def DenseVectorField(
    field: FieldInfo | NoValue | Any = NoValue(),
    *,
    default: Any = NoValue(),
    default_factory: Any = NoValue(),
    dims: int,
    index: bool = True,
    similarity: str = "cosine",
    m: int | None = None,
    ef_construction: int | None = None,
    es_properties: dict | None = None,
) -> Any:
    """
    Helper function to create dense vector fields with optional HNSW parameters.

    Args:
        field: The base field info
        default: Default value for the field
        default_factory: Callable that returns the default value
        dims: Vector dimensions (required)
        index: Whether to make the vector searchable
        similarity: Similarity metric ("cosine", "dot_product", or "l2_norm")
        m: HNSW graph parameter - maximum number of connections per node (default: 16)
        ef_construction: HNSW construction parameter - controls index build quality (default: 100)
        es_properties: Additional ES properties to merge
    """

    es_properties = es_properties or {}
    vector_props = {
        "type": "dense_vector",
        "dims": dims,
        "index": index,
        "similarity": similarity,
    }

    if index and (m is not None or ef_construction is not None):
        # Only add HNSW parameters if the vector is indexed
        vector_props["index_options"] = {
            "type": "hnsw",
            **({"m": m} if m is not None else {}),
            **(
                {"ef_construction": ef_construction}
                if ef_construction is not None
                else {}
            ),
        }

    es_properties.update(vector_props)
    return SearchField(
        field,
        default=default,
        default_factory=default_factory,
        es_properties=es_properties,
    )


def DictField(
    field: FieldInfo | NoValue | Any = NoValue(),
    *,
    default: Any = NoValue(),
    default_factory: Any = NoValue(),
    properties: dict | None = None,
    dynamic: bool | str = False,
) -> Any:
    """
    Helper function to create dictionary fields with explicit mappings.

    Args:
        field: The base field info
        default: Default value for the field
        default_factory: Callable that returns the default value
        properties: Nested field mappings
        dynamic: Whether to allow dynamic field
    """
    es_properties = {"type": "object", "dynamic": dynamic}
    if properties:
        es_properties["properties"] = properties

    return SearchField(
        field,
        default=default,
        default_factory=default_factory,
        es_properties=es_properties,
    )


class Sort(str, Enum):
    """Sort order enumeration for queries."""

    ASC = "asc"
    DESC = "desc"
