from typing import Iterator, List, Optional, Sequence, Tuple

from langchain_core.documents import Document
from langchain_core.stores import BaseStore


class MongoDBByteStore(BaseStore[str, bytes]):
    """BaseStore implementation using MongoDB as the underlying store.

    Examples:
        Create a MongoDBByteStore instance and perform operations on it:

        .. code-block:: python

            # Instantiate the MongoDBByteStore with a MongoDB connection
            from langchain.storage import MongoDBByteStore

            mongo_conn_str = "mongodb://localhost:27017/"
            mongodb_store = MongoDBBytesStore(mongo_conn_str, db_name="test-db",
                                         collection_name="test-collection")

            # Set values for keys
            mongodb_store.mset([("key1", "hello"), ("key2", "workd")])

            # Get values for keys
            values = mongodb_store.mget(["key1", "key2"])
            # [bytes1, bytes1]

            # Iterate over keys
            for key in mongodb_store.yield_keys():
                print(key)

            # Delete keys
            mongodb_store.mdelete(["key1", "key2"])
    """

    def __init__(
        self,
        connection_string: str,
        db_name: str,
        collection_name: str,
        *,
        client_kwargs: Optional[dict] = None,
    ) -> None:
        """Initialize the MongoDBStore with a MongoDB connection string.

        Args:
            connection_string (str): MongoDB connection string
            db_name (str): name to use
            collection_name (str): collection name to use
            client_kwargs (dict): Keyword arguments to pass to the Mongo client
        """
        try:
            from pymongo import MongoClient
        except ImportError as e:
            raise ImportError(
                "The MongoDBStore requires the pymongo library to be "
                "installed. "
                "pip install pymongo"
            ) from e

        if not connection_string:
            raise ValueError("connection_string must be provided.")
        if not db_name:
            raise ValueError("db_name must be provided.")
        if not collection_name:
            raise ValueError("collection_name must be provided.")

        self.client: MongoClient = MongoClient(
            connection_string, **(client_kwargs or {})
        )
        self.collection = self.client[db_name][collection_name]

    def mget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
        """Get the list of documents associated with the given keys.

        Args:
            keys (list[str]): A list of keys representing Document IDs..

        Returns:
            list[Document]: A list of Documents corresponding to the provided
                keys, where each Document is either retrieved successfully or
                represented as None if not found.
        """
        result = self.collection.find({"_id": {"$in": keys}})
        result_dict = {doc["_id"]: doc["value"] for doc in result}
        return [result_dict.get(key) for key in keys]

    def mset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
        """Set the given key-value pairs.

        Args:
            key_value_pairs (list[tuple[str, Document]]): A list of id-document
                pairs.
        """
        from pymongo import UpdateOne

        updates = [{"_id": k, "value": v} for k, v in key_value_pairs]
        self.collection.bulk_write(
            [UpdateOne({"_id": u["_id"]}, {"$set": u}, upsert=True) for u in updates]
        )

    def mdelete(self, keys: Sequence[str]) -> None:
        """Delete the given ids.

        Args:
            keys (list[str]): A list of keys representing Document IDs..
        """
        self.collection.delete_many({"_id": {"$in": keys}})

    def yield_keys(self, prefix: Optional[str] = None) -> Iterator[str]:
        """Yield keys in the store.

        Args:
            prefix (str): prefix of keys to retrieve.
        """
        if prefix is None:
            for doc in self.collection.find(projection=["_id"]):
                yield doc["_id"]
        else:
            for doc in self.collection.find(
                {"_id": {"$regex": f"^{prefix}"}}, projection=["_id"]
            ):
                yield doc["_id"]


class MongoDBStore(BaseStore[str, Document]):
    """BaseStore implementation using MongoDB as the underlying store.

    Examples:
        Create a MongoDBStore instance and perform operations on it:

        .. code-block:: python

            # Instantiate the MongoDBStore with a MongoDB connection
            from langchain.storage import MongoDBStore

            mongo_conn_str = "mongodb://localhost:27017/"
            mongodb_store = MongoDBStore(mongo_conn_str, db_name="test-db",
                                         collection_name="test-collection")

            # Set values for keys
            doc1 = Document(...)
            doc2 = Document(...)
            mongodb_store.mset([("key1", doc1), ("key2", doc2)])

            # Get values for keys
            values = mongodb_store.mget(["key1", "key2"])
            # [doc1, doc2]

            # Iterate over keys
            for key in mongodb_store.yield_keys():
                print(key)

            # Delete keys
            mongodb_store.mdelete(["key1", "key2"])
    """

    def __init__(
        self,
        connection_string: str,
        db_name: str,
        collection_name: str,
        *,
        client_kwargs: Optional[dict] = None,
    ) -> None:
        """Initialize the MongoDBStore with a MongoDB connection string.

        Args:
            connection_string (str): MongoDB connection string
            db_name (str): name to use
            collection_name (str): collection name to use
            client_kwargs (dict): Keyword arguments to pass to the Mongo client
        """
        try:
            from pymongo import MongoClient
        except ImportError as e:
            raise ImportError(
                "The MongoDBStore requires the pymongo library to be "
                "installed. "
                "pip install pymongo"
            ) from e

        if not connection_string:
            raise ValueError("connection_string must be provided.")
        if not db_name:
            raise ValueError("db_name must be provided.")
        if not collection_name:
            raise ValueError("collection_name must be provided.")

        self.client: MongoClient = MongoClient(
            connection_string, **(client_kwargs or {})
        )
        self.collection = self.client[db_name][collection_name]

    def mget(self, keys: Sequence[str]) -> List[Optional[Document]]:
        """Get the list of documents associated with the given keys.

        Args:
            keys (list[str]): A list of keys representing Document IDs..

        Returns:
            list[Document]: A list of Documents corresponding to the provided
                keys, where each Document is either retrieved successfully or
                represented as None if not found.
        """
        result = self.collection.find({"_id": {"$in": keys}})
        result_dict = {doc["_id"]: Document(**doc["value"]) for doc in result}
        return [result_dict.get(key) for key in keys]

    def mset(self, key_value_pairs: Sequence[Tuple[str, Document]]) -> None:
        """Set the given key-value pairs.

        Args:
            key_value_pairs (list[tuple[str, Document]]): A list of id-document
                pairs.
        Returns:
            None
        """
        from pymongo import UpdateOne

        updates = [{"_id": k, "value": v.__dict__} for k, v in key_value_pairs]
        self.collection.bulk_write(
            [UpdateOne({"_id": u["_id"]}, {"$set": u}, upsert=True) for u in updates]
        )

    def mdelete(self, keys: Sequence[str]) -> None:
        """Delete the given ids.

        Args:
            keys (list[str]): A list of keys representing Document IDs..
        """
        self.collection.delete_many({"_id": {"$in": keys}})

    def yield_keys(self, prefix: Optional[str] = None) -> Iterator[str]:
        """Yield keys in the store.

        Args:
            prefix (str): prefix of keys to retrieve.
        """
        if prefix is None:
            for doc in self.collection.find(projection=["_id"]):
                yield doc["_id"]
        else:
            for doc in self.collection.find(
                {"_id": {"$regex": f"^{prefix}"}}, projection=["_id"]
            ):
                yield doc["_id"]
