import functools
from logging import Logger
from typing import Optional, Dict, Any, List, Callable

import beanie.odm.fields
from beanie import Document, init_beanie
from motor.motor_asyncio import AsyncIOMotorClient

from sirius import common, application_performance_monitoring
from sirius.application_performance_monitoring import Operation
from sirius.constants import EnvironmentVariable
from sirius.database.exceptions import DatabaseException, NonUniqueResultException, UncommittedRelationalDocumentException

client: AsyncIOMotorClient = None
database_name: str = common.get_environmental_variable(EnvironmentVariable.DATABASE_NAME)
logger: Logger = application_performance_monitoring.get_logger()


@application_performance_monitoring.transaction(Operation.DATABASE, f"Connect to the {database_name} Database")
async def initialize() -> None:
    global client, database_name
    if client is not None:
        return

    client = AsyncIOMotorClient(common.get_environmental_variable(EnvironmentVariable.MONGO_DB_CONNECTION_STRING))
    await init_beanie(database=client[database_name], document_models=DatabaseDocument.__subclasses__())


def transaction(transaction_name: str) -> Callable:
    def decorator(function: Callable) -> Callable:
        @application_performance_monitoring.transaction(Operation.AORTA_SIRIUS, transaction_name)
        @functools.wraps(function)
        async def wrapper(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
            await initialize()
            return await function(*args, **kwargs)

        return wrapper

    return decorator


class DatabaseDocument(Document):
    id: Optional[beanie.PydanticObjectId] = None

    class Settings:
        validate_on_save = True
        use_state_management = True
        state_management_save_previous = True

        def __init__(self) -> None:
            pass

    def __init__(self, *args: List[Any], **kwargs: Dict[Any, Any]) -> None:
        for key, value in kwargs.items():
            if isinstance(value, DatabaseDocument):
                if value.id is None:
                    raise UncommittedRelationalDocumentException(f"Uncommitted document is trying to be related\nUncommitted Object: {str(object)}\nObject being related: {str(self)}")
                kwargs[key] = value.id  # type: ignore[assignment]

        super().__init__(*args, **kwargs)  # type: ignore[no-untyped-call]

    @application_performance_monitoring.transaction(Operation.DATABASE, "Save")
    async def commit(self) -> "DatabaseDocument":
        if self.id is None:
            return await super().save(link_rule=beanie.WriteRules.WRITE)
        else:
            return await super().save_changes()

    @application_performance_monitoring.transaction(Operation.DATABASE, "Delete")
    async def remove(self) -> None:
        return await super().delete()

    @classmethod
    @application_performance_monitoring.transaction(Operation.DATABASE, "Find Single")
    async def find_unique(cls, *args: List[Any], fetch_links: bool = False) -> Optional["DatabaseDocument"]:
        results_list: List[DatabaseDocument] = await super().find_many(*args).to_list()  # type: ignore[call-overload]

        if len(results_list) == 1:
            result: DatabaseDocument = results_list[0]
        elif len(results_list) == 0:
            return None
        else:
            raise NonUniqueResultException(f"Non-unique result found\nCollection: {cls.__name__}\nSearch Criteria: {str(*args)}")

        if fetch_links:
            await result.fetch_all_links()  # type: ignore[no-untyped-call]
        return result

    @classmethod
    @application_performance_monitoring.transaction(Operation.DATABASE, "Find Multiple")
    async def find_multiple(cls, search_criteria: Dict[Any, Any], fetch_links: bool = False) -> List["DatabaseDocument"]:
        results_list: List[DatabaseDocument] = await super().find_many(search_criteria).to_list()
        if fetch_links:
            for result in results_list:
                await result.fetch_all_links()  # type: ignore[no-untyped-call]
        return results_list

    @staticmethod
    @application_performance_monitoring.transaction(Operation.DATABASE, "Save All")
    async def commit_all(database_document_list: List["DatabaseDocument"]) -> List["DatabaseDocument"]:
        return [await database_document.commit() for database_document in database_document_list]
