import datetime
from abc import ABC, abstractmethod
from enum import Enum
from types import NoneType
from typing import Generic, Optional, Union, TypeVar, Any, Type

from core.database.abc.db_model_abc import DbModelABC
from core.database.database import Database
from core.logger import DBLogger
from core.typing import T, Attribute, AttributeFilters, AttributeSorts

T_DBM = TypeVar("T_DBM", bound=DbModelABC)


class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):

    @abstractmethod
    def __init__(self, source: str, model_type: Type[T_DBM], table_name: str):
        self._logger = DBLogger(source)
        self._model_type = model_type
        self._table_name = table_name

        self._default_filter_condition = None

        self.__attributes: dict[str, type] = {}
        self.__db_names: dict[str, str] = {}
        self.__date_attributes: set[str] = set()
        self.__ignored_attributes: set[str] = set()

        self.__primary_key = "id"
        self.__primary_key_type = int

    @property
    def table_name(self) -> str:
        return self._table_name

    def attribute(
            self,
            attr_name: Attribute,
            attr_type: type,
            db_name: str = None,
            ignore=False,
            primary_key=False,
    ):
        """
        Add an attribute for db and object mapping to the data access object
        :param Attribute attr_name: Name of the attribute in the object
        :param type attr_type: Python type of the attribute to cast db value to
        :param str db_name: Name of the field in the database, if None the attribute lowered attr_name without "_" is used
        :param bool ignore: Defines if field is ignored for create and update (for e.g. auto increment fields or created/updated fields)
        :param bool primary_key: Defines if field is the primary key
        :return:
        """
        if isinstance(attr_name, property):
            attr_name = attr_name.fget.__name__

        self.__attributes[attr_name] = attr_type
        if ignore:
            self.__ignored_attributes.add(attr_name)

        if not db_name:
            db_name = attr_name.lower().replace("_", "")

        self.__db_names[attr_name] = db_name
        if primary_key:
            self.__primary_key = db_name
            self.__primary_key_type = attr_type

        if attr_type in [datetime, datetime.datetime]:
            self.__date_attributes.add(db_name)

    def to_object(self, result: dict) -> T_DBM:
        """
        Convert a result from the database to an object
        :param dict result: Result from the database
        :return:
        """
        value_map: dict[str, T] = {}

        for db_name, value in result.items():
            # Find the attribute name corresponding to the db_name
            attr_name = next(
                (k for k, v in self.__db_names.items() if v == db_name), None
            )
            if attr_name:
                value_map[attr_name] = self._get_value_from_sql(
                    self.__attributes[attr_name], value
                )

        return self._model_type(**value_map)

    async def count(self) -> int:
        result = await self._db.select_map(f"SELECT COUNT(*) FROM {self._table_name}")
        return result[0]["count"]

    async def get_all(self) -> list[T_DBM]:
        result = await self._db.select_map(
            f"SELECT * FROM {self._table_name}{f" WHERE {self._default_filter_condition}" if self._default_filter_condition is not None else ''}"
        )
        if result is None:
            return []

        return [self.to_object(x) for x in result]

    async def get_by_id(self, id: Union[int, str]) -> T_DBM:
        result = await self._db.select_map(
            f"SELECT * FROM {self._table_name} WHERE {f"{self._default_filter_condition} AND " if self._default_filter_condition is not None else ''} {self.__primary_key} = {f"'{id}'" if isinstance(id, str) else id}"
        )
        return self.to_object(result[0])

    async def find_by_id(self, id: Union[int, str]) -> Optional[T_DBM]:
        result = await self._db.select_map(
            f"SELECT * FROM {self._table_name} WHERE {f"{self._default_filter_condition} AND " if self._default_filter_condition is not None else ''} {self.__primary_key} = {f"'{id}'" if isinstance(id, str) else id}"
        )
        if not result or len(result) == 0:
            return None

        return self.to_object(result[0])

    async def get_by(
            self,
            filters: AttributeFilters = None,
            sorts: AttributeSorts = None,
            take: int = None,
            skip: int = None,
    ) -> list[T_DBM]:
        """
        Get all objects by the given filters
        :param AttributeFilter filters:
        :param AttributeSorts sorts:
        :param int skip:
        :param int take:
        :return: List of objects
        :rtype: list[T_DBM]
        :raises ValueError: When no result is found
        """
        result = await self._db.select_map(
            self._build_conditional_query(filters, sorts, take, skip)
        )
        if not result or len(result) == 0:
            raise ValueError("No result found")

        return [self.to_object(x) for x in result]

    async def get_single_by(
            self,
            filters: AttributeFilters = None,
            sorts: AttributeSorts = None,
            take: int = None,
            skip: int = None,
    ) -> T_DBM:
        """
        Get a single object by the given filters
        :param AttributeFilter filters:
        :param AttributeSorts sorts:
        :param int skip:
        :param int take:
        :return: Single object
        :rtype: T_DBM
        :raises ValueError: When no result is found
        :raises ValueError: When more than one result is found
        """
        result = await self.get_by(filters, sorts, take, skip)
        if not result:
            raise ValueError("No result found")
        if len(result) > 1:
            raise ValueError("More than one result found")
        return result[0]

    async def find_by(
            self,
            filters: AttributeFilters = None,
            sorts: AttributeSorts = None,
            take: int = None,
            skip: int = None,
    ) -> list[Optional[T_DBM]]:
        """
        Find all objects by the given filters
        :param AttributeFilter filters:
        :param AttributeSorts sorts:
        :param int skip:
        :param int take:
        :return: List of objects
        :rtype: list[Optional[T_DBM]]
        """
        result = await self._db.select_map(
            self._build_conditional_query(filters, sorts, take, skip)
        )
        if not result or len(result) == 0:
            return []

        return [self.to_object(x) for x in result]

    async def find_single_by(
            self,
            filters: AttributeFilters = None,
            sorts: AttributeSorts = None,
            take: int = None,
            skip: int = None,
    ) -> Optional[T_DBM]:
        """
        Find a single object by the given filters
        :param AttributeFilter filters:
        :param AttributeSorts sorts:
        :param int skip:
        :param int take:
        :return: Single object
        :rtype: Optional[T_DBM]
        :raises ValueError: When more than one result is found
        """
        result = await self.find_by(filters, sorts, take, skip)
        if not result or len(result) == 0:
            return None
        if len(result) > 1:
            raise ValueError("More than one result found")
        return result[0]

    async def _build_create_statement(self, obj: T_DBM, skip_editor=False) -> str:
        allowed_fields = [
            x for x in self.__attributes.keys() if x not in self.__ignored_attributes
        ]

        fields = ", ".join([self.__db_names[x] for x in allowed_fields])
        fields = f"{'EditorId' if not skip_editor else ''}{f', {fields}' if not skip_editor and len(fields) > 0 else f'{fields}'}"

        values = ", ".join(
            [self._get_value_sql(getattr(obj, x)) for x in allowed_fields]
        )
        values = f"{await self._get_editor_id(obj) if not skip_editor else ''}{f', {values}' if not skip_editor and len(values) > 0 else f'{values}'}"

        return f"""
                INSERT INTO {self._table_name} (
                    {fields}
                ) VALUES (
                    {values}
                )
                RETURNING {self.__primary_key};
            """

    async def create(self, obj: T_DBM, skip_editor=False) -> int:
        self._logger.debug(f"create {type(obj).__name__} {obj.__dict__}")

        result = await self._db.execute(
            await self._build_create_statement(obj, skip_editor)
        )
        return self._get_value_from_sql(self.__primary_key_type, result[0][0])

    async def create_many(self, objs: list[T_DBM], skip_editor=False) -> list[int]:
        if len(objs) == 0:
            return []
        self._logger.debug(
            f"create many {type(objs[0]).__name__} {len(objs)} {[x.__dict__ for x in objs]}"
        )

        query = ""
        for obj in objs:
            query += await self._build_create_statement(obj, skip_editor)

        result = await self._db.execute(query)
        return [self._get_value_from_sql(self.__primary_key_type, x[0]) for x in result]

    async def _build_update_statement(self, obj: T_DBM, skip_editor=False) -> str:
        allowed_fields = [
            x for x in self.__attributes.keys() if x not in self.__ignored_attributes
        ]

        fields = ", ".join(
            [
                f"{self.__db_names[x]} = {self._get_value_sql(getattr(obj, x, None))}"
                for x in allowed_fields
            ]
        )
        fields = f"{f'EditorId = {await self._get_editor_id(obj)}' if not skip_editor else ''}{f', {fields}' if not skip_editor and len(fields) > 0 else f'{fields}'}"

        return f"""
                UPDATE {self._table_name}
                SET {fields}
                WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
            """

    async def update(self, obj: T_DBM, skip_editor=False):
        self._logger.debug(f"update {type(obj).__name__} {obj.__dict__}")
        await self._db.execute(await self._build_update_statement(obj, skip_editor))

    async def update_many(self, objs: list[T_DBM], skip_editor=False):
        if len(objs) == 0:
            return
        self._logger.debug(
            f"update many {type(objs[0]).__name__} {len(objs)} {[x.__dict__ for x in objs]}"
        )

        query = ""
        for obj in objs:
            query += await self._build_update_statement(obj, skip_editor)

        await self._db.execute(query)

    async def _build_delete_statement(
            self, obj: T_DBM, hard_delete: bool = False
    ) -> str:
        if hard_delete:
            return f"""
                DELETE FROM {self._table_name}
                WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
            """

        return f"""
                UPDATE {self._table_name}
                SET EditorId = {await self._get_editor_id(obj)},
                    Deleted = true
                WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
            """

    async def delete(self, obj: T_DBM, hard_delete: bool = False):
        self._logger.debug(f"delete {type(obj).__name__} {obj.__dict__}")
        await self._db.execute(await self._build_delete_statement(obj, hard_delete))

    async def delete_many(self, objs: list[T_DBM], hard_delete: bool = False):
        if len(objs) == 0:
            return
        self._logger.debug(
            f"delete many {type(objs[0]).__name__} {len(objs)} {[x.__dict__ for x in objs]}"
        )

        query = ""
        for obj in objs:
            query += await self._build_delete_statement(obj, hard_delete)

        await self._db.execute(query)

    async def _build_restore_statement(self, obj: T_DBM) -> str:
        return f"""
                UPDATE {self._table_name}
                SET EditorId = {await self._get_editor_id(obj)},
                    Deleted = false
                WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
            """

    async def restore(self, obj: T_DBM):
        self._logger.debug(f"restore {type(obj).__name__} {obj.__dict__}")
        await self._db.execute(await self._build_restore_statement(obj))

    async def restore_many(self, objs: list[T_DBM]):
        if len(objs) == 0:
            return
        self._logger.debug(
            f"restore many {type(objs[0]).__name__} {len(objs)} {objs[0].__dict__}"
        )

        query = ""
        for obj in objs:
            query += await self._build_restore_statement(obj)

        await self._db.execute(query)

    def _get_primary_key_value_sql(self, obj: T_DBM) -> str:
        value = getattr(obj, self.__primary_key)
        if isinstance(value, str):
            return f"'{value}'"

        return value

    @staticmethod
    def _get_value_sql(value: Any) -> str:
        if isinstance(value, str):
            if value.lower() == "null":
                return "NULL"
            return f"'{value}'"

        if isinstance(value, NoneType):
            return "NULL"

        if value is None:
            return "NULL"

        if isinstance(value, Enum):
            return str(value.value)

        if isinstance(value, bool):
            return "true" if value else "false"

        if isinstance(value, list):
            if len(value) == 0:
                return "ARRAY[]::text[]"
            return f"ARRAY[{", ".join([DataAccessObjectABC._get_value_sql(x) for x in value])}]"

        return str(value)

    @staticmethod
    def _get_value_from_sql(cast_type: type, value: Any) -> Optional[T]:
        """
        Get the value from the query result and cast it to the correct type
        :param type cast_type:
        :param Any value:
        :return Optional[T]: Casted value, when value is str "NULL" None is returned
        """
        if isinstance(value, str) and "NULL" in value:
            return None

        if isinstance(value, NoneType):
            return None

        if isinstance(value, cast_type):
            return value

        return cast_type(value)

    def _build_conditional_query(
            self,
            filters: AttributeFilters = None,
            sorts: AttributeSorts = None,
            take: int = None,
            skip: int = None,
    ) -> str:
        query = f"SELECT * FROM {self._table_name}"

        if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
            query += f" WHERE {self._build_conditions(filters)}"
        if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
            query += f" ORDER BY {self._build_order_by(sorts)}"
        if take is not None:
            query += f" LIMIT {take}"
        if skip is not None:
            query += f" OFFSET {skip}"
        return query

    def _build_conditions(self, filters: AttributeFilters) -> str:
        """
        Build SQL conditions from the given filters
        :param filters:
        :return:
        """
        if not isinstance(filters, list):
            filters = [filters]

        conditions = []
        for f in filters:
            for attr, values in f.items():
                if isinstance(attr, property):
                    attr = attr.fget.__name__
                db_name = self.__db_names[attr]

                if isinstance(values, dict):
                    for operator, value in values.items():
                        conditions.append(
                            self._build_condition(db_name, operator, value)
                        )
                elif isinstance(values, list):
                    sub_conditions = []
                    for value in values:
                        if isinstance(value, dict):
                            for operator, val in value.items():
                                sub_conditions.append(
                                    self._build_condition(db_name, operator, val)
                                )
                        else:
                            sub_conditions.append(
                                self._get_value_validation_sql(db_name, value)
                            )
                    conditions.append(f"({' OR '.join(sub_conditions)})")
                else:
                    conditions.append(self._get_value_validation_sql(db_name, values))

        return " AND ".join(conditions)

    def _get_value_validation_sql(self, field: str, value: Any):
        value = self._get_value_sql(value)

        if value == "NULL":
            return f"{field} IS NULL"
        return f"{field} = {value}"

    def _build_condition(self, db_name: str, operator: str, value: Any) -> str:
        """
        Build individual SQL condition based on the operator
        :param db_name:
        :param operator:
        :param value:
        :return:
        """
        if db_name in self.__date_attributes:
            db_name = f"TO_CHAR({db_name}, 'DD.MM.YYYY HH24:MI:SS.US')"

        sql_value = self._get_value_sql(value)
        if operator == "equal":
            return f"{db_name} = {sql_value}"
        elif operator == "notEqual":
            return f"{db_name} != {sql_value}"
        elif operator == "greater":
            return f"{db_name} > {sql_value}"
        elif operator == "greaterOrEqual":
            return f"{db_name} >= {sql_value}"
        elif operator == "less":
            return f"{db_name} < {sql_value}"
        elif operator == "lessOrEqual":
            return f"{db_name} <= {sql_value}"
        elif operator == "isNull":
            return f"{db_name} IS NULL"
        elif operator == "isNotNull":
            return f"{db_name} IS NOT NULL"
        elif operator == "contains":
            return f"{db_name} LIKE '%{value}%'"
        elif operator == "notContains":
            return f"{db_name} NOT LIKE '%{value}%'"
        elif operator == "startsWith":
            return f"{db_name} LIKE '{value}%'"
        elif operator == "endsWith":
            return f"{db_name} LIKE '%{value}'"
        elif operator == "in":
            return (
                f"{db_name} IN ({', '.join([self._get_value_sql(x) for x in value])})"
            )
        elif operator == "notIn":
            return f"{db_name} NOT IN ({', '.join([self._get_value_sql(x) for x in value])})"
        else:
            raise ValueError(f"Unsupported operator: {operator}")

    def _build_order_by(self, sorts: AttributeSorts) -> str:
        """
        Build SQL order by clause from the given sorts
        :param sorts:
        :return:
        """
        if not isinstance(sorts, list):
            sorts = [sorts]

        sort_clauses = []
        for sort in sorts:
            for attr, direction in sort.items():
                if isinstance(attr, property):
                    attr = attr.fget.__name__

                match attr:
                    case "createdUtc":
                        attr = "created"
                    case "updatedUtc":
                        attr = "updated"

                if attr.endswith("Utc") and attr.split("Utc")[0].lower() in [
                    "created",
                    "updated",
                ]:
                    attr = attr.replace("Utc", "")

                db_name = self.__db_names[attr]
                sort_clauses.append(f"{db_name} {direction.upper()}")

        return ", ".join(sort_clauses)

    @staticmethod
    async def _get_editor_id(obj: T_DBM):
        editor_id = obj.editor_id
        if editor_id is None:
            from api.route import Route

            user = await Route.get_user()
            if user is not None:
                editor_id = user.id

        return editor_id if editor_id is not None else "NULL"
