from __future__ import annotations

import logging
import uuid
from typing import (
    TYPE_CHECKING,
    Any,
    Iterable,
    Sequence,
)

import requests
from enum import Enum

from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.vectorstores import VectorStore
from endee import Endee as EndeeClient, Precision, constants

if TYPE_CHECKING:
    pass

if TYPE_CHECKING:
    from collections.abc import Generator, Iterable, Sequence

    from langchain_endee.sparse_embeddings import SparseEmbeddings
logger = logging.getLogger(__name__)


class EndeeVectorStoreError(Exception):
    """`EndeeVectorStore` related exceptions."""




class RetrievalMode(str, Enum):
    """Modes for retrieving vectors from Qdrant."""

    DENSE = "dense"
    HYBRID = "hybrid"



class EndeeVectorStore(VectorStore):
    """Endee vector store integration.

    Endee is a high-performance vector database designed for speed and efficiency.
    It enables rapid Approximate Nearest Neighbor (ANN) searches using the HNSW 
    algorithm with support for multiple distance metrics, metadata filtering, and 
    configurable precision levels.

    Setup:
        Install `endee-langchain` package.

        ```bash
        pip install -qU endee-langchain
        ```

    Key init args — indexing params:
        index_name:
            Name of the Endee index.
        embedding:
            Embedding function to use.
        dimension:
            Dimension of the vector embeddings.

    Key init args — client params:
        api_token:
            Endee API token for authentication.
        space_type:
            Distance metric (cosine, l2, ip).
        precision:
            Precision level for vector quantization.

    Instantiate:
        ```python
        from langchain_endee import EndeeVectorStore
        from langchain_openai import OpenAIEmbeddings

        vector_store = EndeeVectorStore(
            api_token="your-api-token",
            index_name="demo_index",
            embedding=OpenAIEmbeddings(),
            dimension=1536,
            space_type=SPACE_TYPE.COSINE,
            precision=PRECISION.INT8D,
        )
        ```

    Add Documents:
        ```python
        from langchain_core.documents import Document
        from uuid import uuid4

        document_1 = Document(page_content="foo", metadata={"baz": "bar"})
        document_2 = Document(page_content="thud", metadata={"bar": "baz"})
        document_3 = Document(page_content="i will be deleted :(")

        documents = [document_1, document_2, document_3]
        ids = [str(uuid4()) for _ in range(len(documents))]
        vector_store.add_documents(documents=documents, ids=ids)
        ```

    Delete Documents:
        ```python
        vector_store.delete(ids=[ids[-1]])
        ```

    Search:
        ```python
        results = vector_store.similarity_search(
            query="thud",
            k=1,
        )
        for doc in results:
            print(f"* {doc.page_content} [{doc.metadata}]")
        ```

        ```python
        * thud [{"bar": "baz", "_id": "..."}]
        ```

    Search with filter:
        ```python
        # Use Endee filter format
        results = vector_store.similarity_search(
            query="thud",
            k=1,
            filter=[{"bar": {"$eq": "baz"}}],
        )
        for doc in results:
            print(f"* {doc.page_content} [{doc.metadata}]")
        ```

        ```python
        * thud [{"bar": "baz", "_id": "..."}]
        ```

    Search with score:
        ```python
        results = vector_store.similarity_search_with_score(query="qux", k=1)
        for doc, score in results:
            print(f"* [SIM={score:.3f}] {doc.page_content} [{doc.metadata}]")
        ```

        ```python
        * [SIM=0.833] foo [{'baz': 'bar', '_id': '...'}]
        ```

    Use as Retriever:
        ```python
        retriever = vector_store.as_retriever(
            search_kwargs={"k": 1},
        )
        retriever.invoke("thud")
        ```

        ```python
        [Document(metadata={"bar": "baz", "_id": "..."}, page_content="thud")]
        ```
    """  # noqa: E501

    # Maximum token limits for common embedding models
    EMBEDDING_MODEL_LIMITS = {
        "openai": 8191,      # text-embedding-3-small/large, text-embedding-ada-002
        "cohere": 512,       # embed-english-v3.0, embed-multilingual-v3.0
        "huggingface": 512,  # Most sentence-transformers models
        "default": 512,      # Conservative default
    }
    

    CONTENT_KEY: str = "text"
    METADATA_KEY: str = "metadata"

    def __init__(
        self,
        embedding: Embeddings,
        api_token: str | None = None,
        index_name: str | None = None,
        max_text_length: int | None = None,
        embedding_model_type: str | None = None,
        endee_client: EndeeClient | None = None,
        retrieval_mode: RetrievalMode = RetrievalMode.DENSE,
        dimension: int | None = None,
        # space_type: SpaceType.COSINE
        space_type: str = "cosine",
        precision: str = Precision.INT8D,
        M: int = constants.DEFAULT_M,
        ef_con: int = constants.DEFAULT_EF_CON,
        content_payload_key: str = CONTENT_KEY,
        sparse_embedding: SparseEmbeddings | None = None,
        metadata_payload_key: str = METADATA_KEY,
        force_recreate: bool = False,  # noqa: FBT001, FBT002
        validate_index_config: bool = True,  # noqa: FBT001, FBT002
    ) -> None:
        """Initialize a new instance of `EndeeVectorStore`.

        Args:
            embedding: Embedding function to use for generating vector embeddings.
            api_token: Endee API token. Required if `endee_client` is not provided
                for the cloud server, if docker deployment locally, not needed.
            index_name: Name of the Endee index. Required if creating or using an 
                index.
            max_text_length: Maximum text length in tokens. If None, will be
                auto-detected based on embedding_model_type or set to default.
            embedding_model_type: Type of embedding model ('openai', 'cohere', 
                'huggingface'). If None, will attempt to auto-detect from embedding
                class name, otherwise uses 'default'.
            endee_client: Existing Endee client instance. If provided, `api_token` 
                is ignored.
            dimension: Dimension of vector embeddings. Required when creating a new 
                index. Should match your embedding model's output dimension.
            space_type: Distance metric to use. Options: Space_Type.COSINE, 
                Space_Type.L2, SpaceType.IP 
                Default: Space_Type.COSINE.
            precision: Precision level for vectors. Options: Precision.BINARY2, 
                Precision.FLOAT16, Precision.INT8D, Precision.INT16D, 
                Precision.FLOAT32. Default: Precision.INT8D.
            M: HNSW graph connectivity parameter. Higher values increase recall but 
                use more memory. Default: 16.
            ef_con: HNSW construction parameter. Higher values improve index quality 
                but slow down indexing. Default: 128.
            content_payload_key: Key name for storing document content in metadata.
                Default: "text".
            metadata_payload_key: Key name for storing document metadata. 
                Default: "metadata".
            force_recreate: Whether to delete and recreate the index if it exists.
                Default: False.
            validate_index_config: Whether to validate index configuration on 
                initialization. Default: True.

        Raises:
            ValueError: If required parameters are missing or invalid.
            EndeeVectorStoreError: If index initialization fails.

        Example:
            ```python
            from langchain_endee import EndeeVectorStore
            from langchain_openai import OpenAIEmbeddings

            # Create with new index
            vector_store = EndeeVectorStore(
                api_token="your-token",
                index_name="my-index",
                embedding=OpenAIEmbeddings(),
                dimension=1536,
                space_type=Space_Type.COSINE,
                precision=Precision.INT8D,
            )

            # Or use existing Endee client
            from endee import Endee
            client = Endee(token="your-token")
            
            vector_store = EndeeVectorStore(
                endee_client=client,
                index_name="my-index",
                embedding=OpenAIEmbeddings(),
            )
            ```
        """
        if embedding is None:
            msg = "Embedding function must be provided"
            raise ValueError(msg)

        self._embeddings = embedding
        self.content_payload_key = content_payload_key
        self.metadata_payload_key = metadata_payload_key
        self.space_type = space_type
        self.precision = precision
        self.dimension = dimension
        self.M = M
        self.ef_con = ef_con
        self.retrieval_mode = retrieval_mode
        self._sparse_embeddings = sparse_embedding














        # Initialize Endee client
        if endee_client is None:
            if api_token is None:
                try:
                    resp = requests.get(
                        "http://127.0.0.1:8080/api/v1/health",
                        timeout=2,
                    )
                    if resp.status_code != 200:
                        raise RuntimeError

                    data = resp.json()
                    if data.get("status") != "ok":
                        raise RuntimeError

                    self._client = EndeeClient(token="")
                except Exception:
                    raise ValueError(
                        "api_token must be provided if endee_client is not provided. "
                        "Local Endee server is also not running on http://127.0.0.1:8080"
                    )
            else:    
                self._client = EndeeClient(token=api_token)
        else:
            self._client = endee_client

        # Initialize or get index
        if index_name is None:
            msg = "index_name must be provided"
            raise ValueError(msg)

        self.index_name = index_name

        # Check if index exists
        existing_indexes = self._client.list_indexes()
        index_exists = any(
            idx.get("name") == index_name
            for idx in existing_indexes.get("indexes", [])
        )

        if index_exists and force_recreate:
            logger.info(f"Deleting existing index: {index_name}")
            self._client.delete_index(index_name)
            index_exists = False

        if not index_exists:
            # Create new index
            # Validate dimension first
            if dimension is None:
                msg = (
                    f"Index '{index_name}' does not exist and dimension is not "
                    "provided. Please provide dimension to create a new index."
                )
                raise ValueError(msg)

            logger.info(f"Creating new Endee index: {index_name}")
            sparse_dim=0
            if sparse_embedding is not None:
                sparse_dim=30522
            


            self._create_index(
                name=index_name,
                dimension=dimension,
                space_type=space_type,
                precision=precision,
                M=M,
                ef_con=ef_con,
                sparse_dim=sparse_dim
            )

        else:
            logger.info(f"Using existing Endee index: {index_name}")

        self._index = self._client.get_index(name=index_name)

        if validate_index_config:
            self._validate_index_config()

        # Auto-detect embedding model type if not provided
        if embedding_model_type is None:
            embedding_model_type = self._detect_embedding_model_type(embedding)
        
        self.embedding_model_type = embedding_model_type
    
        
        # Set max_text_length based on model type if not explicitly provided
        if max_text_length is None:
            self.max_text_length = self.EMBEDDING_MODEL_LIMITS.get(
                embedding_model_type, 
                self.EMBEDDING_MODEL_LIMITS["default"]
            )
            logger.info(
                f"Auto-detected embedding model type: '{embedding_model_type}'. "
                f"Setting max_text_length to {self.max_text_length} tokens."
            )
        else:
            self.max_text_length = max_text_length
            logger.info(f"Using custom max_text_length: {max_text_length} tokens.")

    @property
    def embeddings(self) -> Embeddings:
        """Get the embeddings instance that is being used.

        Returns:
            Embeddings: An instance of `Embeddings`.
        """
        return self._embeddings

    @property
    def client(self) -> EndeeClient:
        """Get the Endee client instance that is being used.

        Returns:
            EndeeClient: An instance of Endee client.
        """
        return self._client
    
    @property
    def sparse_embeddings(self) -> SparseEmbeddings:
        """Get the sparse embeddings instance that is being used.

        Raises:
            ValueError: If sparse embeddings are `None`.

        Returns:
            SparseEmbeddings: An instance of `SparseEmbeddings`.

        """
        if self._sparse_embeddings is None:
            msg = (
                "Sparse embeddings are `None`. "
                "Please set using the `sparse_embedding` parameter."
            )
            raise ValueError(msg)
        return self._sparse_embeddings

    @property
    def index(self) -> Any:
        """Get the Endee index instance that is being used.

        Returns:
            Endee index instance.
        """
        return self._index

    def _create_index(
        self,
        name: str,
        dimension: int,
        space_type: str,
        precision: str,
        M: int,  # noqa: N803
        ef_con: int,
        sparse_dim:int,
    ) -> None:
        """Create a new Endee index.

        Args:
            name: Name of the index.
            dimension: Dimension of vectors.
            space_type: Distance metric (cosine, l2, ip).
            precision: Precision level (binary2, int8d, int16d, float16, float32).
            M: HNSW graph connectivity parameter.
            ef_con: HNSW construction parameter.

        Raises:
            EndeeVectorStoreError: If index creation fails.
        """
        try:
            create_params = {
                "name": name,
                "dimension": dimension,
                "space_type": space_type,
                "M": M,
                "ef_con": ef_con,
                "precision": precision,
                "sparse_dim":sparse_dim,
            }

            self._client.create_index(**create_params)
        except Exception as e:
            msg = f"Failed to create Endee index '{name}': {e}"
            raise EndeeVectorStoreError(msg) from e

    def _validate_index_config(self) -> None:
        """Validate that the index configuration matches the vector store settings.

        Raises:
            EndeeVectorStoreError: If index configuration is incompatible.
        """
        try:
            index_info = self._index.describe()
            
            # Validate dimension if available in index info
            if "dimension" in index_info and self.dimension is not None:
                if index_info["dimension"] != self.dimension:
                    msg = (
                        f"Existing index has dimension {index_info['dimension']} "
                        f"but expected {self.dimension}. "
                        "Set force_recreate=True to recreate the index."
                    )
                    raise EndeeVectorStoreError(msg)
        except Exception as e:
            logger.warning(f"Could not validate index config: {e}")

    def _detect_embedding_model_type(self, embedding: Embeddings) -> str:
        """Auto-detect the embedding model type from the embedding instance.
        
        Args:
            embedding: Embeddings instance.
            
        Returns:
            Detected model type string ('openai', 'cohere', 'huggingface', 'default').
        """
        class_name = embedding.__class__.__name__.lower()
        module_name = embedding.__class__.__module__.lower()
        
        # Check class name and module for common patterns
        if "openai" in class_name or "openai" in module_name:
            return "openai"
        elif "cohere" in class_name or "cohere" in module_name:
            return "cohere"
        elif (
            "huggingface" in class_name 
            or "huggingface" in module_name
            or "sentence" in class_name
            or "transformers" in module_name
        ):
            return "huggingface"
        else:
            logger.warning(
                f"Could not auto-detect embedding model type from {class_name}. "
                f"Using default limit of {self.EMBEDDING_MODEL_LIMITS['default']} tokens."
            )
            return "default"
        
    def _estimate_tokens(self, text: str) -> int:
        """Estimate the number of tokens in a text.
        
        Uses a simple heuristic: ~4 characters per token for English text.
        
        Args:
            text: Input text string.
            
        Returns:
            Estimated token count.
        """
        # Simple approximation: 1 token ≈ 4 characters
        return len(text) // 4
    
    def _truncate_text(self, text: str, max_tokens: int | None = None) -> str:
        """Truncate text to fit within token limit.
        
        Args:
            text: Input text to truncate.
            max_tokens: Maximum number of tokens. If None, uses self.max_text_length.
            
        Returns:
            Truncated text string.
        """
        if max_tokens is None:
            max_tokens = self.max_text_length
            
        estimated_tokens = self._estimate_tokens(text)
        
        if estimated_tokens <= max_tokens:
            return text
            
        # Truncate to approximate character count
        # Leaving some buffer (90%) to be safe
        max_chars = int(max_tokens * 4 * 0.9)
        truncated = text[:max_chars]
        
        logger.warning(
            f"Text truncated from ~{estimated_tokens} to ~{max_tokens} tokens "
            f"(model type: {self.embedding_model_type})"
        )
        
        return truncated
    
    def _validate_batch_size(self, batch_size: int) -> int:
        """Validate and adjust batch size to Endee's limits.
        
        Args:
            batch_size: Requested batch size.
            
        Returns:
            Valid batch size.
            
        Raises:
            ValueError: If batch_size exceeds maximum allowed.
        """
        if batch_size > constants.MAX_VECTORS_PER_BATCH:
            msg = (
                f"batch_size ({batch_size}) cannot exceed "
                f"{constants.MAX_VECTORS_PER_BATCH} (Endee's maximum batch size)"
            )
            raise ValueError(msg)
        return batch_size

    def _prepare_texts_and_metadata(
        self,
        texts: Iterable[str],
        metadatas: list[dict] | None,
        ids: list[str] | None,
    ) -> tuple[list[str], list[str], list[dict]]:
        """Prepare and validate texts, IDs, and metadata.
        
        Args:
            texts: Input texts.
            metadatas: Optional metadata list.
            ids: Optional ID list.
            
        Returns:
            Tuple of (processed_texts, ids, metadatas).
            
        Raises:
            ValueError: If lengths don't match.
        """
        texts = list(texts)
        ids = list(ids) if ids else [str(uuid.uuid4()) for _ in texts]
        metadatas = metadatas or [{} for _ in texts]
        
        # Validate lengths match
        if len(texts) != len(ids):
            msg = f"Number of texts ({len(texts)}) must match number of ids ({len(ids)})"
            raise ValueError(msg)
            
        if len(texts) != len(metadatas):
            msg = (
                f"Number of texts ({len(texts)}) must match "
                f"number of metadatas ({len(metadatas)})"
            )
            raise ValueError(msg)
        
        # Truncate texts if needed
        processed_texts = [self._truncate_text(text) for text in texts]
        
        return processed_texts, ids, metadatas
    
    def _generate_embeddings_in_batches(
        self,
        texts: list[str],
        embedding_chunk_size: int,
    ) -> list[list[float]]:
        """Generate embeddings for texts in batches.
        
        Args:
            texts: List of texts to embed.
            embedding_chunk_size: Batch size for embedding generation.
            
        Returns:
            List of embedding vectors.
            
        Raises:
            Exception: If embedding generation fails.
        """
        embeddings = []
        
        for i in range(0, len(texts), embedding_chunk_size):
            sub_texts = texts[i : i + embedding_chunk_size]
            
            try:
                sub_embeddings = self.embeddings.embed_documents(sub_texts)
                embeddings.extend(sub_embeddings)
            except Exception as e:
                logger.error(
                    f"Error generating embeddings for batch {i}-{i+len(sub_texts)}: {e}"
                )
                raise
                
        return embeddings

    def _build_upsert_entries(
        self,
        ids: list[str],
        embeddings: list[list[float]],
        texts: list[str],
        metadatas: list[dict],
        index: list[list[int]] | None = None,
        value: list[list[float]] | None = None,
    ) -> list[dict[str, Any]]:
        """Build entry dictionaries for upserting to Endee.

        Args:
            ids: List of document IDs.
            embeddings: List of embedding vectors.
            texts: List of text content.
            metadatas: List of metadata dictionaries.
            index: Optional list of sparse vector indices for hybrid index.
                Each element is a list of integer indices corresponding to
                non-zero positions in the sparse embedding.
            value: Optional list of sparse vector values for hybrid index.
                Each element is a list of float values corresponding to the
                indices in the `index` parameter.

        Returns:
            List of entry dictionaries ready for upsert.
        """
        entries = []

        for i, (entry_id, embedding, text, metadata) in enumerate(zip(
            ids, embeddings, texts, metadatas, strict=False
        )):
            # Build metadata payload
            meta = {
                self.content_payload_key: text,
                self.metadata_payload_key: metadata,
            }

            # Extract filter values - exclude internal keys
            filter_data = {
                key: val
                for key, val in metadata.items()
                # if key not in [self.content_payload_key, self.metadata_payload_key]
            }

            entry = {
                "id": entry_id,
                "vector": embedding,
                "meta": meta,
                "filter": filter_data,
            }

            if index is not None and value is not None:
                entry["sparse_indices"] = index[i]
                entry["sparse_values"] = value[i]

            entries.append(entry)

        return entries
    
    def _upsert_batch(self, entries: list[dict[str, Any]]) -> None:
        """Upsert a batch of entries to Endee index.
        
        Args:
            entries: List of entry dictionaries to upsert.
            
        Raises:
            Exception: If upsert operation fails.
        """
        try:
            self._index.upsert(entries)
        except Exception as e:
            logger.error(f"Error upserting batch of {len(entries)} entries: {e}")
            raise


    def _generate_sparse_embeddings_in_batches(
        self,
        texts: list[str],
        embedding_chunk_size: int,
    ) -> tuple[list[list[int]], list[list[float]]]:
        """Generate sparse embeddings for texts in batches.

        Args:
            texts: List of texts to embed.
            embedding_chunk_size: Batch size for embedding generation.

        Returns:
            Tuple of (indices, values) where each is a list per document.

        Raises:
            Exception: If sparse embedding generation fails.
        """
        all_indices: list[list[int]] = []
        all_values: list[list[float]] = []

        for i in range(0, len(texts), embedding_chunk_size):
            sub_texts = texts[i : i + embedding_chunk_size]

            try:
                sparse_vectors = self.sparse_embeddings.embed_documents(sub_texts)
                for sv in sparse_vectors:
                    all_indices.append(sv.indices)
                    all_values.append(sv.values)
            except Exception as e:
                logger.error(
                    f"Error generating sparse embeddings for batch {i}-{i+len(sub_texts)}: {e}"
                )
                raise

        return all_indices, all_values

    
    def add_texts(
        self,
        texts: Iterable[str],
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        batch_size: int = constants.MAX_VECTORS_PER_BATCH,
        embedding_chunk_size: int = 100,
        **kwargs: Any,
    ) -> list[str]:
        """Add texts with embeddings to the vector store.
        
        This method handles:
        - Automatic text truncation based on detected embedding model type
        - Batch processing for large datasets
        - Embedding generation in configurable chunks
        - Metadata and filter preparation
        
        Args:
            texts: Iterable of strings to add to the vectorstore.
            metadatas: Optional list of metadatas associated with the texts.
                Must have same length as texts if provided.
            ids: Optional list of ids to associate with the texts. If not provided,
                UUIDs will be generated. Must have same length as texts if provided.
            batch_size: Batch size for insertion to Endee. Max 1000. Default: 1000.
            embedding_chunk_size: Batch size for embedding generation. Smaller values
                use less memory but may be slower. Default: 100.
            **kwargs: Additional keyword arguments (currently unused).
            
        Returns:
            List of ids from adding the texts into the vectorstore.
            
        Raises:
            ValueError: If batch_size exceeds 1000 (Endee's maximum) or if
                lengths of texts, ids, and metadatas don't match.
            Exception: If embedding generation or upsert operations fail.
            
        Example:
            ```python
            # Basic usage - automatically uses detected model limits
            texts = ["foo", "bar", "baz"]
            ids = vector_store.add_texts(texts)
            
            # With metadata
            metadatas = [{"key": "val1"}, {"key": "val2"}, {"key": "val3"}]
            ids = vector_store.add_texts(texts, metadatas=metadatas)
            
            # With custom IDs and smaller batches
            custom_ids = ["id1", "id2", "id3"]
            ids = vector_store.add_texts(
                texts, 
                metadatas=metadatas,
                ids=custom_ids,
                batch_size=500,
                embedding_chunk_size=50
            )
            ```
        """
        # Validate batch size
        batch_size = self._validate_batch_size(batch_size)
        
        # Prepare and validate inputs (includes automatic truncation)
        processed_texts, ids, metadatas = self._prepare_texts_and_metadata(
            texts, metadatas, ids
        )
        
        # Process in batches
        for i in range(0, len(processed_texts), batch_size):
            batch_end = i + batch_size
            
            # Extract batch
            chunk_texts = processed_texts[i:batch_end]
            chunk_ids = ids[i:batch_end]
            chunk_metadatas = metadatas[i:batch_end]
            
            logger.info(
                f"Processing batch {i//batch_size + 1}: "
                f"{len(chunk_texts)} texts"
            )
            
            # Generate embeddings for this batch
            embeddings = self._generate_embeddings_in_batches(
                chunk_texts, embedding_chunk_size
            )

            sparse_indices = None
            sparse_values = None
            if self.retrieval_mode == RetrievalMode.HYBRID:
                sparse_indices, sparse_values = self._generate_sparse_embeddings_in_batches(
                    chunk_texts, embedding_chunk_size
                )





            
            # Build entries for upsert
            entries = self._build_upsert_entries(
                chunk_ids, embeddings, chunk_texts, chunk_metadatas,
                index=sparse_indices, value=sparse_values,
            )
            
            # Upsert to Endee
            self._upsert_batch(entries)
            
        logger.info(f"Successfully added {len(ids)} texts to vector store")
        return ids

    @classmethod
    def from_texts(
        cls,
        texts: list[str],
        embedding: Embeddings,
        metadatas: list[dict] | None = None,
        *,
        ids: list[str] | None = None,
        api_token: str | None = None,
        index_name: str | None = None,
        endee_client: EndeeClient | None = None,
        dimension: int | None = None,
        # space_type: SpaceType.COSINE
        space_type: str = "cosine",
        precision: str = Precision.INT8D,
        M: int = constants.DEFAULT_M,
        ef_con: int = constants.DEFAULT_EF_CON,
        content_payload_key: str = CONTENT_KEY,
        metadata_payload_key: str = METADATA_KEY,
        batch_size: int = constants.MAX_VECTORS_PER_BATCH,

        retrieval_mode: RetrievalMode = RetrievalMode.DENSE,
        sparse_embedding: SparseEmbeddings | None = None,

        embedding_chunk_size: int = 100,
        force_recreate: bool = False,  # noqa: FBT001, FBT002
        validate_index_config: bool = True,  # noqa: FBT001, FBT002
        **kwargs: Any,
    ) -> EndeeVectorStore:
        """Construct an instance of `EndeeVectorStore` from a list of texts.

        This is a user-friendly interface that:

        1. Creates embeddings, one for each text
        2. Creates an Endee index if it doesn't exist
        3. Adds the text embeddings to the Endee database

        This is intended to be a quick way to get started.

        Args:
            texts: List of texts to add.
            embedding: Embedding function.
            metadatas: Optional list of metadatas associated with texts.
            ids: Optional list of ids. If not provided, UUIDs will be generated.
            api_token: Endee API token. Required if `endee_client` is not provided.
            index_name: Index name. Required.
            endee_client: Optional Endee client instance.
            dimension: Vector dimension. Required when creating a new index. Should
                match your embedding model's output dimension (max 10000).
            space_type: Distance metric to use. Options: Space_Type.COSINE, 
                Space_Type.L2, SpaceType.IP 
                Default: Space_Type.COSINE.
            precision: Precision level for vectors. Options: Precision.BINARY2, 
                Precision.FLOAT16, Precision.INT8D, Precision.INT16D, 
                Precision.FLOAT32. Default: Precision.INT8D.
            M: HNSW graph connectivity parameter. Higher values increase recall but 
                use more memory. Default: 16.
            ef_con: HNSW construction parameter. Higher values improve index quality 
                but slow down indexing. Default: 128.
            content_payload_key: Key for storing text content. Default: "text".
            metadata_payload_key: Key for storing metadata. Default: "metadata".
            batch_size: Batch size for insertion. Max 1000. Default: 1000.
            embedding_chunk_size: Batch size for embedding generation. Default: 100.
            force_recreate: Whether to delete and recreate the index if it exists.
                Default: False.
            validate_index_config: Whether to validate index config. Default: True.
            **kwargs: Additional arguments passed to `add_texts`.

        Returns:
            EndeeVectorStore instance.

        Raises:
            ValueError: If required parameters are missing.

        Example:
            ```python
            from langchain_endee import EndeeVectorStore
            from langchain_openai import OpenAIEmbeddings

            texts = ["foo", "bar", "baz"]
            embeddings = OpenAIEmbeddings()
            
            vector_store = EndeeVectorStore.from_texts(
                texts=texts,
                embedding=embeddings,
                api_token="your-token",
                index_name="my-index",
                dimension=1536,
            )
            ```
        """
        if dimension is None and endee_client is None:
            msg = "dimension must be explicitly provided when creating a new index"
            raise ValueError(msg)

        endee = cls(
            embedding=embedding,
            api_token=api_token,
            index_name=index_name,
            endee_client=endee_client,
            dimension=dimension,
            space_type=space_type,
            precision=precision,
            M=M,
 
            retrieval_mode=retrieval_mode,
            sparse_embedding=sparse_embedding,
            ef_con=ef_con,
            content_payload_key=content_payload_key,
            metadata_payload_key=metadata_payload_key,
            force_recreate=force_recreate,
            validate_index_config=validate_index_config,
        )

        endee.add_texts(
            texts=texts,
            metadatas=metadatas,
            ids=ids,
            batch_size=batch_size,
            embedding_chunk_size=embedding_chunk_size,
            **kwargs,
        )
        return endee

    @classmethod
    def from_documents(
        cls,
        documents: list[Document],
        embedding: Embeddings,
        ids: Sequence[str] | None = None,
        api_token: str | None = None,
        index_name: str | None = None,
        endee_client: EndeeClient | None = None,
        dimension: int | None = None,
        # space_type: SpaceType.COSINE
        space_type: str = "cosine",
        precision: str = Precision.INT8D,
   
        retrieval_mode: RetrievalMode = RetrievalMode.DENSE,
        sparse_embedding: SparseEmbeddings | None = None,
        M: int = constants.DEFAULT_M,
        ef_con: int = constants.DEFAULT_EF_CON,
        content_payload_key: str = CONTENT_KEY,
        metadata_payload_key: str = METADATA_KEY,
        batch_size: int = constants.MAX_VECTORS_PER_BATCH,
        embedding_chunk_size: int = 100,
        force_recreate: bool = False,  # noqa: FBT001, FBT002
        validate_index_config: bool = True,  # noqa: FBT001, FBT002
        **kwargs: Any,
    ) -> EndeeVectorStore:
        """Construct `EndeeVectorStore` from a list of documents.

        Args:
            documents: List of Document objects to add.
            embedding: Embedding function.
            ids: Optional list of ids. If not provided, UUIDs will be generated.
            api_token: Endee API token. Required if `endee_client` is not provided.
            index_name: Index name. Required.
            endee_client: Optional Endee client instance.
            dimension: Vector dimension. Required when creating a new index.
            space_type: Distance metric type. Default: "cosine".
            precision: Precision level. Default: "int8d".
            M: HNSW graph connectivity parameter. Default: 16.
            ef_con: HNSW construction parameter. Default: 128.
            content_payload_key: Key for storing text content. Default: "text".
            metadata_payload_key: Key for storing metadata. Default: "metadata".
            batch_size: Batch size for insertion. Max 1000. Default: 1000.
            embedding_chunk_size: Batch size for embedding generation. Default: 100.
            force_recreate: Whether to delete and recreate the index if it exists.
                Default: False.
            validate_index_config: Whether to validate index config. Default: True.
            **kwargs: Additional arguments passed to `add_texts`.

        Returns:
            EndeeVectorStore instance.

        Example:
            ```python
            from langchain_core.documents import Document
            from langchain_endee import EndeeVectorStore
            from langchain_openai import OpenAIEmbeddings

            docs = [
                Document(page_content="foo", metadata={"key": "value"}),
                Document(page_content="bar", metadata={"key": "other"}),
            ]
            
            vector_store = EndeeVectorStore.from_documents(
                documents=docs,
                embedding=OpenAIEmbeddings(),
                api_token="your-token",
                index_name="my-index",
                dimension=1536,
            )
            ```
        """
        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]
        return cls.from_texts(
            texts=texts,
            embedding=embedding,
            metadatas=metadatas,
            ids=ids,
            api_token=api_token,
            index_name=index_name,
            endee_client=endee_client,
            dimension=dimension,
            space_type=space_type,

        retrieval_mode=retrieval_mode,
        sparse_embedding=sparse_embedding,
            precision=precision,
            M=M,
            ef_con=ef_con,
            content_payload_key=content_payload_key,
            metadata_payload_key=metadata_payload_key,
            batch_size=batch_size,
            embedding_chunk_size=embedding_chunk_size,
            force_recreate=force_recreate,
            validate_index_config=validate_index_config,
            **kwargs,
        )

    @classmethod
    def from_existing_index(
        cls,
        index_name: str,
        embedding: Embeddings,
        api_token: str | None = None,
        endee_client: EndeeClient | None = None,
        content_payload_key: str = CONTENT_KEY,
        metadata_payload_key: str = METADATA_KEY,
        validate_index_config: bool = True,  # noqa: FBT001, FBT002
    ) -> EndeeVectorStore:
        """Construct `EndeeVectorStore` from existing index without adding data.

        Args:
            index_name: Name of the existing Endee index.
            embedding: Embedding function to use.
            api_token: Endee API token. Required if `endee_client` is not provided.
            endee_client: Optional Endee client instance.
            content_payload_key: Key for storing text content. Default: "text".
            metadata_payload_key: Key for storing metadata. Default: "metadata".
            validate_index_config: Whether to validate index config. Default: True.

        Returns:
            EndeeVectorStore instance.

        Raises:
            EndeeVectorStoreError: If index does not exist.

        Example:
            ```python
            from langchain_endee import EndeeVectorStore
            from langchain_openai import OpenAIEmbeddings

            vector_store = EndeeVectorStore.from_existing_index(
                index_name="my-existing-index",
                embedding=OpenAIEmbeddings(),
                api_token="your-token",
            )
            ```
        """
        # if api_token is None and endee_client is None:
        #     msg = "Either api_token or endee_client must be provided"
        #     raise ValueError(msg)

        return cls(
            embedding=embedding,
            api_token=api_token,
            index_name=index_name,
            endee_client=endee_client,
            content_payload_key=content_payload_key,
            metadata_payload_key=metadata_payload_key,
            validate_index_config=validate_index_config,
        )

    def similarity_search(
        self,
        query: str,
        k: int = 4,
        filter: list[dict[str, Any]] | None = None,  # noqa: A002
        ef: int = constants.DEFAULT_EF_SEARCH,
        prefilter_cardinality_threshold: int | None = None,
        filter_boost_percentage: int | None = None,
        **kwargs: Any,
    ) -> list[Document]:
        """Return docs most similar to query.

        Args:
            query: Text query.
            k: Number of results to return. Max 512. Default: 4.
            filter: Optional filter conditions in Endee format.
                Example: [{"status": {"$eq": "published"}}]
            ef: Runtime search parameter. Higher values improve recall but increase
                latency. Max 1024. Default: 128.
            prefilter_cardinality_threshold: Controls when search switches from
                HNSW filtered search to brute-force prefiltering. Default: None
                (uses server default of 10,000). Range: 1,000–1,000,000.
            filter_boost_percentage: Expands the internal HNSW candidate pool by
                this percentage when a filter is active. Default: None (uses
                server default of 0). Range: 0–100.
            **kwargs: Additional keyword arguments passed to the search.

        Returns:
            List of `Document` objects most similar to the query.

        Example:
            ```python
            # Simple search
            results = vector_store.similarity_search("my query", k=3)

            # Search with filter
            results = vector_store.similarity_search(
                query="my query",
                k=3,
                filter=[{"status": {"$eq": "published"}}]
            )
            ```
        """
        docs_and_scores = self.similarity_search_with_score(
            query, k=k, filter=filter, ef=ef,
            prefilter_cardinality_threshold=prefilter_cardinality_threshold,
            filter_boost_percentage=filter_boost_percentage,
            **kwargs,
        )
        return [doc for doc, _ in docs_and_scores]

    def similarity_search_with_score(
        self,
        query: str,
        k: int = 4,
        filter: list[dict[str, Any]] | None = None,  # noqa: A002
        ef: int = 128,
        prefilter_cardinality_threshold: int | None = None,
        filter_boost_percentage: int | None = None,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]:
        """Return docs most similar to query with similarity scores.

        Uses hybrid search (dense + sparse) when retrieval_mode is HYBRID,
        otherwise performs dense-only search.

        Args:
            query: Text query.
            k: Number of results to return. Max 512. Default: 4.
            filter: Optional filter conditions in Endee format.
            ef: Runtime search parameter. Max 1024. Default: 128.
            prefilter_cardinality_threshold: Controls when search switches from
                HNSW filtered search to brute-force prefiltering. Default: None
                (uses server default of 10,000). Range: 1,000–1,000,000.
            filter_boost_percentage: Expands the internal HNSW candidate pool by
                this percentage when a filter is active. Default: None (uses
                server default of 0). Range: 0–100.
            **kwargs: Additional keyword arguments passed to the search.

        Returns:
            List of tuples of (Document, similarity_score).

        Example:
            ```python
            results = vector_store.similarity_search_with_score("my query", k=3)
            for doc, score in results:
                print(f"Score: {score:.4f} - {doc.page_content}")
            ```
        """
        embedding = self.embeddings.embed_query(query)

        if self.retrieval_mode == RetrievalMode.HYBRID:
            sparse_vector = self.sparse_embeddings.embed_query(query)
            kwargs["sparse_indices"] = sparse_vector.indices
            kwargs["sparse_values"] = sparse_vector.values

        return self.similarity_search_by_vector_with_score(
            embedding, k=k, filter=filter, ef=ef,
            prefilter_cardinality_threshold=prefilter_cardinality_threshold,
            filter_boost_percentage=filter_boost_percentage,
            **kwargs,
        )

    def similarity_search_by_vector(
        self,
        embedding: list[float],
        k: int = 4,
        filter: list[dict[str, Any]] | None = None,  # noqa: A002
        ef: int = 128,
        prefilter_cardinality_threshold: int | None = None,
        filter_boost_percentage: int | None = None,
        **kwargs: Any,
    ) -> list[Document]:
        """Return docs most similar to embedding vector.

        Args:
            embedding: Query embedding vector.
            k: Number of results to return. Max 512. Default: 4.
            filter: Optional filter conditions in Endee format.
            ef: Runtime search parameter. Max 1024. Default: 128.
            prefilter_cardinality_threshold: Controls when search switches from
                HNSW filtered search to brute-force prefiltering. Default: None
                (uses server default of 10,000). Range: 1,000–1,000,000.
            filter_boost_percentage: Expands the internal HNSW candidate pool by
                this percentage when a filter is active. Default: None (uses
                server default of 0). Range: 0–100.
            **kwargs: Additional keyword arguments passed to the search.

        Returns:
            List of `Document` objects most similar to the query.

        Example:
            ```python
            embedding = embeddings.embed_query("my query")
            results = vector_store.similarity_search_by_vector(embedding, k=3)
            ```
        """
        docs_and_scores = self.similarity_search_by_vector_with_score(
            embedding, k=k, filter=filter, ef=ef,
            prefilter_cardinality_threshold=prefilter_cardinality_threshold,
            filter_boost_percentage=filter_boost_percentage,
            **kwargs,
        )
        return [doc for doc, _ in docs_and_scores]

    def similarity_search_by_vector_with_score(
        self,
        embedding: list[float],
        k: int = 4,
        filter: list[dict[str, Any]] | None = None,  # noqa: A002
        ef: int = constants.DEFAULT_EF_SEARCH,
        prefilter_cardinality_threshold: int | None = None,
        filter_boost_percentage: int | None = None,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]:
        """Return docs most similar to embedding vector with similarity scores.

        Args:
            embedding: Query embedding vector.
            k: Number of results to return. Max 512. Default: 4.
            filter: Optional filter conditions in Endee format.
                Supported operators: $eq, $in, $range
                Example: [{"score": {"$range": [70, 95]}}]
            ef: Runtime search parameter. Higher values improve recall.
                Max 1024. Default: 128.
            prefilter_cardinality_threshold: Controls when search switches from
                HNSW filtered search to brute-force prefiltering on the matched
                subset. Default: None (uses server default of 10,000).
                Range: 1,000–1,000,000.
            filter_boost_percentage: Expands the internal HNSW candidate pool by
                this percentage when a filter is active, compensating for
                filtered-out results. Default: None (uses server default of 0).
                Range: 0–100.
            **kwargs: Additional keyword arguments. Supports:
                - include_vectors: Whether to include vector data in results
                - sparse_indices: List of sparse vector indices for hybrid search
                - sparse_values: List of sparse vector values for hybrid search
                - log: Enable logging for debugging

        Returns:
            List of tuples of (Document, similarity_score).

        Example:
            ```python
            embedding = embeddings.embed_query("my query")
            results = vector_store.similarity_search_by_vector_with_score(
                embedding,
                k=3,
                filter=[{"status": {"$eq": "active"}}]
            )
            for doc, score in results:
                print(f"Score: {score:.4f}")
            ```
        """
        # Prepare query parameters
        query_params = {
            "vector": embedding,
            "top_k": k,
            "ef": ef,
            "include_vectors": kwargs.get("include_vectors", False),
        }

        if filter is not None:
            query_params["filter"] = filter

        if prefilter_cardinality_threshold is not None:
            query_params["prefilter_cardinality_threshold"] = prefilter_cardinality_threshold

        if filter_boost_percentage is not None:
            query_params["filter_boost_percentage"] = filter_boost_percentage

        # Include sparse vectors for hybrid search
        sparse_indices = kwargs.get("sparse_indices")
        sparse_values = kwargs.get("sparse_values")
        if sparse_indices is not None and sparse_values is not None:
            query_params["sparse_indices"] = sparse_indices
            query_params["sparse_values"] = sparse_values

        # Execute query
        results = self._index.query(**query_params)

        # Process results
        docs = []
        for res in results:
            meta = res.get("meta", {})
            
            # Extract content and metadata
            text = meta.get(self.content_payload_key, "")
            metadata = meta.get(self.metadata_payload_key, {})
            
            # Add additional fields to metadata
            metadata["_id"] = res.get("id")
            
            # Add filter fields if present
            if "filter" in res:
                metadata["_filter"] = res["filter"]
            
            score = res.get("similarity", 0.0)
            
            if not text:
                logger.warning(
                    f"Found document with no `{self.content_payload_key}` key. "
                    "Skipping."
                )
                continue
                
            docs.append((Document(page_content=text, metadata=metadata), score))

        return docs

    def delete(
        self,
        ids: list[str] | None = None,
        filter: list[dict[str, Any]] | None = None,  # noqa: A002
        **kwargs: Any,
    ) -> bool | None:
        """Delete documents by their ids or filter.

        Endee supports two deletion methods:
        1. Delete by specific vector IDs
        2. Delete by filter conditions

        Args:
            ids: List of ids to delete.
            filter: Filter conditions for deletion in Endee format.
                Example: [{"tags": {"$eq": "temporary"}}]
            **kwargs: Additional keyword arguments (currently unused).

        Returns:
            True if deletion is successful, False otherwise, None if no operation.

        Raises:
            ValueError: If neither ids nor filter is provided.

        Example:
            ```python
            # Delete by IDs
            vector_store.delete(ids=["id1", "id2", "id3"])

            # Delete by filter
            vector_store.delete(filter=[{"status": {"$eq": "expired"}}])
            ```
        """
        if ids is None and filter is None:
            msg = "Either ids or filter must be provided"
            raise ValueError(msg)

        try:
            if ids is not None:
                # Delete by IDs - one at a time for better error handling
                for doc_id in ids:
                    try:
                        self._index.delete_vector(doc_id)
                    except Exception as e:
                        logger.warning(f"Error deleting vector with ID {doc_id}: {e}")
                        return False
            elif filter is not None:
                # Delete by filter
                self._index.delete_with_filter(filter=filter)

            return True
        except Exception as e:
            logger.error(f"Error during deletion: {e}")
            return False

    def get_by_ids(self, ids: Sequence[str], /) -> list[Document]:
        """Get documents by their ids.

        Uses Endee's get_vector API to retrieve specific vectors by ID.

        Args:
            ids: Sequence of document ids to retrieve.

        Returns:
            List of Document objects.

        Example:
            ```python
            docs = vector_store.get_by_ids(["id1", "id2"])
            for doc in docs:
                print(doc.page_content)
            ```
        """
        docs = []
        
        for doc_id in ids:
            try:
                result = self._index.get_vector(doc_id)
                
                meta = result.get("meta", {})
                text = meta.get(self.content_payload_key, "")
                metadata = meta.get(self.metadata_payload_key, {})
                
                # Add additional fields
                metadata["_id"] = result.get("id")
                if "filter" in result:
                    metadata["_filter"] = result["filter"]
                
                if text:
                    docs.append(Document(page_content=text, metadata=metadata))
                else:
                    logger.warning(
                        f"Document {doc_id} has no `{self.content_payload_key}` key."
                    )
            except Exception as e:
                logger.warning(f"Error retrieving document {doc_id}: {e}")
                
        return docs
