"""CloudflareVectorize vector stores."""

# MARK: - Imports
from __future__ import annotations

import asyncio
import json
import time
import uuid
from functools import cached_property
from typing import (
    Any,
    Dict,
    Iterable,
    List,
    Optional,
    Sequence,
    Type,
    TypeVar,
    cast,
)

import requests
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.utils import from_env, secret_from_env
from langchain_core.vectorstores import VectorStore
from pydantic import SecretStr
from sqlalchemy import (
    Column,
    MetaData,
    Table,
    Text,
    create_engine,
    delete,
    insert,
    select,
)
from sqlalchemy import text as sql_text
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.engine import Engine
from typing_extensions import TypedDict

from ._errors import TokenErrors
from ._types import BindingQueryOptions, VectorizedDict

# MARK: - Constants
MAX_INSERT_SIZE = 5000
DEFAULT_WAIT_SECONDS = 5
DEFAULT_TOP_K = 20
MAX_TOP_K = 100
DEFAULT_DIMENSIONS = 1024
DEFAULT_METRIC = "cosine"

# Type variable for class methods that return CloudflareVectorize
VST = TypeVar("VST", bound="CloudflareVectorize")


# MARK: - RequestsKwargs
class RequestsKwargs(TypedDict, total=False):
    """TypedDict for requests kwargs."""

    timeout: int


# MARK: - VectorizeRecord
class VectorizeRecord:
    """Helper class to enforce Cloudflare Vectorize vector format.

    Attributes:
        id: Unique identifier for the vector
        text: The original text content
        values: The vector embedding values
        namespace: Optional namespace for the vector
        metadata: Optional metadata associated with the vector
    """

    def __init__(
        self,
        id: str,
        text: str,
        values: List[float],
        namespace: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ):
        """Initialize a VectorizeRecord.

        Args:
            id: Unique identifier for the vector
            text: The original text content
            values: The vector embedding values
            namespace: Optional namespace for the vector
            metadata: Optional metadata associated with the vector
        """
        self.id = id
        self.text = text
        self.values = values
        self.namespace = namespace
        self.metadata = metadata or {}

    def to_dict(self) -> VectorizedDict:
        """Convert to dictionary format for API requests."""
        vector_dict = VectorizedDict(id=self.id, values=self.values, text=self.text)

        if self.namespace:
            vector_dict["namespace"] = self.namespace

        if self.metadata:
            vector_dict["metadata"] = self.metadata

        return vector_dict


# MARK: - CloudflareVectorize
class CloudflareVectorize(VectorStore):
    """CloudflareVectorize vector store integration.

    Setup:
      Install ``langchain-cloudflare``.

      .. code-block:: bash

        pip install -qU langchain-cloudflare

    Key init args - indexing params:
        embedding: Embeddings
            Embeddings instance for converting texts to vectors
        index_name: str
            Optional name for the default Vectorize index

    Key init args - client params:
        account_id: str
            Cloudflare account ID. If not specified, will be read from
            the CF_ACCOUNT_ID environment variable.
        api_token: str
            Optional global API token for all Cloudflare services.
            If not specified, will be read from the
            CF_API_TOKEN environment variable.
        base_url: str
            Base URL for Cloudflare API (default: "https://api.cloudflare.com/client/v4")
        d1_database_id: str
            Optional D1 database ID for storing text data.
            If not specified, will be read from the
            CF_D1_DATABASE_ID environment variable.
        vectorize_api_token: str
            Optional API token for Vectorize service. If not specified,
            will be read from the CF_VECTORIZE_API_TOKEN environment variable.
        d1_api_token: str
            Optional API token for D1 database service. If not specified,
            will be read from the CF_D1_API_TOKEN environment variable.
        binding: Any
            Optional Vectorize binding (env.VECTORIZE) for use in Python Workers.
            When provided, uses the binding instead of REST API calls.

    See full list of supported init args and their descriptions in the params section.

    Usage in Python Workers:
        When running in a Cloudflare Python Worker, you can use the
        Vectorize binding directly for better performance:

        .. code-block:: python

            from workers import WorkerEntrypoint, Response
            from langchain_cloudflare import CloudflareVectorize
            from langchain_cloudflare.embeddings import CloudflareWorkersAIEmbeddings

            class Default(WorkerEntrypoint):
                async def fetch(self, request):
                    # Create embeddings using AI binding
                    embeddings = CloudflareWorkersAIEmbeddings(
                        model_name="@cf/baai/bge-large-en-v1.5",
                        binding=self.env.AI,
                    )

                    # Create vectorstore using Vectorize binding
                    vectorstore = CloudflareVectorize(
                        embedding=embeddings,
                        binding=self.env.VECTORIZE,  # Pass the Vectorize binding
                    )

                    # Perform similarity search
                    results = await vectorstore.asimilarity_search("query", k=5)
                    return Response.json({"results": [doc.page_content for doc in results]})

    Instantiate:
        .. code-block:: python

            from langchain_cloudflare.embeddings import (
                CloudflareWorkersAIEmbeddings,
            )
            from langchain_cloudflare.vectorstores import (
                CloudflareVectorize,
            )

            MODEL_WORKERSAI = "@cf/baai/bge-large-en-v1.5"

            # From environment variables
            embedder = CloudflareWorkersAIEmbeddings(model_name=MODEL_WORKERSAI)
            cfVect = CloudflareVectorize(embedding=embedder)

            # Or with explicit credentials
            embedder = CloudflareWorkersAIEmbeddings(
                account_id=cf_acct_id, api_token=cf_ai_token, model_name=MODEL_WORKERSAI
            )

            cfVect = CloudflareVectorize(
                embedding=embedder,
                account_id=cf_acct_id,
                d1_api_token=cf_d1_token,  # (Optional if using global token)
                vectorize_api_token=cf_vectorize_token,  # (Optional if using global token)
                d1_database_id=d1_database_id,  # (Optional if not using D1)
            )

    Add Documents:
        .. code-block:: python

            from langchain_core.documents import Document

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

            documents = [document_1, document_2, document_3]
            ids = ["1", "2", "3"]
            vectorize_index_name = f"test-langchain-cloudflare"

            cfVect.create_index(
                index_name=vectorize_index_name,
                wait=True
            )

            r = cfVect.add_documents(
                index_name=vectorize_index_name,
                documents=documents,
                ids=ids
            )

    Update Documents:
      .. code-block:: python

          updated_document = Document(
                id="3",
                page_content="This is an updated document!",
            )

            r = cfVect.add_documents(
                index_name=vectorize_index_name,
                documents=[updated_document],
                upsert=True
            )

    Delete Documents:
        .. code-block:: python

            r = cfVect.delete(
                index_name=vectorize_index_name,
                ids=["3"]
            )

    Search:
        .. code-block:: python

            results = cfVect.similarity_search(
                index_name=vectorize_index_name,
                query="foo",
                return_metadata="all",
                k=1
            )

    Search with score:
        .. code-block:: python

            results = cfVect.similarity_search_with_score(
                index_name=vectorize_index_name,
                query="foo",
                k=1,
                return_metadata="all"
            )

    Use as Retriever:
          .. code-block:: python

            retriever = cfVect.as_retriever(
                search_type="similarity",
                search_kwargs={"k": 1, "index_name": vectorize_index_name},
            )

            r = retriever.get_relevant_documents("foo")

    """  # noqa: E501

    def __init__(
        self,
        embedding: Embeddings,
        account_id: Optional[str] = None,
        api_token: Optional[str] = None,
        base_url: str = "https://api.cloudflare.com/client/v4",
        d1_database_id: Optional[str] = None,
        index_name: Optional[str] = None,
        binding: Any = None,
        d1_binding: Any = None,
        **kwargs: Any,
    ) -> None:
        """Initialize a CloudflareVectorize instance.

        Args:
            embedding: Embeddings instance for converting texts to vectors
            account_id: Cloudflare account ID. If not specified, will be read from
                the CF_ACCOUNT_ID environment variable.
            api_token: Optional global API token for all Cloudflare services.
                If not specified, will be read from the
                CF_API_TOKEN environment variable.
            base_url: Base URL for Cloudflare API (default: "https://api.cloudflare.com/client/v4")
            d1_database_id: Optional D1 database ID for storing text data.
                If not specified, will be read from the
                CF_D1_DATABASE_ID environment variable.
            index_name: Optional name for the default Vectorize index
            binding: Optional Vectorize binding (env.VECTORIZE) for use in
                Python Workers. When provided, uses the binding instead of
                REST API calls.
            d1_binding: Optional D1 database binding (env.D1) for use in
                Python Workers. When provided, uses the binding instead of
                REST API calls for D1 operations.
            **kwargs:
                Additional arguments including:
                - vectorize_api_token: Token for Vectorize service, if api_token not
                    global scoped. If not specified, will be read from the
                    CF_VECTORIZE_API_TOKEN environment variable.
                - d1_api_token: Token for D1 database service, if api_token not
                    global scoped. If not specified, will be read from the
                    CF_D1_API_TOKEN environment variable.
                - default_wait_seconds: # seconds to wait between mutation checks

        Raises:
            ValueError: If required API tokens are not provided (when not using binding)
        """
        self.embedding = embedding
        self.base_url = base_url
        self.d1_base_url = base_url
        self.index_name = index_name
        self.binding = binding
        self.d1_binding = d1_binding
        self.default_wait_seconds = kwargs.get(
            "default_wait_seconds", DEFAULT_WAIT_SECONDS
        )

        # If binding is provided, skip REST API setup
        if self.binding is not None:
            # When using binding, we don't need account_id or api_token
            self.account_id = account_id or ""
            self.d1_database_id = d1_database_id or ""
            self.api_token = None
            self.vectorize_api_token = None
            self.d1_api_token = None
            self._headers = {}
            self.d1_headers = {}
            return

        # Check environment variables if parameters not provided
        if account_id is None:
            account_id = from_env("CF_ACCOUNT_ID", default="")()
        self.account_id = account_id

        if d1_database_id is None:
            d1_database_id = from_env("CF_D1_DATABASE_ID", default="")()
        self.d1_database_id = d1_database_id

        # Use the provided API token or get from environment - convert to SecretStr
        self.api_token = (
            SecretStr(api_token)
            if api_token
            else secret_from_env("CF_API_TOKEN", default=None)()
        )

        # Extract and convert token kwargs to SecretStr
        vectorize_token = kwargs.get(
            "vectorize_api_token", from_env("CF_VECTORIZE_API_TOKEN", default=None)()
        )
        self.vectorize_api_token = (
            SecretStr(vectorize_token) if vectorize_token else None
        )

        d1_token = kwargs.get(
            "d1_api_token", from_env("CF_D1_API_TOKEN", default=None)()
        )
        self.d1_api_token = SecretStr(d1_token) if d1_token else None

        # Validate the account ID
        if not self.account_id:
            raise ValueError(TokenErrors.NO_ACCOUNT_ID_SET)

        # Set headers for Vectorize and D1 using get_secret_value() for the tokens
        self._headers = {
            "Authorization": (
                f"Bearer {self._get_token(token_name='vectorize_api_token')}"
            ),
            "Content-Type": "application/json",
        }
        self.d1_headers = {
            "Authorization": (f"Bearer {self._get_token(token_name='d1_api_token')}"),
            "Content-Type": "application/json",
        }

        has_global_token = (
            self.api_token is not None and self.api_token.get_secret_value()
        )

        if not has_global_token and not self.vectorize_api_token:
            raise ValueError(TokenErrors.INSUFFICIENT_VECTORIZE_TOKENS)

        if self.d1_database_id and not has_global_token and not self.d1_api_token:
            raise ValueError(TokenErrors.NO_GLOBAL_TOKEN_WITH_D1_TOKEN)

    # MARK: - Token Helper
    def _get_token(self, token_name: str) -> str:
        """Get the token value from the instance attribute or global API token.

        Args:
            token_name: The name of the token attribute to retrieve.

        Returns:
            The token value or empty string if not found.
        """
        if token := getattr(self, token_name):
            return token.get_secret_value()

        if self.api_token and self.api_token.get_secret_value():
            return self.api_token.get_secret_value()

        return ""

    @property
    def embeddings(self) -> Embeddings:
        """Get the embeddings model used by this vectorstore.

        Returns:
            Embeddings: The embeddings model instance
        """
        return self.embedding

    @cached_property
    def fully_qualified_base_url(self) -> str:
        """Get the fully qualified base URL for Cloudflare API requests."""
        return f"{self.base_url}/accounts/{self.account_id}"

    def _get_url(self, endpoint: str, index_name: str) -> str:
        """Get full URL for an API endpoint.

        Args:
            endpoint: The API endpoint path
            index_name: Name of the Vectorize index

        Returns:
            str: The complete URL for the API endpoint
        """
        return (
            f"{self.fully_qualified_base_url}/"
            f"vectorize/v2/indexes/{index_name}/{endpoint}"
        )

    def _get_base_url(self, endpoint: str) -> str:
        """Get base URL for index management endpoints.

        Args:
            endpoint: The API endpoint path

        Returns:
            str: The complete URL for the API endpoint
        """
        return f"{self.fully_qualified_base_url}/vectorize/v2/indexes{endpoint}"

    def _get_d1_url(self, endpoint: str) -> str:
        """Get full URL for a D1 API endpoint.

        Args:
            endpoint: The API endpoint path

        Returns:
            str: The complete URL for the API endpoint
        """
        return f"{self.d1_base_url}/accounts/{self.account_id}/d1/{endpoint}"

    def _get_d1_engine(self) -> Engine:
        """Get a SQLAlchemy engine for the D1 database.

        Returns:
            Engine: SQLAlchemy engine configured for Cloudflare D1

        Raises:
            ValueError: If D1 credentials are not configured
        """
        if not self.d1_database_id:
            raise ValueError("D1 database ID is required")

        # Get the API token for D1
        api_token = (
            self.d1_api_token.get_secret_value()
            if self.d1_api_token
            else self.api_token.get_secret_value()
            if self.api_token
            else None
        )
        if not api_token:
            raise ValueError("D1 API token is required")

        # Create connection string for sqlalchemy-cloudflare-d1
        # Format: cloudflare_d1://account_id:api_token@database_id
        connection_string = (
            f"cloudflare_d1://{self.account_id}:{api_token}@{self.d1_database_id}"
        )
        return create_engine(connection_string)

    def _get_d1_table(
        self, table_name: str, metadata: Optional[MetaData] = None
    ) -> Table:
        """Get a SQLAlchemy Table object for D1 document storage.

        Args:
            table_name: Name of the table
            metadata: Optional SQLAlchemy MetaData instance

        Returns:
            Table: SQLAlchemy Table object for the D1 table
        """
        if metadata is None:
            metadata = MetaData()

        return Table(
            table_name,
            metadata,
            Column("id", Text, primary_key=True, autoincrement=False),
            Column("text", Text),
            Column("namespace", Text),
            Column("metadata", Text),
        )

    def _get_d1_async_engine(self) -> Any:
        """Get an async SQLAlchemy engine for the D1 database.

        For Python Workers with d1_binding, creates a sync engine using
        create_engine_from_binding (which uses pyodide.ffi.run_sync internally).
        For REST API mode, uses the async dialect.

        Returns:
            Engine or AsyncEngine: SQLAlchemy engine configured for Cloudflare D1.
            For Workers with d1_binding, returns a sync Engine.
            For REST API mode, returns an AsyncEngine.

        Raises:
            ValueError: If D1 credentials are not configured
        """
        if self.d1_binding is not None:
            # For Workers, use create_engine_from_binding which handles
            # async-to-sync bridging via pyodide.ffi.run_sync()
            # This avoids the need for greenlet which isn't available in Pyodide
            from sqlalchemy_cloudflare_d1 import create_engine_from_binding

            return create_engine_from_binding(self.d1_binding)

        # For REST API mode, use async engine
        from sqlalchemy.ext.asyncio import create_async_engine

        if not self.d1_database_id:
            raise ValueError("D1 database ID is required")

        # Get the API token for D1
        api_token = (
            self.d1_api_token.get_secret_value()
            if self.d1_api_token
            else self.api_token.get_secret_value()
            if self.api_token
            else None
        )
        if not api_token:
            raise ValueError("D1 API token is required")

        # Create async connection string for sqlalchemy-cloudflare-d1
        # Format: cloudflare_d1+async://account_id:api_token@database_id
        connection_string = (
            f"cloudflare_d1+async://{self.account_id}:{api_token}@{self.d1_database_id}"
        )
        return create_async_engine(connection_string)

    def _is_sync_engine(self, engine: Any) -> bool:
        """Check if the engine is a sync engine (from create_engine_from_binding).

        Args:
            engine: SQLAlchemy engine to check

        Returns:
            bool: True if sync engine, False if async engine
        """
        # AsyncEngine has a 'sync_engine' attribute, regular Engine does not
        return not hasattr(engine, "sync_engine")

    @staticmethod
    def _validate_table_name(table_name: str) -> None:
        """Validate that table_name is provided and contains only safe characters.

        Table names must be alphanumeric with underscores only to prevent
        SQL injection attacks. This is defense-in-depth alongside SQLAlchemy's
        identifier quoting.

        Args:
            table_name: Name of the table

        Raises:
            ValueError: If table_name is empty, None, or contains unsafe characters
        """
        if not table_name:
            raise ValueError("table_name must be provided")
        if not table_name.replace("_", "").replace("-", "").isalnum():
            raise ValueError(
                f"Invalid table_name '{table_name}': "
                "table names must be alphanumeric with underscores or hyphens only"
            )

    @staticmethod
    def _validate_metadata_key(key: str) -> None:
        """Validate that a metadata key contains only safe characters.

        Args:
            key: The metadata key to validate

        Raises:
            ValueError: If key contains unsafe characters
        """
        if not key.replace("_", "").isalnum():
            raise ValueError(
                f"Invalid metadata key '{key}': "
                "keys must be alphanumeric with underscores only"
            )

    @staticmethod
    def _validate_operation(operation: str) -> None:
        """Validate that operation is AND or OR.

        Args:
            operation: The SQL operation to validate

        Raises:
            ValueError: If operation is not AND or OR
        """
        if operation not in ("AND", "OR"):
            raise ValueError("operation must be 'AND' or 'OR'")

    @staticmethod
    def _build_metadata_filter_query(
        table_name: str,
        metadata_filters: Dict[str, List[str]],
        operation: str,
    ) -> tuple:
        """Build SQL query and parameters for metadata filtering.

        Args:
            table_name: Name of the table to query
            metadata_filters: Dictionary of {key: [values]} to filter by
            operation: "AND" or "OR" to combine conditions

        Returns:
            Tuple of (sql_string, param_dict) for execution

        Raises:
            ValueError: If metadata keys contain unsafe characters
        """
        from sqlalchemy.engine import default

        dialect = default.DefaultDialect()
        quoted_table = dialect.identifier_preparer.quote_identifier(table_name)

        clauses: List[str] = []
        params: List[Any] = []

        for key, values in metadata_filters.items():
            if not values:
                continue

            # Validate key contains only safe characters
            CloudflareVectorize._validate_metadata_key(key)

            in_placeholders = ",".join(
                f":p{len(params) + i + 1}" for i in range(len(values))
            )

            param_key = f":p{len(params)}"
            clauses.append(
                f"""EXISTS (
                    SELECT 1
                    FROM json_each({quoted_table}.metadata, {param_key})
                    WHERE json_each.value IN ({in_placeholders})
                )"""
            )

            params.append(f"$.{key}")
            params.extend(values)

        where_clause = f" {operation} ".join(clauses)
        sql = f"SELECT * FROM {quoted_table} WHERE {where_clause}"
        param_dict = {f"p{i}": v for i, v in enumerate(params)}

        return sql, param_dict

    @staticmethod
    def _rows_to_dicts(rows: Sequence[Any]) -> List[Dict[str, Any]]:
        """Convert database rows to list of dictionaries.

        Args:
            rows: Sequence of row tuples from database

        Returns:
            List of dictionaries with id, text, namespace, metadata keys
        """
        return [
            {
                "id": row[0],
                "text": row[1],
                "namespace": row[2],
                "metadata": row[3],
            }
            for row in rows
        ]

    @staticmethod
    def _prepare_records_for_insert(data: List["VectorizeRecord"]) -> List[Dict]:
        """Convert VectorizeRecords to dicts for database insertion.

        Args:
            data: List of VectorizeRecord objects

        Returns:
            List of dictionaries ready for SQLAlchemy insert
        """
        records = []
        for record in data:
            record_dict = record.to_dict()
            records.append(
                {
                    "id": record_dict.get("id", ""),
                    "text": record_dict.get("text", ""),
                    "namespace": record_dict.get("namespace", ""),
                    "metadata": json.dumps(record_dict.get("metadata", {})),
                }
            )
        return records

    @staticmethod
    def _filter_request_kwargs(kwargs: Dict[str, Any]) -> RequestsKwargs:
        """Filter kwargs to only include those allowed for the specified HTTP client.

        Args:
            kwargs: Dictionary of keyword arguments

        Returns:
            Dictionary containing only the allowed kwargs for the specified client

        """
        allowed = ["timeout"]
        filtered_kwargs = {k: v for k, v in kwargs.items() if k in allowed}

        return cast(RequestsKwargs, filtered_kwargs)

    # MARK: - _prep_search_request
    @staticmethod
    def _prep_search_request(
        query_embedding: list[float],
        k: int = DEFAULT_TOP_K,
        md_filter: Optional[Dict] = None,
        namespace: Optional[str] = None,
        return_metadata: Optional[str] = "none",
        return_values: bool = False,
    ) -> Dict:
        """Prepare a search request for the Vectorize API.

        Args:
            query_embedding: Vector embedding for the search query
            k: Number of results to return (default: DEFAULT_TOP_K)
            md_filter: Optional metadata filter to apply to results
            namespace: Optional namespace to search within
            return_metadata: Controls metadata return:
                "none" (default), "indexed", or "all"
            return_values: Whether to return vector values (default: False)

        Returns:
            Dict: Formatted search request for the API

        Raises:
            ValueError: If index_name is not provided
        """
        top_k = DEFAULT_TOP_K if return_metadata == "all" or return_values else k
        top_k = min(top_k, MAX_TOP_K)

        # Prepare search request
        search_request = {"vector": query_embedding, "topK": top_k}

        if namespace:
            search_request["namespace"] = namespace

        # Add filter if provided
        if md_filter:
            search_request["filter"] = md_filter

        # Add metadata return preference
        if return_metadata:
            search_request["returnMetadata"] = return_metadata

        # Add vector values return preference
        if return_values:
            search_request["returnValues"] = return_values

        return search_request

    # MARK: - _combine_vectorize_and_d1_data
    @staticmethod
    def _combine_vectorize_and_d1_data(
        vector_data: List[Dict[str, Any]], d1_response: List[Dict[str, Any]]
    ) -> List[Document]:
        """Combine vector data from Vectorize API with text data from D1 database.

        Args:
            vector_data: List of vector data dictionaries from Vectorize API
            d1_response: Response from D1 database containing text data

        Returns:
            List[Document]: List of Documents with combined data from both sources

        Raises:
            ValueError: If index_name is not provided
        """
        # Create a lookup dictionary for D1 text data by ID
        id_to_text = {}
        for item in d1_response:
            if "id" in item and "text" in item:
                id_to_text[item["id"]] = item["text"]

        documents = []
        for vector in vector_data:
            # Create a Document with the complete vector data in metadata
            vector_id = vector.get("id")
            metadata = {}

            # Add metadata if returned
            if "metadata" in vector or "namespace" in vector or "values" in vector:
                if "metadata" in vector:
                    metadata.update(vector.get("metadata", {}))

                if "namespace" in vector:
                    metadata["_namespace"] = vector["namespace"]

                if "values" in vector:
                    metadata["_values"] = vector.get("values", [])

            # Get the text content from D1 results
            text_content = id_to_text.get(vector_id, "")

            # Add values if returned
            document_args = {
                "id": vector_id,
                "page_content": text_content,
                "metadata": metadata,
            }

            documents.append(Document(**document_args))

        return documents

    # MARK: - Binding Helper Methods

    async def _binding_insert(
        self, vectors: List[Dict[str, Any]], upsert: bool = False
    ) -> Dict[str, Any]:
        """Insert or upsert vectors using the Vectorize binding.

        Args:
            vectors: List of vector dicts with id, values, and optional metadata
            upsert: If True, use upsert (insert or update), otherwise insert only

        Returns:
            Dict with mutationId and count information
        """
        from .bindings import (
            convert_vectorize_mutation_response,
            convert_vectors_for_binding,
        )

        js_vectors = convert_vectors_for_binding(vectors)

        if upsert:
            response = await self.binding.upsert(js_vectors)
        else:
            response = await self.binding.insert(js_vectors)

        return convert_vectorize_mutation_response(response)

    async def _binding_query(
        self,
        vector: List[float],
        top_k: int = DEFAULT_TOP_K,
        filter_dict: Optional[Dict[str, Any]] = None,
        namespace: Optional[str] = None,
        return_metadata: str = "none",
        return_values: bool = False,
    ) -> Dict[str, Any]:
        """Query vectors using the Vectorize binding.

        Args:
            vector: The query vector
            top_k: Number of results to return
            filter_dict: Optional metadata filter
            namespace: Optional namespace to search within
            return_metadata: "none", "indexed", or "all"
            return_values: Whether to return vector values

        Returns:
            Dict with matches array
        """
        from .bindings import (
            convert_query_options_for_binding,
            convert_vectorize_query_response,
        )

        # Build options object
        options: BindingQueryOptions = {"topK": top_k}
        if filter_dict:
            options["filter"] = filter_dict
        if namespace:
            options["namespace"] = namespace
        if return_metadata and return_metadata != "none":
            options["returnMetadata"] = return_metadata
        if return_values:
            options["returnValues"] = return_values

        # Convert vector and options for JS
        from .bindings import convert_vectors_for_binding

        js_vector = convert_vectors_for_binding(vector)
        js_options = convert_query_options_for_binding(options)

        response = await self.binding.query(js_vector, js_options)

        return convert_vectorize_query_response(response)

    async def _binding_get_by_ids(self, ids: List[str]) -> List[Dict[str, Any]]:
        """Get vectors by IDs using the Vectorize binding.

        Args:
            ids: List of vector IDs to retrieve

        Returns:
            List of vector dicts with id, values, and metadata
        """
        from .bindings import (
            convert_vectorize_get_response,
            convert_vectors_for_binding,
        )

        js_ids = convert_vectors_for_binding(ids)
        response = await self.binding.getByIds(js_ids)

        return convert_vectorize_get_response(response)

    async def _binding_delete_by_ids(self, ids: List[str]) -> Dict[str, Any]:
        """Delete vectors by IDs using the Vectorize binding.

        Args:
            ids: List of vector IDs to delete

        Returns:
            Dict with mutationId and count information
        """
        from .bindings import (
            convert_vectorize_mutation_response,
            convert_vectors_for_binding,
        )

        js_ids = convert_vectors_for_binding(ids)
        response = await self.binding.deleteByIds(js_ids)

        return convert_vectorize_mutation_response(response)

    async def _binding_describe(self) -> Dict[str, Any]:
        """Get index information using the Vectorize binding.

        Returns:
            Dict with index configuration details
        """
        from .bindings import convert_vectorize_describe_response

        response = await self.binding.describe()

        return convert_vectorize_describe_response(response)

    # MARK: - _poll_mutation_status
    def _poll_mutation_status(
        self,
        index_name: str,
        mutation_id: Optional[str] = None,
        wait_seconds: Optional[int] = None,
    ) -> None:
        """Poll the mutation status of an index operation until completion.

        Args:
            index_name: Name of the Vectorize index
            mutation_id: Optional ID of the mutation to wait for
            wait_seconds:
                Optional number of seconds to wait
                per check interval for mutation status

        Raises:
            Exception: If polling encounters repeated errors
        """
        poll_interval_seconds = wait_seconds or self.default_wait_seconds
        time.sleep(poll_interval_seconds)
        err_cnt = 0
        err_lim = 5
        while True:
            try:
                response_index = self.get_index_info(index_name)
                err_cnt = 0
            except Exception as e:
                if err_cnt >= err_lim:
                    raise Exception("Index Mutation Error:", str(e))
                err_cnt += 1
                time.sleep(2**err_cnt)
                continue

            if mutation_id:
                index_mutation_id = response_index.get("processedUpToMutation")
                if index_mutation_id == mutation_id:
                    break
            else:
                return

            time.sleep(poll_interval_seconds)

    # MARK: - _apoll_mutation_status
    async def _apoll_mutation_status(
        self,
        index_name: str,
        mutation_id: Optional[str] = None,
        wait_seconds: Optional[int] = None,
    ) -> None:
        """Asynchronously poll the mutation status
        of an index operation until completion.

        Args:
            index_name: Name of the Vectorize index
            mutation_id: Optional ID of the mutation to wait for
            wait_seconds:
                Optional number of seconds to wait
                per check interval for mutation status

        Raises:
            Exception: If polling encounters repeated errors
        """
        poll_interval_seconds = wait_seconds or self.default_wait_seconds
        await asyncio.sleep(poll_interval_seconds)
        err_cnt = 0
        err_lim = 5
        while True:
            try:
                response_index = await self.aget_index_info(index_name)
                err_cnt = 0
            except Exception as e:
                if err_cnt >= err_lim:
                    raise Exception("Index Mutation Error:", str(e))
                err_cnt += 1
                await asyncio.sleep(2**err_cnt)
                continue

            if mutation_id:
                index_mutation_id = response_index.get("processedUpToMutation")
                if index_mutation_id == mutation_id:
                    break
            else:
                return

            await asyncio.sleep(poll_interval_seconds)

    # MARK: - d1_create_table
    def d1_create_table(self, table_name: str, **kwargs: Any) -> Dict[str, Any]:
        """Create a table in a D1 database using SQLAlchemy.

        Args:
            table_name: Name of the table to create
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        engine = self._get_d1_engine()
        metadata = MetaData()
        self._get_d1_table(table_name, metadata)
        metadata.create_all(engine)

        return {"success": True}

    # MARK: - ad1_create_table
    async def ad1_create_table(self, table_name: str, **kwargs: Any) -> Dict[str, Any]:
        """Asynchronously create a table in a D1 database.

        Uses the async SQLAlchemy engine with cloudflare_d1+async dialect.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to create
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        engine = self._get_d1_async_engine()
        metadata = MetaData()
        self._get_d1_table(table_name, metadata)

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            # The engine already handles async-to-sync via run_sync internally
            metadata.create_all(engine)
        else:
            # For REST API mode, use async patterns
            async with engine.begin() as conn:
                await conn.run_sync(metadata.create_all)
            await engine.dispose()

        return {"success": True}

    # MARK: - d1_drop_table
    def d1_drop_table(self, table_name: str, **kwargs: Any) -> Dict[str, Any]:
        """Delete a table from a D1 database using SQLAlchemy.

        Args:
            table_name: Name of the table to delete
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        engine = self._get_d1_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)
        table.drop(engine, checkfirst=True)

        return {"success": True}

    # MARK: - ad1_drop_table
    async def ad1_drop_table(self, table_name: str, **kwargs: Any) -> Dict[str, Any]:
        """Asynchronously delete a table from a D1 database.

        Uses the async SQLAlchemy engine with cloudflare_d1+async dialect.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to delete
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        engine = self._get_d1_async_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            table.drop(engine, checkfirst=True)
        else:
            # For REST API mode, use async patterns
            async with engine.begin() as conn:
                await conn.run_sync(
                    lambda sync_conn: table.drop(sync_conn, checkfirst=True)
                )
            await engine.dispose()

        return {"success": True}

    # MARK: - d1_upsert_texts
    def d1_upsert_texts(
        self,
        table_name: str,
        data: List[VectorizeRecord],
        upsert: bool = False,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Insert or update text data in a D1 database table using SQLAlchemy.

        Uses parameterized queries via SQLAlchemy to prevent SQL injection.

        Args:
            table_name: Name of the table to insert data into
            data: List of VectorizeRecord objects containing data to insert
            upsert:
                If true (default: False),
                insert or update the text data otherwise insert only.
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not data:
            return {"success": True, "changes": 0}

        engine = self._get_d1_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)
        records = self._prepare_records_for_insert(data)

        with engine.connect() as conn:
            if upsert:
                stmt = sqlite_insert(table).values(records)
                stmt = stmt.on_conflict_do_update(
                    index_elements=["id"],
                    set_={
                        "text": stmt.excluded.text,
                        "namespace": stmt.excluded.namespace,
                        "metadata": stmt.excluded.metadata,
                    },
                )
                conn.execute(stmt)
                conn.commit()
            else:
                conn.execute(insert(table), records)
                conn.commit()

        return {"success": True, "changes": len(records)}

    # MARK: - ad1_upsert_texts
    async def ad1_upsert_texts(
        self,
        table_name: str,
        data: List[VectorizeRecord],
        upsert: bool = False,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Asynchronously insert or update text data in a D1 database table.

        Uses the async SQLAlchemy engine with cloudflare_d1+async dialect.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to insert data into
            data: List of VectorizeRecord objects containing data to insert
            upsert:
                If true (default: False),
                insert or update the text data otherwise insert only.
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not data:
            return {"success": True, "changes": 0}

        engine = self._get_d1_async_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)
        records = self._prepare_records_for_insert(data)

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            with engine.begin() as conn:
                if upsert:
                    stmt = sqlite_insert(table).values(records)
                    stmt = stmt.on_conflict_do_update(
                        index_elements=["id"],
                        set_={
                            "text": stmt.excluded.text,
                            "namespace": stmt.excluded.namespace,
                            "metadata": stmt.excluded.metadata,
                        },
                    )
                    conn.execute(stmt)
                else:
                    conn.execute(insert(table), records)
        else:
            # For REST API mode, use async patterns
            async with engine.begin() as conn:
                if upsert:
                    stmt = sqlite_insert(table).values(records)
                    stmt = stmt.on_conflict_do_update(
                        index_elements=["id"],
                        set_={
                            "text": stmt.excluded.text,
                            "namespace": stmt.excluded.namespace,
                            "metadata": stmt.excluded.metadata,
                        },
                    )
                    await conn.execute(stmt)
                else:
                    await conn.execute(insert(table), records)
            await engine.dispose()

        return {"success": True, "changes": len(records)}

    # MARK: - d1_get_by_ids
    def d1_get_by_ids(self, table_name: str, ids: List[str], **kwargs: Any) -> List:
        """Retrieve text data from a D1 database table using SQLAlchemy.

        Args:
            table_name: Name of the table to retrieve data from
            ids: List of ids to retrieve
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            List of dictionaries with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not ids:
            return []

        engine = self._get_d1_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)

        with engine.connect() as conn:
            stmt = select(table).where(table.c.id.in_(ids))
            result = conn.execute(stmt)
            rows = result.fetchall()

        return self._rows_to_dicts(rows)

    # MARK: - ad1_get_by_ids
    async def ad1_get_by_ids(
        self, table_name: str, ids: List[str], **kwargs: Any
    ) -> List:
        """Asynchronously retrieve text data from a D1 database table.

        Uses the async SQLAlchemy engine with cloudflare_d1+async dialect.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to retrieve data from
            ids: List of ids to retrieve
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            List of dictionaries with query results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not ids:
            return []

        engine = self._get_d1_async_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            with engine.connect() as conn:
                stmt = select(table).where(table.c.id.in_(ids))
                result = conn.execute(stmt)
                rows = result.fetchall()
        else:
            # For REST API mode, use async patterns
            async with engine.connect() as conn:
                stmt = select(table).where(table.c.id.in_(ids))
                result = await conn.execute(stmt)
                rows = result.fetchall()
            await engine.dispose()

        return self._rows_to_dicts(rows)

    # MARK: - d1_metadata_query
    def d1_metadata_query(
        self,
        table_name: str,
        metadata_filters: Dict[str, List[str]],
        operation: Optional[str] = "AND",
        **kwargs: Any,
    ) -> List[Dict[str, Any]]:
        """Retrieve text data from a D1 database table with a metadata query.

        Uses SQLAlchemy with safe identifier handling and parameterized queries.

        Args:
            table_name: Name of the table to retrieve data from
            metadata_filters: Dictionary of filters to apply to the query
            operation: Operation to combine query conditions. "AND" (Default) or "OR"
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            List of dictionaries with query results

        Raises:
            ValueError: If table_name is not provided or operation is invalid
        """
        self._validate_table_name(table_name)

        if not metadata_filters:
            return []

        op = operation or "AND"
        self._validate_operation(op)

        sql, param_dict = self._build_metadata_filter_query(
            table_name, metadata_filters, op
        )

        engine = self._get_d1_engine()
        with engine.connect() as conn:
            result = conn.execute(sql_text(sql), param_dict)
            rows = result.fetchall()

        return self._rows_to_dicts(rows)

    # MARK: - ad1_metadata_query
    async def ad1_metadata_query(
        self,
        table_name: str,
        metadata_filters: Dict[str, List[str]],
        operation: Optional[str] = "AND",
        **kwargs: Any,
    ) -> List[Dict[str, Any]]:
        """Asynchronously retrieve text data from D1 with metadata query.

        Uses the async SQLAlchemy engine with safe identifier handling.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to retrieve data from
            metadata_filters: Dictionary of filters to apply to the query
            operation: Operation to combine query conditions. "AND" (Default) or "OR"
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            List of dictionaries with query results

        Raises:
            ValueError: If table_name is not provided or operation is invalid
        """
        self._validate_table_name(table_name)

        if not metadata_filters:
            return []

        op = operation or "AND"
        self._validate_operation(op)

        sql, param_dict = self._build_metadata_filter_query(
            table_name, metadata_filters, op
        )

        engine = self._get_d1_async_engine()

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            with engine.connect() as conn:
                result = conn.execute(sql_text(sql), param_dict)
                rows = result.fetchall()
        else:
            # For REST API mode, use async patterns
            async with engine.connect() as conn:
                result = await conn.execute(sql_text(sql), param_dict)
                rows = result.fetchall()
            await engine.dispose()

        return self._rows_to_dicts(rows)

    # MARK: - d1_delete
    def d1_delete(
        self,
        table_name: str,
        ids: List[str],
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Delete data from a D1 database table using SQLAlchemy.

        Args:
            table_name: Name of the table to delete from
            ids: List of ids to delete
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with deletion results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not ids:
            return {"success": True, "changes": 0}

        engine = self._get_d1_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)

        with engine.connect() as conn:
            stmt = delete(table).where(table.c.id.in_(ids))
            result = conn.execute(stmt)
            conn.commit()

        return {"success": True, "changes": result.rowcount}

    # MARK: - ad1_delete
    async def ad1_delete(
        self, table_name: str, ids: List[str], **kwargs: Any
    ) -> Dict[str, Any]:
        """Asynchronously delete data from a D1 database table.

        Uses the async SQLAlchemy engine with cloudflare_d1+async dialect.
        For Workers with d1_binding, uses sync engine with run_sync bridging.

        Args:
            table_name: Name of the table to delete from
            ids: List of ids to delete
            **kwargs: Additional keyword arguments (unused, kept for compatibility)

        Returns:
            Response data with deletion results

        Raises:
            ValueError: If table_name is not provided
        """
        self._validate_table_name(table_name)

        if not ids:
            return {"success": True, "changes": 0}

        engine = self._get_d1_async_engine()
        metadata = MetaData()
        table = self._get_d1_table(table_name, metadata)

        if self._is_sync_engine(engine):
            # For Workers with d1_binding, use sync patterns
            with engine.begin() as conn:
                stmt = delete(table).where(table.c.id.in_(ids))
                result = conn.execute(stmt)
            rowcount = result.rowcount
        else:
            # For REST API mode, use async patterns
            async with engine.begin() as conn:
                stmt = delete(table).where(table.c.id.in_(ids))
                result = await conn.execute(stmt)
            rowcount = result.rowcount
            await engine.dispose()

        return {"success": True, "changes": rowcount}

    # MARK: - add_texts
    def add_texts(
        self,
        texts: Iterable[str],
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> list[str]:
        """Add texts to the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/insert/
        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/upsert/

        Args:
            texts: Iterable of strings to add to the vectorstore.
            metadatas: Optional list of metadatas associated with the texts.
            ids: Optional list of ids to associate with the texts.
            namespaces: Optional list of namespaces for each vector.
            upsert:
                If True (default: False),
                uses the insert endpoint which will fail
                if vectors with the same IDs already exist.
                If False (default), uses upsert which will create or update vectors.
            index_name: Name of the Vectorize index.
            include_d1: Whether to include D1 database in insert/upsert (default: True)
            wait: If True (default: False), wait until vectors are ready.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Operation response with ids of added texts.

        Raises:
            ValueError: If the number of texts exceeds MAX_INSERT_SIZE.
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Convert texts to list if it's not already
        texts_list = list(texts)

        # Check if the number of texts exceeds the maximum allowed
        if len(texts_list) > MAX_INSERT_SIZE:
            raise ValueError(
                f"Number of texts ({len(texts_list)}) "
                f"exceeds maximum allowed ({MAX_INSERT_SIZE})"
            )

        # Generate embeddings for the texts
        embeddings = self.embedding.embed_documents(texts_list)

        # Generate IDs if not provided
        if ids is None or list(set(ids)) == [None]:
            ids = [str(uuid.uuid4()) for _ in texts_list]

        # Prepare vectors with metadata if provided
        vectors = []
        for i, (embedding, id, text) in enumerate(zip(embeddings, ids, texts_list)):
            # Get metadata if provided
            metadata = {}
            if metadatas is not None and i < len(metadatas):
                metadata = metadatas[i].copy()
                metadata.pop("_namespace", None)
                metadata.pop("_values", None)

            # Get namespace if provided
            namespace = None
            if namespaces is not None and i < len(namespaces):
                namespace = namespaces[i]

            # Create VectorizeRecord
            vector = VectorizeRecord(
                id=id,
                text=text,
                values=embedding,
                namespace=namespace,
                metadata=metadata,
            )

            vectors.append(vector)

        # Choose endpoint based on upsert parameter
        endpoint = "upsert" if upsert else "insert"

        # Convert vectors to newline-delimited JSON
        vector_dicts = [
            {
                "id": vector.id,
                "values": vector.values,
                "namespace": vector.namespace,
                "metadata": vector.metadata,
            }
            for vector in vectors
        ]
        ndjson_data = "\n".join(json.dumps(x) for x in vector_dicts)

        # Copy headers and set correct content type for NDJSON
        headers = self._headers.copy()
        headers["Content-Type"] = "application/x-ndjson"

        # Make API call to insert/upsert vectors
        filtered_kwargs = self._filter_request_kwargs(kwargs)
        response = requests.post(
            self._get_url(endpoint, index_name),
            headers=headers,
            data=ndjson_data.encode("utf-8"),
            **filtered_kwargs,
        )
        response.raise_for_status()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            self.d1_create_table(table_name=index_name, **kwargs)

            # add values to D1Database
            self.d1_upsert_texts(
                table_name=index_name, data=vectors, upsert=upsert, **kwargs
            )

        mutation_response = response.json()
        mutation_id = mutation_response.get("result", {}).get("mutationId")

        if wait and mutation_id:
            self._poll_mutation_status(
                index_name=index_name,
                mutation_id=mutation_id,
            )

        return ids

    # MARK: - aadd_texts
    async def aadd_texts(
        self,
        texts: Iterable[str],
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> list[str]:
        """Asynchronously add texts to the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/insert/
        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/upsert/


        Args:
            texts: Iterable of strings to add to the vectorstore.
            metadatas: Optional list of metadatas associated with the texts.
            ids: Optional list of ids to associate with the texts.
            namespaces: Optional list of namespaces for each vector.
            upsert:
                If True (default: False),
                uses the insert endpoint which will fail
                if vectors with the same IDs already exist.
                If False (default), uses upsert which will create or update vectors.
            index_name: Name of the Vectorize index.
            include_d1: Whether to include D1 database in insert/upsert (default: True)
            wait: If True (default: False), wait until vectors are ready.

        Returns:
            List of ids from adding the texts into the vectorstore.

        Raises:
            ValueError: If the number of texts exceeds MAX_INSERT_SIZE.
        """
        # Convert texts to list if it's not already
        texts_list = list(texts)

        # When using binding, index_name is optional (configured in wrangler.jsonc)
        # For REST API, it's required
        index_name = index_name or self.index_name
        if not index_name and self.binding is None:
            raise ValueError("index_name must be provided")

        # Check if the number of texts exceeds the maximum allowed
        if len(texts_list) > MAX_INSERT_SIZE:
            raise ValueError(
                f"Number of texts ({len(texts_list)}) "
                f"exceeds maximum allowed ({MAX_INSERT_SIZE})"
            )

        # Generate embeddings for the texts
        embeddings = await self.embedding.aembed_documents(texts_list)

        # Generate IDs if not provided
        if ids is None or list(set(ids)) == [None]:
            ids = [str(uuid.uuid4()) for _ in texts_list]

        # Prepare vectors with metadata if provided
        vectors = []
        for i, (embedding, id, text) in enumerate(zip(embeddings, ids, texts_list)):
            # Get metadata if provided
            metadata = {}
            if metadatas is not None and i < len(metadatas):
                metadata = metadatas[i].copy()

            # Get namespace if provided
            namespace = None
            if namespaces is not None and i < len(namespaces):
                namespace = namespaces[i]

            # Create VectorizeRecord
            vector = VectorizeRecord(
                id=id,
                text=text,
                values=embedding,
                namespace=namespace,
                metadata=metadata,
            )

            vectors.append(vector)

        # Convert vectors to dict format for both binding and REST API
        vector_dicts = [
            {
                "id": v.id,
                "values": v.values,
                "namespace": v.namespace,
                "metadata": v.metadata,
            }
            for v in vectors
        ]

        # Use binding if available (for Python Workers)
        if self.binding is not None:
            mutation_response = await self._binding_insert(vector_dicts, upsert=upsert)
            mutation_id = mutation_response.get("mutationId")
        else:
            # Choose endpoint based on upsert parameter
            endpoint = "upsert" if upsert else "insert"

            # Convert vectors to newline-delimited JSON
            ndjson_data = "\n".join(json.dumps(vd) for vd in vector_dicts)

            # Import httpx here to avoid dependency issues
            import httpx

            # Copy headers and set correct content type for NDJSON
            headers = self._headers.copy()
            headers["Content-Type"] = "application/x-ndjson"

            # Make API call to insert/upsert vectors
            # index_name is guaranteed to be str here (validated above for REST API)
            assert index_name is not None
            async with httpx.AsyncClient() as client:
                response = await client.post(
                    self._get_url(endpoint, index_name),
                    headers=headers,
                    content=ndjson_data.encode("utf-8"),
                    **kwargs,
                )
                response.raise_for_status()

            mutation_response = response.json()
            mutation_id = mutation_response.get("result", {}).get("mutationId")

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # Use index_name for D1 table name (empty string fallback for binding mode)
            table_name = index_name or ""
            # create D1 table if not exists
            await self.ad1_create_table(table_name=table_name)

            # add values to D1Database
            await self.ad1_upsert_texts(
                table_name=table_name,
                data=vectors,
            )

        if wait and mutation_id and index_name:
            await self._apoll_mutation_status(
                index_name=index_name,
                mutation_id=mutation_id,
            )

        return ids

    # MARK: - similarity_search
    def similarity_search(
        self,
        query: str,
        k: int = DEFAULT_TOP_K,
        return_metadata: str = "all",
        index_name: Optional[str] = None,
        md_filter: Optional[Dict[str, Any]] = None,
        namespace: Optional[str] = None,
        return_values: bool = False,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> List[Document]:
        """Search for similar documents to a query string.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/query/

        Args:
            query: Query string to search for
            k: Number of results to return (default: DEFAULT_TOP_K)
            return_metadata:
                Controls metadata return:
                "none", "indexed", or "all" (default: "all")
            index_name: Name of the Vectorize index (optional)
            md_filter: Optional metadata filter to apply to results
            namespace: Optional namespace to search within
            return_values: Whether to return vector embeddings (default: False)
            include_d1: Whether to include D1 database lookups (default: True)
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            List[Document]: The matching documents

            Each Document has:
            - Empty page_content (as Vectorize doesn't store text)
            - metadata containing the complete vector data:
              - id: Identifier for the vector
              - metadata: Any metadata associated with the vector (if requested)
              - page_content: The text content of the document
              - namespace: The namespace the vector belongs to
              - score: The similarity score
              - values: The vector embeddings (if requested)

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        docs_and_scores = self.similarity_search_with_score(
            query=query,
            k=k,
            md_filter=md_filter,
            namespace=namespace,
            index_name=index_name,
            return_metadata=return_metadata,
            return_values=return_values,
            include_d1=include_d1,
            **kwargs,
        )

        return [doc for doc, _ in docs_and_scores]

    # MARK: - similarity_search_with_score
    def similarity_search_with_score(
        self,
        query: str,
        k: int = DEFAULT_TOP_K,
        return_metadata: str = "all",
        return_values: bool = False,
        include_d1: bool = True,
        index_name: Optional[str] = None,
        md_filter: Optional[Dict[str, Any]] = None,
        namespace: Optional[str] = None,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]:
        """Search for similar vectors to a query string and return with scores.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/query/

        Args:
            query: Query string to search for
            k: Number of results to return (default: DEFAULT_TOP_K)
            return_metadata:
                Controls metadata return:
                "none", "indexed", or "all" (default: "all")
            return_values: Whether to return vector embeddings (default: False)
            include_d1: Whether to include D1 database lookups (default: True)
            index_name: Name of the Vectorize index (optional)
            md_filter: Optional metadata filter to apply to results
            namespace: Optional namespace to search within
            **kwargs: Additional keyword arguments to pass to the requests call

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

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Generate embedding for the query
        query_embedding = self.embedding.embed_query(query)

        search_request = self._prep_search_request(
            query_embedding=query_embedding,
            k=k,
            md_filter=md_filter,
            namespace=namespace,
            return_metadata=return_metadata,
            return_values=return_values,
        )

        # Make API call to query vectors
        filtered_kwargs = self._filter_request_kwargs(kwargs)
        response = requests.post(
            self._get_url("query", index_name),
            headers=self._headers,
            json=search_request,
            **filtered_kwargs,
        )
        response.raise_for_status()
        results = response.json().get("result", {}).get("matches", [])

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # query D1 for raw results
            ids = [x.get("id") for x in results]

            d1_results_records = self.d1_get_by_ids(
                table_name=index_name, ids=ids, **kwargs
            )
        else:
            d1_results_records = []

        # Use _combine_vectorize_and_d1_data to create documents
        documents = self._combine_vectorize_and_d1_data(
            vector_data=results, d1_response=d1_results_records
        )

        # Extract scores from results
        scores = [result.get("score", 0.0) for result in results]

        # Return as list of (Document, score) tuples
        return list(zip(documents, scores))

    # MARK: - asimilarity_search
    async def asimilarity_search(
        self,
        query: str,
        k: int = DEFAULT_TOP_K,
        index_name: Optional[str] = None,
        md_filter: Optional[Dict[str, Any]] = None,
        namespace: Optional[str] = None,
        return_metadata: str = "none",
        return_values: bool = False,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> List[Document]:
        """Asynchronously search for similar documents to a query string.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/query/

        Args:
            query: Query string to search for
            k: Number of results to return (default: DEFAULT_TOP_K)
            return_metadata:
                Controls metadata return:
                "none", "indexed", or "all" (default: "all")
            return_values: Whether to return vector embeddings (default: False)
            include_d1: Whether to include D1 database lookups (default: True)
            index_name: Name of the Vectorize index (optional)
            md_filter: Optional metadata filter to apply to results
            namespace: Optional namespace to search within
            **kwargs: Additional arguments to pass to the API call

        Returns:
            List of Documents most similar to the query.

            Each Document has:
            - Empty page_content (as Vectorize doesn't store text)
            - metadata containing the complete vector data:
              - id: Identifier for the vector
              - metadata: Any metadata associated with the vector (if requested)
              - page_content: The text content of the document
              - namespace: The namespace the vector belongs to
              - score: The similarity score
              - values: The vector embeddings (if requested)

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        docs_and_scores = await self.asimilarity_search_with_score(
            query=query,
            k=k,
            md_filter=md_filter,
            namespace=namespace,
            index_name=index_name,
            return_metadata=return_metadata,
            return_values=return_values,
            include_d1=include_d1,
            **kwargs,
        )
        return [doc for doc, _ in docs_and_scores]

    # MARK: - asimilarity_search_with_score
    async def asimilarity_search_with_score(
        self,
        query: str,
        k: int = DEFAULT_TOP_K,
        index_name: Optional[str] = None,
        md_filter: Optional[Dict[str, Any]] = None,
        namespace: Optional[str] = None,
        return_metadata: str = "none",
        return_values: bool = False,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> list[tuple[Document, float]]:
        """Asynchronously search for similar vectors to a query string
            and return with scores.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/query/

        Args:
            query: Query string to search for.
            k: Number of results to return.
            md_filter: Optional metadata filter expression to limit results.
            namespace: Optional namespace to search in.
            index_name: Name of the Vectorize index.
            return_metadata:
                Controls metadata return:
                "none" (default), "indexed", or "all".
            return_values: Whether to return vector embeddings (default: False).
            include_d1: Whether to include D1 database lookups (default: True)
            **kwargs: Additional keyword arguments to pass to the httpx client

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

        Raises:
            ValueError: If index_name is not provided
        """
        # When using binding, index_name is optional (configured in wrangler.jsonc)
        # For REST API, it's required
        index_name = index_name or self.index_name
        if not index_name and self.binding is None:
            raise ValueError("index_name must be provided")

        # Generate embedding for the query
        query_embedding = await self.embedding.aembed_query(query)

        # Use binding if available (for Python Workers)
        if self.binding is not None:
            response_data = await self._binding_query(
                vector=query_embedding,
                top_k=k,
                filter_dict=md_filter,
                namespace=namespace,
                return_metadata=return_metadata,
                return_values=return_values,
            )
            results = response_data.get("matches", [])
        else:
            search_request = self._prep_search_request(
                query_embedding=query_embedding,
                k=k,
                md_filter=md_filter,
                namespace=namespace,
                return_metadata=return_metadata,
                return_values=return_values,
            )

            # Import httpx here to avoid dependency issues
            import httpx

            # Make API call to query vectors
            # index_name is guaranteed to be str here (validated above for REST API)
            assert index_name is not None
            filtered_kwargs = self._filter_request_kwargs(kwargs)
            async with httpx.AsyncClient() as client:
                response = await client.post(
                    self._get_url("query", index_name),
                    headers=self._headers,
                    json=search_request,
                    **filtered_kwargs,
                )
                response.raise_for_status()
                response_data = response.json()

            results = response_data.get("result", {}).get("matches", [])

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # query D1 for raw results
            ids = [x.get("id") for x in results]
            table_name = index_name or ""

            d1_results_records = await self.ad1_get_by_ids(
                table_name=table_name, ids=ids, **kwargs
            )
        else:
            d1_results_records = []

        # Use _combine_vectorize_and_d1_data to create documents
        documents = self._combine_vectorize_and_d1_data(results, d1_results_records)

        # Extract scores from results
        scores = [result.get("score", 0.0) for result in results]

        # Return as list of (Document, score) tuples
        return list(zip(documents, scores))

    # MARK: - delete
    def delete(
        self,
        ids: Optional[list[str]] = None,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> Optional[bool]:
        """Delete vectors by ID from the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/delete_by_ids/

        Args:
            ids: List of ids to delete. If None, deletes all vectors.
            index_name: Name of the Vectorize index.
            include_d1: Whether to include D1 database lookups (default: True)
            wait:
                Wait until mutation_id shows that the
                data has been deleted (default: False).
            **kwargs: Additional keyword arguments to pass to the requests call.

        Returns:
            True if deletion is successful, False otherwise.

        Raises:
            ValueError: If index_name is not provided
        """
        if ids is None:
            # Cloudflare Vectorize doesn't have a delete all endpoint
            raise ValueError("Deleting all vectors is not supported")

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        delete_request = {"ids": [str(x) if type(x) is not str else x for x in ids]}

        response = requests.post(
            self._get_url("delete_by_ids", index_name),
            headers=self._headers,
            json=delete_request,
            **kwargs,
        )
        response.raise_for_status()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            self.d1_delete(table_name=index_name, ids=ids)

        mutation_response = response.json()
        mutation_id = mutation_response.get("result", {}).get("mutationId")

        if wait and mutation_id:
            self._poll_mutation_status(
                index_name=index_name,
                mutation_id=mutation_id,
            )

        return True

    # MARK: - adelete
    async def adelete(
        self,
        ids: Optional[list[str]] = None,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> Optional[bool]:
        """Asynchronously delete vectors by ID from the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/delete_by_ids/

        Args:
            ids: List of ids to delete. If None, deletes all vectors.
            index_name:
                Name of the Vectorize index
                (Optional if passed in class instantiation).
            include_d1: Whether to include D1 database lookups (default: True)
            wait:
                Wait until mutation_id shows that
                the data has been deleted (default: False).

        Returns:
            True if deletion is successful, False otherwise.

        Raises:
            ValueError: If index_name is not provided
        """
        if ids is None:
            # Cloudflare Vectorize doesn't have a delete all endpoint
            raise ValueError("Deleting all vectors is not supported")

        ids_list = [str(x) if type(x) is not str else x for x in ids]

        # Use binding if available (for Python Workers)
        # Binding doesn't need index_name - it's already configured in wrangler.jsonc
        if self.binding is not None:
            mutation_response = await self._binding_delete_by_ids(ids_list)
            mutation_id = mutation_response.get("mutationId")
            index_name = self.index_name  # For D1 operations if needed
        else:
            index_name = index_name or self.index_name

            if not index_name:
                raise ValueError("index_name must be provided")
            delete_request = {"ids": ids_list}

            # Make API call to delete vectors
            import httpx

            async with httpx.AsyncClient() as client:
                response = await client.post(
                    self._get_url("delete_by_ids", index_name),
                    headers=self._headers,
                    json=delete_request,
                )
            response.raise_for_status()

            mutation_response = response.json()
            mutation_id = mutation_response.get("result", {}).get("mutationId")

        if include_d1 and (self.d1_database_id or self.d1_binding):
            table_name = index_name or ""
            await self.ad1_delete(table_name=table_name, ids=ids_list, **kwargs)

        if wait and mutation_id and index_name:
            await self._apoll_mutation_status(
                index_name=index_name,
                mutation_id=mutation_id,
            )

        return True

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

        The returned documents are expected to have the ID field set to the ID of the
        document in the vector store.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/get_by_ids/

        Args:
            ids: List of ids to retrieve.

        Returns:
            List of Documents.

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = self.index_name

        if not index_name:
            raise ValueError("index_name must be provided in the constructor")

        get_request = {"ids": [str(x) if type(x) is not str else x for x in ids]}

        # Get vector data from Vectorize API
        response = requests.post(
            self._get_url("get_by_ids", index_name),
            headers=self._headers,
            json=get_request,
        )
        response.raise_for_status()

        vector_data = response.json().get("result", {})

        if self.d1_database_id:
            # Get text data from D1 database
            d1_response = self.d1_get_by_ids(table_name=index_name, ids=list(ids))
        else:
            d1_response = []

        # Combine data into LangChain Document objects
        documents = self._combine_vectorize_and_d1_data(vector_data, d1_response)

        return documents

    # MARK: - aget_by_ids
    async def aget_by_ids(self, ids: Sequence[str], /) -> list[Document]:
        """Async get documents by their IDs.

        The returned documents are expected to have the ID field set to the ID of the
        document in the vector store.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/get_by_ids/

        Args:
            ids: List of ids to retrieve.

        Returns:
            List of Documents.

        Raises:
            ValueError: If index_name is not provided
        """
        ids_list = [str(x) if type(x) is not str else x for x in ids]

        # Use binding if available (for Python Workers)
        # Binding doesn't need index_name - it's already configured in wrangler.jsonc
        if self.binding is not None:
            vector_data = await self._binding_get_by_ids(ids_list)
            index_name = self.index_name  # For D1 operations if needed
        else:
            index_name = self.index_name

            if not index_name:
                raise ValueError("index_name must be provided")
            get_request = {"ids": ids_list}

            # Get vector data from Vectorize API
            import httpx

            async with httpx.AsyncClient() as client:
                response = await client.post(
                    self._get_url("get_by_ids", index_name),
                    headers=self._headers,
                    json=get_request,
                )
            response.raise_for_status()

            vector_data = response.json().get("result", {})

        if self.d1_database_id:
            table_name = index_name or ""
            d1_response = await self.ad1_get_by_ids(table_name=table_name, ids=ids_list)
        else:
            d1_response = []

        documents = self._combine_vectorize_and_d1_data(vector_data, d1_response)

        return documents

    # MARK: - get_index_info
    def get_index_info(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Get information about the current index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/info/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Dictionary containing index information.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        response = requests.get(
            self._get_url("info", index_name), headers=self._headers, **kwargs
        )
        response.raise_for_status()

        return response.json().get("result", {})

    # MARK: - aget_index_info
    async def aget_index_info(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Asynchronously get information about the current index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/info/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the httpx call

        Returns:
            Dictionary containing index information.

        Raises:
            ValueError: If index_name is not provided
        """

        # Use binding if available (for Python Workers)
        # Binding doesn't need index_name - it's already configured in wrangler.jsonc
        if self.binding is not None:
            return await self._binding_describe()

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.get(
                self._get_url("info", index_name), headers=self._headers, **kwargs
            )
            response.raise_for_status()
            response_data = response.json()

        return response_data.get("result", {})

    # MARK: - create_metadata_index
    def create_metadata_index(
        self,
        property_name: str,
        index_type: str = "string",
        index_name: Optional[str] = None,
        wait: bool = False,
        **kwargs: Any,
    ) -> Dict:
        """Create a metadata index for a specific property.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/create/

        Args:
            property_name: The metadata property to index.
            index_type: The type of index to create (default: "string").
            index_name: Name of the Vectorize index.
            wait:
                Wait until mutation_id shows that
                the index is created (default: False).

        Returns:
            Response data with mutation ID.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        response = requests.post(
            f"{self._get_url('metadata_index/create', index_name)}",
            headers=self._headers,
            json={"propertyName": property_name, "indexType": index_type},
            **kwargs,
        )
        response.raise_for_status()
        mutation_response = response.json()
        mutation_id = mutation_response.get("result", {}).get("mutationId")

        if wait:
            self._poll_mutation_status(index_name=index_name, mutation_id=mutation_id)

        return response.json().get("result", {})

    # MARK: - acreate_metadata_index
    async def acreate_metadata_index(
        self,
        property_name: str,
        index_type: str = "string",
        index_name: Optional[str] = None,
        wait: bool = False,
        **kwargs: Any,
    ) -> Dict:
        """Asynchronously create a metadata index for a specific property.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/create/

        Args:
            property_name: The metadata property to index.
            index_type: The type of index to create (default: "string").
            index_name: Name of the Vectorize index.
            wait: Wait until the index is created (default: False).

        Returns:
            Response data with mutation ID.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self._get_url('metadata_index/create', index_name)}",
                headers=self._headers,
                json={"propertyName": property_name, "indexType": index_type},
                **kwargs,
            )
            response.raise_for_status()

            mutation_response = response.json()
            mutation_id = mutation_response.get("result", {}).get("mutationId")

        if wait:
            await self._apoll_mutation_status(
                index_name=index_name, mutation_id=mutation_id
            )

        return response.json().get("result", {})

    # MARK: - list_metadata_indexes
    def list_metadata_indexes(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> List[Dict[str, str]]:
        """List all metadata indexes for the current index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/list/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            List of metadata indexes with their property names and index types.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        response = requests.get(
            f"{self._get_url('metadata_index/list', index_name)}",
            headers=self._headers,
            **kwargs,
        )
        response.raise_for_status()

        return response.json().get("result", {}).get("metadataIndexes", [])

    # MARK: - alist_metadata_indexes
    async def alist_metadata_indexes(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> List[Dict[str, str]]:
        """Asynchronously list all metadata indexes for the current index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/list/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            List of metadata indexes with their property names and index types.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self._get_url('metadata_index/list', index_name)}",
                headers=self._headers,
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        return response_data.get("result", {}).get("metadataIndexes", [])

    # MARK: - delete_metadata_index
    def delete_metadata_index(
        self, property_name: str, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Delete a metadata index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/delete/

        Args:
            property_name: The metadata property index to delete.
            index_name: Name of the Vectorize index.

        Returns:
            Response data with mutation ID.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        response = requests.post(
            f"{self._get_url('metadata_index/delete', index_name)}",
            headers=self._headers,
            json={"property": property_name},
            **kwargs,
        )
        response.raise_for_status()
        return response.json().get("result", {})

    # MARK: - adelete_metadata_index
    async def adelete_metadata_index(
        self, property_name: str, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Asynchronously delete a metadata index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/subresources/metadata_index/methods/delete/

        Args:
            property_name: The metadata property to remove indexing for.
            index_name: Name of the Vectorize index.

        Returns:
            Response data with mutation ID.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self._get_url('metadata_index/delete', index_name)}",
                headers=self._headers,
                json={"propertyName": property_name},
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        return response_data.get("result", {})

    # MARK: - get_index
    def get_index(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Get information about the current Vectorize index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/get/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Dictionary containing index configuration and details.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        response = requests.get(
            f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes/{index_name}",
            headers=self._headers,
            **kwargs,
        )
        response.raise_for_status()

        return response.json().get("result", {})

    # MARK: - aget_index
    async def aget_index(
        self, index_name: Optional[str] = None, **kwargs: Any
    ) -> Dict[str, Any]:
        """Asynchronously get information about the current Vectorize index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/get/

        Args:
            index_name: Name of the Vectorize index.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Dictionary containing index information.

        Raises:
            ValueError: If index_name is not provided
        """

        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes/{index_name}",
                headers=self._headers,
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        return response_data.get("result", {})

    # MARK: - create_index
    def create_index(
        self,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        index_name: Optional[str] = None,
        description: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Create a new Vectorize index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            dimensions: Number of dimensions for the vector embeddings
            metric: Distance metric to use (e.g., "cosine", "euclidean")
            index_name: Name for the new index
            description: Optional description for the index
            include_d1: Whether to create a D1 table for the index
            wait: Whether to wait for the index to be created
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Response data from the API

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Use provided token or get class level token
        token = self.vectorize_api_token or self.api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        data = {
            "name": index_name,
            "config": {
                "dimensions": dimensions,
                "metric": metric,
            },
        }

        if description:
            data["description"] = description

        # Check if index already exists - but handle various error states
        try:
            r = self.get_index(index_name, **kwargs)
            if r:
                raise ValueError(f"Index {index_name} already exists")
        except requests.exceptions.HTTPError as e:
            # If 404 Not Found or 410 Gone, we can create the index
            if e.response.status_code in [404, 410]:
                pass  # Index doesn't exist or was removed, so we can create it
            else:
                # Re-raise for other HTTP errors
                raise

        # Create the index
        response = requests.post(
            f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes",
            headers=headers,
            json=data,
            **kwargs,
        )
        response.raise_for_status()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # Create D1 table if not exists
            self.d1_create_table(table_name=index_name, **kwargs)

        if wait:
            self._poll_mutation_status(index_name=index_name)

        return response.json().get("result", {})

    # MARK: - acreate_index
    async def acreate_index(
        self,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        index_name: Optional[str] = None,
        description: Optional[str] = None,
        include_d1: bool = True,
        wait: bool = False,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Asyncronously Create a new Vectorize index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            dimensions: Number of dimensions for the vector embeddings
            metric: Distance metric to use (e.g., "cosine", "euclidean")
            index_name: Name for the new index
            description: Optional description for the index
            include_d1: Whether to create a D1 table for the index
            wait: Whether to wait for the index to be created
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Response data from the API

        Raises:
            ValueError: If index_name is not provided
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Use provided token or get class level token
        token = self.vectorize_api_token or self.api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        data = {
            "name": index_name,
            "config": {
                "dimensions": dimensions,
                "metric": metric,
            },
        }

        if description:
            data["description"] = description

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes",
                headers=headers,
                json=data,
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # create D1 table if not exists
            await self.ad1_create_table(table_name=index_name, **kwargs)

        if wait:
            await self._apoll_mutation_status(index_name=index_name)

        return response_data.get("result", {})

    # MARK: - list_indexes
    def list_indexes(self, **kwargs: Any) -> List[Dict[str, Any]]:
        """List all Vectorize indexes for the account.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/list/

        Args:
            **kwargs: Additional arguments to pass to the API request

        Returns:
            List[Dict[str, Any]]: List of index configurations and details

        Raises:
            ValueError: If index_name is not provided
        """
        # Use provided token or get class level token
        token = self.api_token or self.vectorize_api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        response = requests.get(
            f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes",
            headers=headers,
            **kwargs,
        )
        response.raise_for_status()

        return response.json().get("result", [])

    # MARK: - alist_indexes
    async def alist_indexes(self, **kwargs: Any) -> List[Dict[str, Any]]:
        """Asynchronously list all Vectorize indexes for the account.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/list/

        Args:
            **kwargs: Additional arguments to pass to the httpx request

        Returns:
            List[Dict[str, Any]]: List of index configurations and details

        Raises:
            ValueError: If index_name is not provided
        """
        # Use provided token or get class level token
        token = self.vectorize_api_token or self.api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes",
                headers=headers,
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        return response_data.get("result", [])

    # MARK: - delete_index
    def delete_index(
        self,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Delete a Vectorize index and optionally its associated D1 table.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/delete/

        Args:
            index_name:
                Name of the index to delete (uses instance default if not provided)
            include_d1: Whether to also delete the associated D1 table (default: True)
            **kwargs: Additional arguments to pass to the requests call

        Returns:
            Dict[str, Any]: Response data from the deletion operation

        Raises:
            ValueError: If index_name is not provided and no default exists
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Use provided token or get class level token
        token = self.vectorize_api_token or self.api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        response = requests.delete(
            f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes/{index_name}",
            headers=headers,
            **kwargs,
        )
        response.raise_for_status()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            # delete D1 table if exists
            self.d1_drop_table(table_name=index_name)

        return response.json().get("result", {})

    # MARK: - adelete_index
    async def adelete_index(
        self,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Asynchronously delete a Vectorize index.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/delete/

        Args:
            index_name:
                Name of the index to delete (uses instance default if not provided)
            include_d1: Whether to also delete the associated D1 table (default: True)
            **kwargs: Additional arguments to pass to the httpx call

        Returns:
            Dict[str, Any]: Response data from the deletion operation

        Raises:
            ValueError: If index_name is not provided and no default exists
        """
        index_name = index_name or self.index_name

        if not index_name:
            raise ValueError("index_name must be provided")

        # Use provided token or get class level token
        token = self.vectorize_api_token or self.api_token
        token_value = token.get_secret_value() if token else ""

        headers = {
            "Authorization": f"Bearer {token_value}",
            "Content-Type": "application/json",
        }

        # Import httpx here to avoid dependency issues
        import httpx

        async with httpx.AsyncClient() as client:
            response = await client.delete(
                f"{self.base_url}/accounts/{self.account_id}/vectorize/v2/indexes/{index_name}",
                headers=headers,
                **kwargs,
            )
            response.raise_for_status()

            response_data = response.json()

        if include_d1 and (self.d1_database_id or self.d1_binding):
            await self.ad1_drop_table(table_name=index_name)

        return response_data.get("result", {})

    # MARK: - from_texts
    @classmethod
    def from_texts(
        cls: Type[VST],
        texts: List[str],
        embedding: Embeddings,
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        account_id: Optional[str] = None,
        d1_database_id: Optional[str] = None,
        index_name: Optional[str] = None,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        api_token: Optional[str] = None,
        **kwargs: Any,
    ) -> VST:
        """Create a CloudflareVectorize vectorstore from raw texts.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            texts: List of texts to add to the vectorstore
            embedding: Embedding function to use for the vectors
            metadatas: List of metadata dictionaries to associate with the texts
            ids: List of unique identifiers for the texts
            namespaces: List of namespaces for the texts
            upsert: Whether to upsert the texts into the vectorstore
            account_id: Cloudflare account ID. If not specified, will be read from
                the CF_ACCOUNT_ID environment variable.
            api_token: Optional global API token for all Cloudflare services. If not
                specified, will be read from the CF_API_TOKEN environment variable.
            base_url: Base URL for Cloudflare API (default:
                "https://api.cloudflare.com/client/v4")
            d1_database_id: Optional D1 database ID for storing text data. If not
                specified, will be read from the CF_D1_DATABASE_ID environment variable.
            index_name: Optional name for the default Vectorize index
            **kwargs:
                Additional arguments including:
                - vectorize_api_token: Token for Vectorize service, if api_token not
                    global scoped. If not specified, will be read from the
                    CF_VECTORIZE_API_TOKEN environment variable.
                - d1_api_token: Token for D1 database service, if api_token not
                    global scoped. If not specified, will be read from the
                    CF_D1_API_TOKEN environment variable.
                - default_wait_seconds: # seconds to wait between mutation checks

        Returns:
            CloudflareVectorize: A new CloudflareVectorize vectorstore

        Raises:
            ValueError: If account_id or index_name is not provided
        """
        if not account_id or not index_name:
            raise ValueError("account_id and index_name must be provided")

        if not ids:
            ids = [uuid.uuid4().hex for _ in range(len(texts))]

        vectorize_api_token = kwargs.pop("vectorize_api_token", None)
        d1_api_token = kwargs.pop("d1_api_token", None)

        vectorstore = cls(
            embedding=embedding,
            account_id=account_id,
            d1_database_id=d1_database_id,
            api_token=api_token,
            vectorize_api_token=vectorize_api_token,
            d1_api_token=d1_api_token,
        )

        # create vectorize index if not exists
        vectorstore.create_index(
            index_name=index_name, dimensions=dimensions, metric=metric, **kwargs
        )

        if kwargs.get("wait"):
            vectorstore._poll_mutation_status(index_name=index_name)

        vectorstore.add_texts(
            texts=texts,
            metadatas=metadatas,
            ids=ids,
            namespaces=namespaces,
            upsert=upsert,
            index_name=index_name,
            **kwargs,
        )

        return vectorstore

    # MARK: - afrom_texts
    @classmethod
    async def afrom_texts(
        cls: Type[VST],
        texts: List[str],
        embedding: Embeddings,
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        account_id: Optional[str] = None,
        d1_database_id: Optional[str] = None,
        index_name: Optional[str] = None,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        api_token: Optional[str] = None,
        **kwargs: Any,
    ) -> VST:
        """Asynchronously create a CloudflareVectorize vectorstore from raw texts.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            texts: List of texts to add to the vectorstore
            embedding: Embedding function to use for the vectors
            metadatas: List of metadata dictionaries to associate with the texts
            ids: List of unique identifiers for the texts
            namespaces: List of namespaces for the texts
            upsert: Whether to upsert the texts into the vectorstore
            account_id: Cloudflare account ID. If not specified, will be read from
                the CF_ACCOUNT_ID environment variable.
            api_token: Optional global API token for all Cloudflare services. If not
                specified, will be read from the CF_API_TOKEN environment variable.
            base_url: Base URL for Cloudflare API (default:
                "https://api.cloudflare.com/client/v4")
            d1_database_id: Optional D1 database ID for storing text data. If not
                specified, will be read from the CF_D1_DATABASE_ID environment variable.
            index_name: Optional name for the default Vectorize index
            dimensions: Number of dimensions for the vector embeddings
            metric:
                Distance metric to use (e.g., "cosine", "euclidean", default: "cosine")
            api_token:
                Cloudflare API token, optional if using separate tokens for each service
            **kwargs: Additional keyword arguments to pass to the httpx call

        Returns:
            CloudflareVectorize: A new CloudflareVectorize vectorstore

        Raises:
            ValueError: If account_id or index_name is not provided
        """
        if not account_id or not index_name:
            raise ValueError("account_id and index_name must be provided")

        if not ids:
            ids = [uuid.uuid4().hex for _ in range(len(texts))]

        vectorize_api_token = kwargs.pop("vectorize_api_token", None)
        d1_api_token = kwargs.pop("d1_api_token", None)

        vectorstore = cls(
            embedding=embedding,
            account_id=account_id,
            d1_database_id=d1_database_id,
            api_token=api_token,
            vectorize_api_token=vectorize_api_token,
            d1_api_token=d1_api_token,
        )

        await vectorstore.acreate_index(
            index_name=index_name, dimensions=dimensions, metric=metric, **kwargs
        )

        if kwargs.get("wait"):
            await vectorstore._apoll_mutation_status(index_name=index_name)

        await vectorstore.aadd_texts(
            texts=texts,
            metadatas=metadatas,
            ids=ids,
            namespaces=namespaces,
            upsert=upsert,
            index_name=index_name,
            **kwargs,
        )

        return vectorstore

    # MARK: - from_documents
    @classmethod
    def from_documents(
        cls: Type[VST],
        documents: List[Document],
        embedding: Embeddings,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        account_id: Optional[str] = None,
        d1_database_id: Optional[str] = None,
        index_name: Optional[str] = None,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        api_token: Optional[str] = None,
        **kwargs: Any,
    ) -> VST:
        """Create a CloudflareVectorize vectorstore from documents.

            https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            documents: List of Documents to add to the vectorstore.
            embedding: Embedding function to use to embed the documents.
            ids: Optional list of ids to associate with the documents.
            namespaces: Optional list of namespaces for each vector.
            upsert: If True (default: False), uses insert instead of upsert.
            account_id: Cloudflare account ID.
            d1_database_id: D1 database ID (Optional if using Vectorize only).
            index_name: Name of the Vectorize index.
            dimensions: Number of dimensions for vectors when creating a new index.
            metric:
                Distance metric to use (e.g., "cosine", "euclidean", default: "cosine")
            api_token:
                Cloudflare API token,
                optional if using separate tokens for each service.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            CloudflareVectorize vectorstore.

        Raises:
            ValueError: If the number of documents exceeds MAX_INSERT_SIZE.
        """
        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]

        if "ids" not in kwargs:
            # Filter out None values and use str(uuid.uuid4()) for documents without IDs
            ids = [
                doc.id if doc.id is not None else str(uuid.uuid4()) for doc in documents
            ]
        else:
            ids = None

        return cls.from_texts(
            texts=texts,
            embedding=embedding,
            metadatas=metadatas,
            ids=ids,
            namespaces=namespaces,
            upsert=upsert,
            account_id=account_id,
            d1_database_id=d1_database_id,
            index_name=index_name,
            dimensions=dimensions,
            metric=metric,
            api_token=api_token,
            **kwargs,
        )

    # MARK: - afrom_documents
    @classmethod
    async def afrom_documents(
        cls: Type[VST],
        documents: List[Document],
        embedding: Embeddings,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        account_id: Optional[str] = None,
        d1_database_id: Optional[str] = None,
        index_name: Optional[str] = None,
        dimensions: int = DEFAULT_DIMENSIONS,
        metric: str = DEFAULT_METRIC,
        api_token: Optional[str] = None,
        **kwargs: Any,
    ) -> VST:
        """Asynchronously create a CloudflareVectorize vectorstore from documents.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/

        Args:
            documents: List of Documents to add to the vectorstore.
            embedding: Embedding function to use to embed the documents.
            ids: Optional list of ids to associate with the documents.
            namespaces: Optional list of namespaces for each vector.
            upsert: If True (default: False), uses insert instead of upsert.
            account_id: Cloudflare account ID.
            d1_database_id: D1 database ID (Optional if using Vectorize only).
            index_name: Name of the Vectorize index.
            dimensions: Number of dimensions for vectors when creating a new index.
            metric:
                Distance metric to use (e.g., "cosine", "euclidean", default: "cosine")
            api_token:
                Cloudflare API token,
                optional if using separate tokens for each service.
            **kwargs: Additional keyword arguments to pass to the httpx call

        Returns:
            CloudflareVectorize vectorstore.

        Raises:
            ValueError: If the number of documents exceeds MAX_INSERT_SIZE.
        """
        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]

        if "ids" not in kwargs:
            # Use document IDs if they exist, falling back to provided ids
            doc_ids = []
            for doc in documents:
                if hasattr(doc, "id") and doc.id is not None:
                    doc_ids.append(doc.id)
                else:
                    doc_ids.append(str(uuid.uuid4()))
            kwargs["ids"] = doc_ids
        else:
            ids = None

        return await cls.afrom_texts(
            texts=texts,
            embedding=embedding,
            metadatas=metadatas,
            ids=ids,
            namespaces=namespaces,
            upsert=upsert,
            account_id=account_id,
            d1_database_id=d1_database_id,
            index_name=index_name,
            dimensions=dimensions,
            metric=metric,
            api_token=api_token,
            **kwargs,
        )

    # MARK: - add_documents
    def add_documents(
        self,
        documents: List[Document],
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        index_name: Optional[str] = None,
        wait: bool = False,
        **kwargs: Any,
    ) -> list[str]:
        """Add documents to the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/insert/

        Args:
            documents: List of Documents to add to the vectorstore.
            namespaces: Optional list of namespaces for each vector.
            upsert:
                If True (default: False), uses the insert endpoint
                which will fail if vectors with the same IDs already exist.
                If False (default), uses upsert which will create or update vectors.
            index_name: Name of the Vectorize index.
            wait: If True (default: False), poll until all documents have been added.
            **kwargs: Additional keyword arguments to pass to the requests call

        Returns:
            Operation response with ids of added documents.

        Raises:
            ValueError: If the number of documents exceeds MAX_INSERT_SIZE.
        """
        index_name = index_name or self.index_name

        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]

        # Only generate and pass ids if not already in kwargs
        if "ids" not in kwargs:
            # Generate valid string IDs, ensuring no None values
            doc_ids = []
            for doc in documents:
                if hasattr(doc, "id") and doc.id is not None:
                    doc_ids.append(doc.id)
                else:
                    doc_ids.append(str(uuid.uuid4()))
            kwargs["ids"] = doc_ids

        return self.add_texts(
            texts=texts,
            metadatas=metadatas,
            namespaces=namespaces,
            upsert=upsert,
            index_name=index_name,
            wait=wait,
            **kwargs,
        )

    # MARK: - aadd_documents
    async def aadd_documents(
        self,
        documents: List[Document],
        ids: Optional[List[str]] = None,
        namespaces: Optional[List[str]] = None,
        upsert: bool = False,
        index_name: Optional[str] = None,
        include_d1: bool = True,
        **kwargs: Any,
    ) -> list[str]:
        """Asynchronously add documents to the vectorstore.

        https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/insert/

        Args:
            documents: List of Documents to add to the vectorstore.
            ids: Optional list of ids to associate with the documents.
            namespaces: Optional list of namespaces for each vector.
            upsert:
                If True (default: False), uses the insert endpoint
                which will fail if vectors with the same IDs already exist.
                If False (default), uses upsert which will create or update vectors.
            index_name: Name of the Vectorize index.
            include_d1:
                Whether to also add the documents to the D1 table (default: True)
            **kwargs: Additional keyword arguments to pass to the httpx call

        Returns:
            List of ids from adding the documents into the vectorstore.

        Raises:
            ValueError: If the number of documents exceeds MAX_INSERT_SIZE.
        """
        index_name = index_name or self.index_name

        texts = [doc.page_content for doc in documents]
        metadatas = [doc.metadata for doc in documents]

        # Use document IDs if they exist, falling back to provided ids
        if "ids" not in kwargs:
            # Generate valid string IDs, ensuring no None values
            doc_ids = []
            for doc in documents:
                if hasattr(doc, "id") and doc.id is not None:
                    doc_ids.append(doc.id)
                else:
                    doc_ids.append(str(uuid.uuid4()))
            kwargs["ids"] = doc_ids

        return await self.aadd_texts(
            texts=texts,
            metadatas=metadatas,
            namespaces=namespaces,
            upsert=upsert,
            index_name=index_name,
            include_d1=include_d1,
            **kwargs,
        )
