"""
database.py

A database holds multiple collections.
"""

import typing
import threading
import inspect

import pymongo
import pymongo.collection
import pymongo.database

from yuno import client, collection as yuno_collection
from yuno.watch import OperationType, Watch

ProfilingLevelType = typing.Literal[0, 1, 2]


class ProfilingLevel:
    try:
        OFF = pymongo.OFF
    except AttributeError:  # according to CI
        OFF = 0
    try:
        SLOW_ONLY = pymongo.SLOW_ONLY
    except AttributeError:  # according to CI
        SLOW_ONLY = 1
    try:
        ALL = pymongo.ALL
    except AttributeError:  # according to CI
        ALL = 2


class YunoDatabase(object):
    __overwritten__ = {"__init__", "aggregate", "command", "create_collection", "drop_collection", "get_collection", "list_collection_names", "list_collections", "profiling_info",
                       "profiling_level", "set_profiling_level", "validate_collection", "watch", "on", "_watch_loop", "__setattr__", "__getitem__", "__getattribute__", "__delattr__", "__delitem__", "__repr__", "__name__", "__client__", "__database__", "__realtime__", "__callbacks__", "__annotations__"}

    __name__: str
    """The name of the database"""
    __client__: "client.YunoClient"
    """The client that this database is connected to"""
    __database__: pymongo.database.Database
    """The PyMongo database object"""

    __realtime__: bool = False
    """Whether the database updates in realtime or not"""
    __callbacks__: typing.Dict[OperationType, typing.List[typing.Callable]] = {}
    """The callbacks registered for realtime updates"""

    def __init__(self, client: "client.YunoClient", name: str = "__yunot_test__") -> None:
        """
        Instantiate the database object

        Parameters
        ----------
        client : YunoClient
            The client that this database is connected to
        name : str, default="__yunot_test__"
            The name of the database
        """
        super().__setattr__("__name__", str(name))
        super().__setattr__("__annotations__", self.__annotations__ if hasattr(self, "__annotations__") else {})
        super().__setattr__("__database__", client.__client__.get_database(name))
        super().__setattr__("__client__", client)
        threading.Thread(target=self._watch_loop, daemon=True).start()

    def aggregate(self, pipeline: typing.List[dict], **kwargs) -> pymongo.cursor.Cursor:
        """
        Perform an aggregation on the database

        Parameters
        ----------
        pipeline : list[dict]
            The pipeline to perform
        kwargs : dict
            Additional arguments to pass to the aggregation
        """
        return self.__database__.aggregate(pipeline, **kwargs)

    def command(self, command: dict, *args, **kwargs) -> dict:
        """
        Execute a command on the database

        Parameters
        ----------
        command : dict
            The command to execute
        args : list
            Additional arguments to pass to the command
        kwargs : dict
            Additional keyword arguments to pass to the command

        Returns
        -------
        dict
            The result of the command
        """
        return self.__database__.command(command, *args, **kwargs)

    def create_collection(self, name: str, **kwargs) -> "yuno_collection.YunoCollection":
        """
        Create a new collection

        Parameters
        ----------
        name : str
            The name of the collection to create
        kwargs : dict
            Additional keyword arguments to pass to the collection creation

        Returns
        -------
        YunoCollection
            The created collection
        """
        self.__database__.create_collection(name, **kwargs)
        return yuno_collection.YunoCollection(self, name)  # this should be tested against __annotations__

    def drop_collection(self, collection: typing.Union[str, "yuno_collection.YunoCollection", pymongo.collection.Collection]) -> None:
        """
        Drops a collection

        Parameters
        ----------
        collection : str, YunoCollection, or pymongo.collection.Collection
            The collection to drop
        """
        if isinstance(collection, yuno_collection.YunoCollection):
            collection = collection.__collection__
        self.__database__.drop_collection(collection)

    def get_collection(self, name: str) -> "yuno_collection.YunoCollection":
        """
        Get a collection by name

        Parameters
        ----------
        name : str
            The name of the collection to get

        Returns
        -------
        YunoCollection
            The collection
        """
        cast = self.__annotations__.get(name, yuno_collection.YunoCollection)
        return cast(self, name)

    def list_collection_names(self, filter: typing.Dict[str, str] = None, **kwargs) -> typing.List[str]:
        """
        List all of the collections in this database

        Parameters
        ----------
        filter : dict[str, str], default=None
            The filter to apply to the list of collections
        kwargs : dict
            Additional keyword arguments to pass to the list_collection_names method

        Returns
        -------
        list[str]
            The list of collection names
        """
        kwargs["filter"] = filter
        return self.__database__.list_collection_names(**kwargs)

    def list_collections(self, filter: typing.Dict[str, str] = None, **kwargs) -> typing.List["yuno_collection.YunoCollection"]:
        """
        List all of the collections in this database

        Parameters
        ----------
        filter : dict[str, str], default=None
            The filter to apply to the list of collections
        kwargs : dict
            Additional keyword arguments to pass to the list_collection_names method

        Returns
        -------
        list[YunoCollection]
            The list of collections
        """
        return [yuno_collection.YunoCollection(self, name) for name in self.list_collection_names(filter, **kwargs)]

    def profiling_info(self, **kwargs):
        """
        Get the profiling information for this database

        Parameters
        ----------
        kwargs : dict
            Additional keyword arguments to pass to the profiling_info method

        Returns
        -------
        dict
            The profiling information
        """
        return self.__database__.profiling_info(**kwargs)

    def profiling_level(self, **kwargs) -> ProfilingLevelType:
        """
        Get the profiling level for this database

        Parameters
        ----------
        kwargs : dict
            Additional keyword arguments to pass to the profiling_level method

        Returns
        -------
        ProfilingLevelType
            The profiling level
        """
        return self.__database__.profiling_level(**kwargs)

    def set_profiling_level(self, level: ProfilingLevelType, **kwargs) -> None:
        """
        Set the profiling level for this database

        Parameters
        ----------
        level : ProfilingLevelType
            The profiling level to set
        kwargs : dict
            Additional keyword arguments to pass to the set_profiling_level method
        """
        self.__database__.set_profiling_level(level, **kwargs)

    def validate_collection(self, collection: typing.Union[str, "yuno_collection.YunoCollection", pymongo.collection.Collection], structure: bool = False, full: bool = False, background: bool = False, *args, **kwargs) -> dict:
        """
        Validate a collection

        Parameters
        ----------
        collection : str, YunoCollection, or pymongo.collection.Collection
            The collection to validate
        structure : bool, default=False
            Whether to validate the structure of the collection
        full : bool, default=False
            Whether to validate the collection's data
        background : bool, default=False
            Whether to validate the collection in the background
        args : list
            Additional arguments to pass to the validate_collection method
        kwargs : dict
            Additional keyword arguments to pass to the validate_collection method

        Returns
        -------
        dict
            The validation information
        """
        if isinstance(collection, yuno_collection.YunoCollection):
            collection = collection.__collection__
        return self.__database__.validate_collection(name_or_collection=collection, scandata=structure, full=full, background=background, *args, **kwargs)

    def _watch_loop(self):
        """
        Internal method that watches the database for changes.

        Also calls all of the callbacks that are registered to the object on the specific operations.
        """
        if not self.__realtime__:
            return
        watch = self.watch(error_limit=10)  # we raise the limit a little bit to be sure we don't miss any changes
        for event in watch:
            if not self.__realtime__:
                break
            for callback, blocking in self.__callbacks__.get(event.operation, []):
                specs = inspect.getfullargspec(callback).args
                kwargs = {}
                if "event" in specs:
                    kwargs["event"] = event
                if "client" in specs:
                    kwargs["client"] = self.__client__
                if "database" in specs:
                    kwargs["database"] = self
                if blocking:
                    callback(**kwargs)
                else:
                    threading.Thread(target=callback, kwargs=kwargs, daemon=True).start()

        watch.close()

    def watch(self, operations: typing.List[OperationType] = None, pipeline: typing.List[dict] = None, full_document: str = None, error_limit: int = 3, error_expiration: float = 60, **kwargs) -> Watch:
        """
        Returns an iterator (Watch) to watch the database for changes.

        Parameters
        ----------
        operations: list[OperationType]
            The operations to watch for.
        pipeline: list[dict]
            The pipeline to watch for.
        full_document: str
            The full_document to watch for.
        error_limit: int
            The number of errors to allow before raising an exception.
        error_expiration: float
            The number of seconds to wait before raising an exception.
        kwargs:
            The kwargs to pass to the watch.

        Returns
        -------
        Watch
            The watch object.

        Example
        --------
        >>> watch = database.watch()
        >>> for event in watch:
        >>>     print(event)
        """
        final_pipeline = []
        if operations:
            final_pipeline.append({"$match": {"operationType": {"$in": operations}}})
        final_pipeline.extend(pipeline if pipeline else [])
        return Watch(self.__database__, pipeline=final_pipeline, full_document=full_document, error_limit=error_limit, error_expiration=error_expiration, **kwargs)

    def on(self, operation: OperationType, callback: typing.Callable, blocking: bool = False) -> None:
        """
        Registers a callback to be called when a certain operation is performed on the current object.

        This implies that __realtime__ is set to True.

        The callback will be called upon the update of the object.

        Parameters
        ----------
        operation: OperationType
            The operation to watch for.
        callback: typing.Callable
            The callback to be called.
        """
        try:
            self.__callbacks__[operation].append((callback, blocking))
        except Exception:
            self.__callbacks__[operation] = [(callback, blocking)]

        self.__realtime__ = True

    def __setattr__(self, name: str, value: dict) -> None:
        """
        Sets an attribute on the object.

        Some objects might affect the object

        Example
        --------
        >>> database.__name__ = "New Database" # this will reinitialize the database with the new name
        """
        if name == "__name__":
            return self.__init__(client=self.__client__, name=value)  # reinitializing the database because it's a different one
        if name == "__realtime__" and not self.__realtime__ and value:
            super().__setattr__(name, value)
            return threading.Thread(target=self._watch_loop, daemon=True).start()
        super().__setattr__(name, value)

    def __getitem__(self, name: str) -> "yuno_collection.YunoCollection":
        """Get a collection from the database. Example: database["collection"]"""
        return self.get_collection(name)

    def __getattribute__(self, name: str) -> typing.Union["yuno_collection.YunoCollection", typing.Any]:
        """
        Get an attribute from the object or a database from the database

        Example
        --------
        >>> database.collection # this will return a collection
        >>> database.__name__ # this will return the name of the database
        """
        if name in super().__getattribute__("__overwritten__"):
            return super().__getattribute__(name)
        return self.__getitem__(name)

    def __delitem__(self, name: str) -> None:
        """Drops a collection. Example: del database["collection"]"""
        self.drop_collection(name)

    def __delattr__(self, name: str) -> None:
        """Drops a collection. Example: del database.collection"""
        self.drop_collection(name)

    def __repr__(self):
        """String representation of the database"""
        return "YunoDatabase('{}')".format(self.__name__)
