# Copyright (c) Microsoft. All rights reserved.

import ast
import json
import logging
import sys
from collections.abc import Callable, Sequence
from typing import Any, ClassVar, Final, Generic, TypeVar

from pydantic import SecretStr, field_validator, model_validator
from weaviate import WeaviateAsyncClient, use_async_with_embedded, use_async_with_local, use_async_with_weaviate_cloud
from weaviate.classes.config import Configure, DataType, Property
from weaviate.classes.init import Auth
from weaviate.classes.query import Filter, MetadataQuery
from weaviate.collections.classes.config_named_vectors import _NamedVectorConfigCreate
from weaviate.collections.classes.config_vectorizers import VectorDistances
from weaviate.collections.classes.data import DataObject
from weaviate.collections.classes.filters import FilterValues, _Filters
from weaviate.collections.collection import CollectionAsync
from weaviate.exceptions import WeaviateClosedClientError, WeaviateConnectionError

from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.data.vector import (
    DistanceFunction,
    GetFilteredRecordOptions,
    IndexKind,
    KernelSearchResults,
    SearchType,
    TModel,
    VectorSearch,
    VectorSearchOptions,
    VectorSearchResult,
    VectorStore,
    VectorStoreCollection,
    VectorStoreCollectionDefinition,
    VectorStoreField,
)
from semantic_kernel.exceptions import (
    ServiceInvalidExecutionSettingsError,
    VectorSearchExecutionException,
    VectorStoreException,
    VectorStoreInitializationException,
    VectorStoreModelValidationError,
    VectorStoreOperationException,
)
from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseSettings
from semantic_kernel.kernel_types import OneOrMany
from semantic_kernel.utils.feature_stage_decorator import release_candidate

if sys.version_info >= (3, 12):
    from typing import override  # pragma: no cover
else:
    from typing_extensions import override  # pragma: no cover
if sys.version_info >= (3, 11):
    from typing import Self  # pragma: no cover
else:
    from typing_extensions import Self  # pragma: no cover

logger = logging.getLogger(__name__)

TKey = TypeVar("TKey", bound=str)

DISTANCE_FUNCTION_MAP: Final[dict[DistanceFunction, VectorDistances]] = {
    DistanceFunction.COSINE_DISTANCE: VectorDistances.COSINE,
    DistanceFunction.DOT_PROD: VectorDistances.DOT,
    DistanceFunction.EUCLIDEAN_SQUARED_DISTANCE: VectorDistances.L2_SQUARED,
    DistanceFunction.MANHATTAN: VectorDistances.MANHATTAN,
    DistanceFunction.HAMMING: VectorDistances.HAMMING,
    DistanceFunction.DEFAULT: VectorDistances.COSINE,
}

INDEX_KIND_MAP: Final[dict[IndexKind, Callable]] = {
    IndexKind.HNSW: Configure.VectorIndex.hnsw,
    IndexKind.FLAT: Configure.VectorIndex.flat,
    IndexKind.DEFAULT: Configure.VectorIndex.flat,
}

DATATYPE_MAP: Final[dict[str, DataType]] = {
    "str": DataType.TEXT,
    "int": DataType.INT,
    "float": DataType.NUMBER,
    "bool": DataType.BOOL,
    "list[str]": DataType.TEXT_ARRAY,
    "list[int]": DataType.INT_ARRAY,
    "list[float]": DataType.NUMBER_ARRAY,
    "list[bool]": DataType.BOOL_ARRAY,
    "default": DataType.TEXT,
}


def _definition_to_weaviate_named_vectors(
    definition: VectorStoreCollectionDefinition,
) -> list[_NamedVectorConfigCreate]:
    """Convert vector store vector fields to Weaviate named vectors.

    Args:
        definition (VectorStoreRecordDefinition): The data model definition.

    Returns:
        list[_NamedVectorConfigCreate]: The Weaviate named vectors.
    """
    vector_list: list[_NamedVectorConfigCreate] = []

    for field in definition.vector_fields:
        if field.distance_function is None or field.distance_function not in DISTANCE_FUNCTION_MAP:
            raise VectorStoreModelValidationError(
                f"Distance function {field.distance_function} is not supported by Weaviate."
            )
        if field.index_kind is None or field.index_kind not in INDEX_KIND_MAP:
            raise VectorStoreModelValidationError(f"Index kind {field.index_kind} is not supported by Weaviate.")
        vector_list.append(
            Configure.NamedVectors.none(
                name=field.storage_name or field.name,  # type: ignore
                vector_index_config=INDEX_KIND_MAP[field.index_kind](
                    distance_metric=DISTANCE_FUNCTION_MAP[field.distance_function]
                ),
            )
        )
    return vector_list


@release_candidate
class WeaviateSettings(KernelBaseSettings):
    """Weaviate model settings.

    Args:
        url: HttpsUrl | None - Weaviate URL (Env var WEAVIATE_URL)
        api_key: SecretStr | None - Weaviate token (Env var WEAVIATE_API_KEY)
        local_host: str | None - Local Weaviate host, i.e. a Docker instance (Env var WEAVIATE_LOCAL_HOST)
        local_port: int | None - Local Weaviate port (Env var WEAVIATE_LOCAL_PORT)
        local_grpc_port: int | None - Local Weaviate gRPC port (Env var WEAVIATE_LOCAL_GRPC_PORT)
        use_embed: bool - Whether to use the embedded client
          (Env var WEAVIATE_USE_EMBED)
    """

    env_prefix: ClassVar[str] = "WEAVIATE_"

    # Using a Weaviate Cloud instance (WCD)
    url: HttpsUrl | None = None
    api_key: SecretStr | None = None

    # Using a local Weaviate instance (i.e. Weaviate in a Docker container)
    local_host: str | None = None
    local_port: int | None = None
    local_grpc_port: int | None = None

    # Using the client embedding options
    use_embed: bool = False

    @model_validator(mode="before")
    @classmethod
    def validate_settings(cls, data: Any) -> dict[str, Any]:
        """Validate Weaviate settings."""
        if isinstance(data, dict):
            enabled = sum([
                cls.is_using_weaviate_cloud(data),
                cls.is_using_local_weaviate(data),
                cls.is_using_client_embedding(data),
            ])

            if enabled == 0:
                raise ServiceInvalidExecutionSettingsError(
                    "Weaviate settings must specify either a ",
                    "Weaviate Cloud instance, a local Weaviate instance, or the client embedding options.",
                )
            if enabled > 1:
                raise ServiceInvalidExecutionSettingsError(
                    "Weaviate settings must specify only one of the following: ",
                    "Weaviate Cloud instance, a local Weaviate instance, or the client embedding options.",
                )

        return data

    @classmethod
    def is_using_weaviate_cloud(cls, data: dict[str, Any]) -> bool:
        """Return whether the Weaviate settings are using a Weaviate Cloud instance.

        `api_key` is not checked here. Clients should report an error if `api_key` is not set during initialization.
        """
        return data.get("url") is not None

    @classmethod
    def is_using_local_weaviate(cls, data: dict[str, Any]) -> bool:
        """Return whether the Weaviate settings are using a local Weaviate instance.

        `local_port` and `local_grpc_port` are not checked here.
        Clients should report an error if `local_port` and `local_grpc_port` are not set during initialization.
        """
        return data.get("local_host") is not None

    @classmethod
    def is_using_client_embedding(cls, data: dict[str, Any]) -> bool:
        """Return whether the Weaviate settings are using the client embedding options."""
        return data.get("use_embed") is True


@release_candidate
class WeaviateCollection(
    VectorStoreCollection[TKey, TModel],
    VectorSearch[TKey, TModel],
    Generic[TKey, TModel],
):
    """A Weaviate collection is a collection of records that are stored in a Weaviate database."""

    async_client: WeaviateAsyncClient
    named_vectors: bool = True
    supported_key_types: ClassVar[set[str] | None] = {"str"}
    supported_search_types: ClassVar[set[SearchType]] = {SearchType.VECTOR, SearchType.KEYWORD_HYBRID}

    def __init__(
        self,
        record_type: type[TModel],
        definition: VectorStoreCollectionDefinition | None = None,
        collection_name: str | None = None,
        embedding_generator: EmbeddingGeneratorBase | None = None,
        url: str | None = None,
        api_key: str | None = None,
        local_host: str | None = None,
        local_port: int | None = None,
        local_grpc_port: int | None = None,
        use_embed: bool = False,
        named_vectors: bool = True,
        async_client: WeaviateAsyncClient | None = None,
        env_file_path: str | None = None,
        env_file_encoding: str | None = None,
    ):
        """Initialize a Weaviate collection.

        Args:
            record_type: The type of the data model.
            definition: The definition of the data model.
            collection_name: The name of the collection.
            embedding_generator: The embedding generator.
            url: The Weaviate URL
            api_key: The Weaviate API key.
            local_host: The local Weaviate host (i.e. Weaviate in a Docker container).
            local_port: The local Weaviate port.
            local_grpc_port: The local Weaviate gRPC port.
            use_embed: Whether to use the embedded client.
            named_vectors: Whether to use named vectors, or a single unnamed vector.
                In both cases the data model can be the same, but it has to have 1 vector
                field if named_vectors is False.
            async_client: A custom Weaviate async client.
            env_file_path: The path to the environment file.
            env_file_encoding: The encoding of the environment file.
        """
        managed_client: bool = False
        if not async_client:
            managed_client = True
            weaviate_settings = WeaviateSettings(
                url=url,
                api_key=api_key,
                local_host=local_host,
                local_port=local_port,
                local_grpc_port=local_grpc_port,
                use_embed=use_embed,
                env_file_path=env_file_path,
                env_file_encoding=env_file_encoding,
            )

            try:
                if weaviate_settings.url:
                    async_client = use_async_with_weaviate_cloud(
                        cluster_url=str(weaviate_settings.url),
                        auth_credentials=Auth.api_key(weaviate_settings.api_key.get_secret_value())
                        if weaviate_settings.api_key
                        else None,
                    )
                elif weaviate_settings.local_host:
                    kwargs: dict[str, Any] = {
                        "host": weaviate_settings.local_host,
                        "port": weaviate_settings.local_port,
                        "grpc_port": weaviate_settings.local_grpc_port,
                    }
                    kwargs = {k: v for k, v in kwargs.items() if v is not None}
                    async_client = use_async_with_local(**kwargs)
                elif weaviate_settings.use_embed:
                    async_client = use_async_with_embedded()
                else:
                    raise NotImplementedError(
                        "Weaviate settings must specify either a custom client, a Weaviate Cloud instance,",
                        " a local Weaviate instance, or the client embedding options.",
                    )
            except Exception as e:
                raise VectorStoreInitializationException(f"Failed to initialize Weaviate client: {e}")

        super().__init__(
            record_type=record_type,
            definition=definition,
            collection_name=collection_name,
            async_client=async_client,  # type: ignore[call-arg]
            managed_client=managed_client,
            named_vectors=named_vectors,  # type: ignore[call-arg]
            embedding_generator=embedding_generator,
        )

    @field_validator("collection_name")
    @classmethod
    def collection_name_must_start_with_uppercase(cls, value: str) -> str:
        """By convention, the collection name starts with an uppercase letter.

        https://weaviate.io/developers/weaviate/manage-data/collections#create-a-collection
        Will change the collection name to start with an uppercase letter if it does not.
        """
        if value[0].isupper():
            return value
        return value[0].upper() + value[1:]

    @override
    async def _inner_upsert(
        self,
        records: Sequence[Any],
        **kwargs: Any,
    ) -> Sequence[TKey]:
        assert all([isinstance(record, DataObject) for record in records])  # nosec
        collection: CollectionAsync = self.async_client.collections.get(self.collection_name)
        response = await collection.data.insert_many(records)
        return [str(v) for _, v in response.uuids.items()]  # type: ignore[misc]

    @override
    async def _inner_get(
        self,
        keys: Sequence[TKey] | None = None,
        options: GetFilteredRecordOptions | None = None,
        **kwargs: Any,
    ) -> OneOrMany[Any] | None:
        if not keys:
            if options is not None:
                raise NotImplementedError("Get without keys is not yet implemented.")
            return None
        collection: CollectionAsync = self.async_client.collections.get(self.collection_name)
        result = await collection.query.fetch_objects(
            filters=Filter.any_of([Filter.by_id().equal(key) for key in keys]),
            include_vector=kwargs.get("include_vectors", False),
        )

        return result.objects

    @override
    async def _inner_delete(self, keys: Sequence[TKey], **kwargs: Any) -> None:
        collection: CollectionAsync = self.async_client.collections.get(self.collection_name)
        await collection.data.delete_many(where=Filter.any_of([Filter.by_id().equal(key) for key in keys]))

    @override
    async def _inner_search(
        self,
        search_type: SearchType,
        options: VectorSearchOptions,
        values: Any | None = None,
        vector: Sequence[float | int] | None = None,
        **kwargs: Any,
    ) -> KernelSearchResults[VectorSearchResult[TModel]]:
        collection: CollectionAsync = self.async_client.collections.get(self.collection_name)
        vector_field = self.definition.try_get_vector_field(options.vector_property_name)
        args = {
            "include_vector": options.include_vectors,
            "limit": options.top,
            "offset": options.skip,
            "return_metadata": MetadataQuery(distance=True),
            "target_vector": vector_field.storage_name or vector_field.name
            if self.named_vectors and vector_field
            else None,
        }
        if not vector:
            vector = await self._generate_vector_from_values(values, options)
        if not vector:
            raise VectorSearchExecutionException("No vector provided, or unable to generate a vector.")
        if filter := self._build_filter(options.filter):  # type: ignore
            args["filters"] = Filter.all_of(filter) if isinstance(filter, list) else filter
        if search_type == SearchType.VECTOR:
            if self.named_vectors and not vector_field:
                raise VectorSearchExecutionException(
                    "Vectorizable text search requires a vector field to be specified in the options."
                )
            try:
                results = await collection.query.near_vector(  # type: ignore
                    near_vector=vector,
                    **args,
                )
            except WeaviateClosedClientError as ex:
                raise VectorSearchExecutionException(
                    "Client is closed, please use the context manager or self.async_client.connect."
                ) from ex
            except Exception as ex:
                raise VectorSearchExecutionException(f"Failed searching using a vector: {ex}") from ex
            return KernelSearchResults(
                results=self._get_vector_search_results_from_results(results.objects), total_count=len(results.objects)
            )
        try:
            results = await collection.query.hybrid(  # type: ignore
                query=json.dumps(values) if isinstance(values, list) else values,
                vector=vector,
                **args,
            )
        except WeaviateClosedClientError as ex:
            raise VectorSearchExecutionException(
                "Client is closed, please use the context manager or self.async_client.connect."
            ) from ex
        except Exception as ex:
            raise VectorSearchExecutionException(f"Failed searching using hybrid: {ex}") from ex

        return KernelSearchResults(
            results=self._get_vector_search_results_from_results(results.objects), total_count=len(results.objects)
        )

    @override
    def _lambda_parser(self, node: ast.AST) -> "_Filters | FilterValues":
        # Use Weaviate Filter and operators for AST translation

        # Comparison operations
        match node:
            case ast.Compare():
                if len(node.ops) > 1:
                    # Chain comparisons (e.g., 1 < x < 3) become AND of each comparison
                    filters: list[_Filters] = []
                    for idx in range(len(node.ops)):
                        left = node.left if idx == 0 else node.comparators[idx - 1]
                        right: FilterValues = node.comparators[idx]  # type: ignore
                        op = node.ops[idx]
                        filters.append(self._lambda_parser(ast.Compare(left=left, ops=[op], comparators=[right])))  # type: ignore
                    return Filter.all_of(filters)
                left = self._lambda_parser(node.left)  # type: ignore
                right: FilterValues = self._lambda_parser(node.comparators[0])  # type: ignore
                op = node.ops[0]
                # left is property name, right is value
                if not isinstance(left, str):
                    raise NotImplementedError("Only simple property filters are supported.")
                match op:
                    case ast.Eq():
                        return Filter.by_property(left).equal(right)
                    case ast.NotEq():
                        return Filter.by_property(left).not_equal(right)
                    case ast.Gt():
                        return Filter.by_property(left).greater_than(right)
                    case ast.GtE():
                        return Filter.by_property(left).greater_or_equal(right)
                    case ast.Lt():
                        return Filter.by_property(left).less_than(right)
                    case ast.LtE():
                        return Filter.by_property(left).less_or_equal(right)
                    case ast.In():
                        return Filter.by_property(left).contains_any(right)  # type: ignore
                    case ast.NotIn():
                        # NotIn is not directly supported, so use NOT(contains_any)
                        raise NotImplementedError("NotIn is not directly supported.")
                raise NotImplementedError(f"Unsupported operator: {type(op)}")
            case ast.BoolOp():
                op = node.op  # type: ignore
                filters: list[_Filters] = [self._lambda_parser(v) for v in node.values]  # type: ignore
                if isinstance(op, ast.And):
                    return Filter.all_of(filters)
                if isinstance(op, ast.Or):
                    return Filter.any_of(filters)
                raise NotImplementedError(f"Unsupported BoolOp: {type(op)}")
            case ast.UnaryOp():
                raise NotImplementedError("Unary +, -, ~, ! are not supported in Weaviate filters.")
            case ast.Attribute():
                # Only allow attributes that are in the data model
                if node.attr not in self.definition.storage_names:
                    raise VectorStoreOperationException(
                        f"Field '{node.attr}' not in data model (storage property names are used)."
                    )
                return node.attr
            case ast.Name():
                # Only allow names that are in the data model
                if node.id not in self.definition.storage_names:
                    raise VectorStoreOperationException(
                        f"Field '{node.id}' not in data model (storage property names are used)."
                    )
                return node.id
            case ast.Constant():
                value = node.value
                if isinstance(value, str):
                    return value.replace("'", "''")
                if isinstance(value, bytes):
                    return value.decode("utf-8").replace("'", "''")
                if isinstance(value, (int, float, bool)) or value is None:
                    return value
                raise VectorStoreOperationException(f"Unsupported constant type: {type(value)}")
        raise NotImplementedError(f"Unsupported AST node: {type(node)}")

    async def _inner_vectorized_search(
        self,
        collection: CollectionAsync,
        vector: list[float | int],
        vector_field: VectorStoreField | None,
        args: dict[str, Any],
    ) -> Any:
        if self.named_vectors and not vector_field:
            raise VectorSearchExecutionException(
                "Vectorizable text search requires a vector field to be specified in the options."
            )
        try:
            return await collection.query.near_vector(
                near_vector=vector,
                target_vector=vector_field.name if self.named_vectors and vector_field else None,
                return_metadata=MetadataQuery(distance=True),
                **args,
            )
        except WeaviateClosedClientError as ex:
            raise VectorSearchExecutionException(
                "Client is closed, please use the context manager or self.async_client.connect."
            ) from ex
        except Exception as ex:
            raise VectorSearchExecutionException(f"Failed searching using a vector: {ex}") from ex

    def _get_record_from_result(self, result: Any) -> Any:
        """Get the record from the returned search result."""
        return result

    def _get_score_from_result(self, result: Any) -> float | None:
        if result.metadata and result.metadata.score is not None:
            return result.metadata.score
        if result.metadata and result.metadata.distance is not None:
            return result.metadata.distance
        return None

    @override
    def _serialize_dicts_to_store_models(self, records: Sequence[dict[str, Any]], **kwargs: Any) -> Sequence[Any]:
        """Create a data object from a record based on the data model definition."""
        records_in_store_model: list[DataObject[dict[str, Any], None]] = []
        for record in records:
            properties = {field.storage_name or field.name: record[field.name] for field in self.definition.data_fields}
            # If key is None, Weaviate will generate a UUID
            key = record[self.definition.key_field.storage_name or self.definition.key_field.name]
            if self.named_vectors:
                vectors = {
                    vector.storage_name or vector.name: record[vector.name] for vector in self.definition.vector_fields
                }
            else:
                vectors = record[self.definition.vector_fields[0].storage_name or self.definition.vector_fields[0].name]
            records_in_store_model.append(DataObject(properties=properties, uuid=key, vector=vectors))
        return records_in_store_model

    @override
    def _deserialize_store_models_to_dicts(self, records: Sequence[Any], **kwargs: Any) -> Sequence[dict[str, Any]]:
        records_in_dict: list[dict[str, Any]] = []
        for record in records:
            properties = {
                field.name: record.properties[field.storage_name or field.name]
                for field in self.definition.data_fields
                if (field.storage_name or field.name) in record.properties
            }
            key = {self.definition.key_field.name: record.uuid}
            if not record.vector:
                records_in_dict.append(properties | key)
            else:
                if self.named_vectors:
                    vectors = {
                        vector.name: record.vector[vector.storage_name or vector.name]
                        for vector in self.definition.vector_fields
                        if (vector.storage_name or vector.name) in record.vector
                    }
                else:
                    vector_field = self.definition.vector_fields[0]
                    vectors = {vector_field.name: record.vector["default"]}
                records_in_dict.append(properties | key | vectors)
        return records_in_dict

    @override
    async def ensure_collection_exists(self, **kwargs) -> None:
        """Create the collection in Weaviate.

        Args:
            **kwargs: Additional keyword arguments, when any kwargs are supplied they are passed
                straight to the Weaviate client.collections.create method.
                Make sure to check the arguments of that method for the specifications.
        """
        if not self.named_vectors and len(self.definition.vector_field_names) != 1:
            raise VectorStoreOperationException(
                "Named vectors must be enabled if there is not exactly one vector field in the data model definition."
            )
        if kwargs:
            try:
                await self.async_client.collections.create(**kwargs)
            except WeaviateClosedClientError as ex:
                raise VectorStoreOperationException(
                    "Client is closed, please use the context manager or self.async_client.connect."
                ) from ex
            except Exception as ex:
                raise VectorStoreOperationException(f"Failed to create collection: {ex}") from ex
        try:
            if self.named_vectors:
                vector_index_config = None
                vectorizer_config = _definition_to_weaviate_named_vectors(self.definition)
            else:
                vector_field = self.definition.vector_fields[0]
                if (
                    vector_field.distance_function is None
                    or vector_field.distance_function not in DISTANCE_FUNCTION_MAP
                ):
                    raise VectorStoreModelValidationError(
                        f"Distance function {vector_field.distance_function} is not supported by Weaviate."
                    )
                if vector_field.index_kind is None or vector_field.index_kind not in INDEX_KIND_MAP:
                    raise VectorStoreModelValidationError(
                        f"Index kind {vector_field.index_kind} is not supported by Weaviate."
                    )
                vector_index_config = INDEX_KIND_MAP[vector_field.index_kind](
                    distance_metric=DISTANCE_FUNCTION_MAP[vector_field.distance_function]
                )
                vectorizer_config = None

            properties: list[Property] = []
            for field in self.definition.data_fields:
                properties.append(
                    Property(
                        name=field.storage_name or field.name,
                        data_type=DATATYPE_MAP[field.type_ or "default"],
                        index_filterable=field.is_indexed,
                        index_full_text=field.is_full_text_indexed,
                    )
                )

            await self.async_client.collections.create(
                name=self.collection_name,
                properties=properties,
                vector_index_config=vector_index_config,
                vectorizer_config=vectorizer_config,
            )
        except WeaviateClosedClientError as ex:
            raise VectorStoreOperationException(
                "Client is closed, please use the context manager or self.async_client.connect."
            ) from ex
        except Exception as ex:
            raise VectorStoreOperationException(f"Failed to create collection: {ex}") from ex

    @override
    async def collection_exists(self, **kwargs) -> bool:
        """Check if the collection exists in Weaviate.

        Args:
            **kwargs: Additional keyword arguments.

        Returns:
            bool: Whether the collection exists.
        """
        try:
            return await self.async_client.collections.exists(self.collection_name)
        except WeaviateClosedClientError as ex:
            raise VectorStoreOperationException(
                "Client is closed, please use the context manager or self.async_client.connect."
            ) from ex
        except Exception as ex:
            raise VectorStoreOperationException(f"Failed to check if collection exists: {ex}") from ex

    @override
    async def ensure_collection_deleted(self, **kwargs) -> None:
        """Delete the collection in Weaviate.

        Args:
            **kwargs: Additional keyword arguments.
        """
        try:
            await self.async_client.collections.delete(self.collection_name)
        except WeaviateClosedClientError as ex:
            raise VectorStoreOperationException(
                "Client is closed, please use the context manager or self.async_client.connect."
            ) from ex
        except Exception as ex:
            raise VectorStoreOperationException(f"Failed to delete collection: {ex}") from ex

    @override
    async def __aenter__(self) -> "WeaviateCollection":
        """Enter the context manager."""
        await self.async_client.connect()
        return self

    @override
    async def __aexit__(self, exc_type, exc_value, traceback) -> None:
        """Exit the context manager."""
        if self.managed_client:
            await self.async_client.close()

    def _validate_data_model(self):
        super()._validate_data_model()
        if self.named_vectors and len(self.definition.vector_field_names) > 1:
            raise VectorStoreModelValidationError(
                "Named vectors must be enabled if there are more then 1 vector fields in the data model definition."
            )


@release_candidate
class WeaviateStore(VectorStore):
    """A Weaviate store is a vector store that uses Weaviate as the backend."""

    async_client: WeaviateAsyncClient

    def __init__(
        self,
        url: str | None = None,
        api_key: str | None = None,
        local_host: str | None = None,
        local_port: int | None = None,
        local_grpc_port: int | None = None,
        use_embed: bool = False,
        embedding_generator: EmbeddingGeneratorBase | None = None,
        async_client: WeaviateAsyncClient | None = None,
        env_file_path: str | None = None,
        env_file_encoding: str | None = None,
    ):
        """Initialize a Weaviate store.

        Args:
            url: The Weaviate URL.
            api_key: The Weaviate API key.
            local_host: The local Weaviate host (i.e. Weaviate in a Docker container).
            local_port: The local Weaviate port.
            local_grpc_port: The local Weaviate gRPC port.
            use_embed: Whether to use the embedded client.
            embedding_generator: The embedding generator.
            async_client: A custom Weaviate async client.
            env_file_path: The path to the environment file.
            env_file_encoding: The encoding of the environment file.
        """
        managed_client: bool = False
        if not async_client:
            managed_client = True
            weaviate_settings = WeaviateSettings(
                url=url,
                api_key=api_key,
                local_host=local_host,
                local_port=local_port,
                local_grpc_port=local_grpc_port,
                use_embed=use_embed,
                env_file_path=env_file_path,
                env_file_encoding=env_file_encoding,
            )

            try:
                if weaviate_settings.url:
                    async_client = use_async_with_weaviate_cloud(
                        cluster_url=str(weaviate_settings.url),
                        auth_credentials=Auth.api_key(weaviate_settings.api_key.get_secret_value())
                        if weaviate_settings.api_key
                        else None,
                    )
                elif weaviate_settings.local_host:
                    kwargs: dict[str, Any] = {
                        "host": weaviate_settings.local_host,
                        "port": weaviate_settings.local_port,
                        "grpc_port": weaviate_settings.local_grpc_port,
                    }
                    kwargs = {k: v for k, v in kwargs.items() if v is not None}
                    async_client = use_async_with_local(
                        **kwargs,
                    )
                elif weaviate_settings.use_embed:
                    async_client = use_async_with_embedded()
                else:
                    raise NotImplementedError(
                        "Weaviate settings must specify either a custom client, a Weaviate Cloud instance,",
                        " a local Weaviate instance, or the client embedding options.",
                    )
            except Exception as e:
                raise VectorStoreInitializationException(f"Failed to initialize Weaviate client: {e}")

        super().__init__(
            async_client=async_client, managed_client=managed_client, embedding_generator=embedding_generator
        )

    @override
    def get_collection(
        self,
        record_type: type[TModel],
        *,
        definition: VectorStoreCollectionDefinition | None = None,
        collection_name: str | None = None,
        embedding_generator: EmbeddingGeneratorBase | None = None,
        **kwargs: Any,
    ) -> WeaviateCollection:
        return WeaviateCollection(
            record_type=record_type,
            definition=definition,
            collection_name=collection_name,
            embedding_generator=embedding_generator or self.embedding_generator,
            async_client=self.async_client,
            **kwargs,
        )

    @override
    async def list_collection_names(self, **kwargs) -> Sequence[str]:
        async with self.async_client:
            try:
                collections = await self.async_client.collections.list_all()
                return [collection.name for collection in collections.values()]
            except Exception as e:
                raise VectorStoreOperationException(f"Failed to list Weaviate collections: {e}")

    @override
    async def __aenter__(self) -> Self:
        """Enter the context manager."""
        if not self.async_client.is_connected():
            try:
                await self.async_client.connect()
            except WeaviateConnectionError as exc:
                raise VectorStoreException("Weaviate client cannot connect.") from exc
        return self

    @override
    async def __aexit__(self, exc_type, exc_value, traceback) -> None:
        """Exit the context manager."""
        if self.managed_client:
            await self.async_client.close()
