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.const import DATETIME_FORMAT
from core.database.abc.db_model_abc import DbModelABC
from core.database.database import Database
from core.database.external_data_temp_table_builder import ExternalDataTempTableBuilder
from core.get_value import get_value
from core.logger import DBLogger
from core.string import camel_to_snake
from core.typing import T, Attribute, AttributeFilters, AttributeSorts, Id

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


class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
    _external_fields: dict[str, ExternalDataTempTableBuilder] = {}
    _operators = [
        "equal",
        "notEqual",
        "greater",
        "greaterOrEqual",
        "less",
        "lessOrEqual",
        "isNull",
        "isNotNull",
        "contains",
        "notContains",
        "startsWith",
        "endsWith",
        "in",
        "notIn",
    ]

    @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.__joins: dict[str, str] = {}

        self.__db_names: dict[str, str] = {}
        self.__foreign_tables: dict[str, str] = {}
        self.__foreign_table_keys: 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,
        aliases: list[str] = None,
    ):
        """
        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
        :param list[str] aliases: List of aliases for the attribute name
        :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
        self.__db_names[db_name] = db_name

        if aliases is not None:
            for alias in aliases:
                if alias in self.__db_names:
                    raise ValueError(f"Alias {alias} already exists")
                self.__db_names[alias] = 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(attr_name)
            self.__date_attributes.add(db_name)

    def reference(
        self,
        attr: Attribute,
        primary_attr: Attribute,
        foreign_attr: Attribute,
        table_name: str,
    ):
        """
        Add a reference to another table for the given attribute
        :param Attribute attr: Name of the attribute in the object
        :param str primary_attr: Name of the primary key in the foreign object
        :param str foreign_attr: Name of the foreign key in the object
        :param str table_name: Name of the table to reference
        :return:
        """
        if isinstance(attr, property):
            attr = attr.fget.__name__

        if isinstance(primary_attr, property):
            primary_attr = primary_attr.fget.__name__

        primary_attr = primary_attr.lower().replace("_", "")

        if isinstance(foreign_attr, property):
            foreign_attr = foreign_attr.fget.__name__

        foreign_attr = foreign_attr.lower().replace("_", "")

        self.__foreign_table_keys[attr] = foreign_attr
        if table_name == self._table_name:
            return

        self.__joins[foreign_attr] = (
            f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
        )
        self.__foreign_tables[attr] = table_name

    def use_external_fields(self, builder: ExternalDataTempTableBuilder):
        self._external_fields[builder.table_name] = builder

    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, filters: AttributeFilters = None) -> int:
        query = f"SELECT COUNT(*) FROM {self._table_name}"
        for join in self.__joins:
            query += f" {self.__joins[join]}"

        if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
            conditions, external_table_deps = await self._build_conditions(filters)
            query = await self._handle_query_external_temp_tables(
                query, external_table_deps, ignore_fields=True
            )
            query += f" WHERE {conditions};"

        result = await self._db.select_map(query)
        if len(result) == 0:
            return 0
        return result[0]["count"]

    async def get_history(
        self,
        entry_id: int,
        by_key: str = None,
        when: datetime = None,
        until: datetime = None,
        without_deleted=False,
    ) -> list[T_DBM]:
        query = f"SELECT {self._table_name}_history.* FROM {self._table_name}_history"
        for join in self.__joins:
            query += f" {self.__joins[join].replace(self._table_name, f'{self._table_name}_history')}"

        query += f" WHERE {f'{self._table_name}_history.{self.__primary_key}' if by_key is None else f'{self._table_name}_history.{by_key}'} = {entry_id}"

        if self._default_filter_condition is not None:
            query += f" AND {self._default_filter_condition}"

        if without_deleted:
            query += f" AND {self._table_name}_history.deleted = false"

        if when is not None:
            query += f" AND {self._attr_from_date_to_char(f'{self._table_name}_history.updated')} = '{when.strftime(DATETIME_FORMAT)}'"

        if until is not None:
            query += f" AND {self._attr_from_date_to_char(f'{self._table_name}_history.updated')} <= '{until.strftime(DATETIME_FORMAT)}'"

        query += f" ORDER BY {self._table_name}_history.updated DESC;"

        result = await self._db.select_map(query)
        if result is None:
            return []
        return [self.to_object(x) for x in result]

    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 ''} ORDER BY {self.__primary_key};"
        )
        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(
            await 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(
            await 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 touch(self, obj: T_DBM):
        """
        Touch the entry to update the last updated date
        :return:
        """
        await self._db.execute(
            f"""
            UPDATE {self._table_name}
            SET updated = NOW()
            WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
        """
        )

    async def touch_many_by_id(self, ids: list[Id]):
        """
        Touch the entries to update the last updated date
        :return:
        """
        if len(ids) == 0:
            return

        await self._db.execute(
            f"""
            UPDATE {self._table_name}
            SET updated = NOW()
            WHERE {self.__primary_key} IN ({", ".join([str(x) for x in ids])});
        """
        )

    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 f"'{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])}]"

        if isinstance(value, datetime.datetime):
            if value.tzinfo is None:
                value = value.replace(tzinfo=datetime.timezone.utc)

            return f"'{value.strftime(DATETIME_FORMAT)}'"

        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)

    async def _handle_query_external_temp_tables(
        self, query: str, external_table_deps: list[str], ignore_fields=False
    ) -> str:
        for dep in external_table_deps:
            temp_table = self._external_fields[dep]
            temp_table_sql = await temp_table.build()

            if not ignore_fields:
                query = query.replace(
                    " FROM",
                    f", {','.join([f'{temp_table.table_name}.{x}' for x in temp_table.fields.keys() if x not in self.__db_names])} FROM",
                )

            query = f"{temp_table_sql}\n{query}"
            query += f" LEFT JOIN {temp_table.table_name} ON {temp_table.join_ref_table}.{self.__primary_key} = {temp_table.table_name}.{temp_table.primary_key}"

        return query

    async def _build_conditional_query(
        self,
        filters: AttributeFilters = None,
        sorts: AttributeSorts = None,
        take: int = None,
        skip: int = None,
    ) -> str:
        filter_conditions = []
        sort_conditions = []

        external_table_deps = []
        query = f"SELECT {self._table_name}.* FROM {self._table_name}"
        for join in self.__joins:
            query += f" {self.__joins[join]}"

        # Collect dependencies from filters
        if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
            filter_conditions, filter_deps = await self._build_conditions(filters)
            external_table_deps.extend(filter_deps)

        # Collect dependencies from sorts
        if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
            sort_conditions, sort_deps = self._build_order_by(sorts)
            external_table_deps.extend(sort_deps)

        # Handle external table dependencies before WHERE and ORDER BY
        if external_table_deps:
            query = await self._handle_query_external_temp_tables(
                query, external_table_deps
            )

        # Add WHERE clause
        if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
            query += f" WHERE {filter_conditions}"

        # Add ORDER BY clause
        if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
            query += f" ORDER BY {sort_conditions}"

        if take is not None:
            query += f" LIMIT {take}"

        if skip is not None:
            query += f" OFFSET {skip}"

        if not query.endswith(";"):
            query += ";"
        return query

    def _get_external_field_key(self, field_name: str) -> Optional[str]:
        """
        Returns the key to get the external field if found, otherwise None.
        :param str field_name: The name of the field to search for.
        :return: The key if found, otherwise None.
        :rtype: Optional[str]
        """
        for key, builder in self._external_fields.items():
            if field_name in builder.fields and field_name not in self.__db_names:
                return key
        return None

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

        conditions = []
        for f in filters:
            f_conditions = []

            for attr, values in f.items():
                if isinstance(attr, property):
                    attr = attr.fget.__name__

                if attr in self.__foreign_tables:
                    foreign_table = self.__foreign_tables[attr]
                    cons, eftd = self._build_foreign_conditions(
                        attr, foreign_table, values
                    )
                    if eftd:
                        external_field_table_deps.extend(eftd)

                    f_conditions.extend(cons)
                    continue

                if attr == "fuzzy":
                    self._handle_fuzzy_filter_conditions(
                        f_conditions, external_field_table_deps, values
                    )
                    continue

                external_fields_table_name = self._get_external_field_key(attr)
                if external_fields_table_name is not None:
                    external_fields_table = self._external_fields[
                        external_fields_table_name
                    ]
                    db_name = f"{external_fields_table.table_name}.{attr}"
                    external_field_table_deps.append(external_fields_table.table_name)
                elif (
                    isinstance(values, dict) or isinstance(values, list)
                ) and not attr in self.__foreign_tables:
                    db_name = f"{self._table_name}.{self.__db_names[attr]}"
                elif attr in self._operators:
                    db_name = f"{self._table_name}.{self.__db_names[attr]}"
                else:
                    db_name = self.__db_names[attr]

                if isinstance(values, dict):
                    for operator, value in values.items():
                        f_conditions.append(
                            self._build_condition(f"{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(f"{db_name}", operator, val)
                                )
                        else:
                            sub_conditions.append(
                                self._get_value_validation_sql(db_name, value)
                            )
                    f_conditions.append(f"({' OR '.join(sub_conditions)})")
                elif attr in self._operators:
                    conditions.append(f"{self._build_condition(db_name, attr, values)}")
                else:
                    f_conditions.append(self._get_value_validation_sql(db_name, values))

            conditions.append(f"({' OR '.join(f_conditions)})")

        return " AND ".join(conditions), external_field_table_deps

    @staticmethod
    def _build_fuzzy_conditions(
        fields: list[str], term: str, threshold: int = 10
    ) -> list[str]:
        conditions = []
        for field in fields:
            conditions.append(
                f"levenshtein({field}::TEXT, '{term}') <= {threshold}"
            )  # Adjust the threshold as needed

        return conditions

    def _build_foreign_conditions(
        self, base_attr: str, table: str, values: dict
    ) -> (list[str], list[str]):
        """
        Build SQL conditions for foreign key references
        :param base_attr: Base attribute name
        :param table: Foreign table name
        :param values: Filter values
        :return: List of conditions, List of external field tables
        """
        external_field_table_deps = []
        conditions = []
        for attr, sub_values in values.items():
            if isinstance(attr, property):
                attr = attr.fget.__name__

            if attr in self.__foreign_tables:
                foreign_table = self.__foreign_tables[attr]
                sub_conditions, eftd = self._build_foreign_conditions(
                    attr, foreign_table, sub_values
                )
                if len(eftd) > 0:
                    external_field_table_deps.extend(eftd)

                conditions.extend(sub_conditions)
                continue

            if attr == "fuzzy":
                self._handle_fuzzy_filter_conditions(
                    conditions, external_field_table_deps, sub_values
                )
                continue

            external_fields_table_name = self._get_external_field_key(attr)
            if external_fields_table_name is not None:
                external_fields_table = self._external_fields[
                    external_fields_table_name
                ]
                db_name = f"{external_fields_table.table_name}.{attr}"
                external_field_table_deps.append(external_fields_table.table_name)
            elif attr in self._operators:
                db_name = f"{self._table_name}.{self.__foreign_table_keys[base_attr]}"
            else:
                db_name = f"{table}.{attr.lower().replace('_', '')}"

            if isinstance(sub_values, dict):
                for operator, value in sub_values.items():
                    conditions.append(
                        f"{self._build_condition(db_name, operator, value)}"
                    )
            elif isinstance(sub_values, list):
                sub_conditions = []
                for value in sub_values:
                    if isinstance(value, dict):
                        for operator, val in value.items():
                            sub_conditions.append(
                                f"{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)})")
            elif attr in self._operators:
                conditions.append(f"{self._build_condition(db_name, attr, sub_values)}")
            else:
                conditions.append(self._get_value_validation_sql(db_name, sub_values))

        return conditions, external_field_table_deps

    def _handle_fuzzy_filter_conditions(
        self, conditions, external_field_table_deps, sub_values
    ):
        fuzzy_fields = get_value(sub_values, "fields", list[str])
        fuzzy_fields_db_names = []
        for fuzzy_field in fuzzy_fields:
            external_fields_table_name = self._get_external_field_key(fuzzy_field)
            if external_fields_table_name is not None:
                external_fields_table = self._external_fields[
                    external_fields_table_name
                ]
                fuzzy_fields_db_names.append(
                    f"{external_fields_table.table_name}.{fuzzy_field}"
                )
                external_field_table_deps.append(external_fields_table.table_name)
            elif fuzzy_field in self.__db_names:
                fuzzy_fields_db_names.append(
                    f"{self._table_name}.{self.__db_names[fuzzy_field]}"
                )
            elif fuzzy_field in self.__foreign_tables:
                fuzzy_fields_db_names.append(
                    f"{self._table_name}.{self.__foreign_table_keys[fuzzy_field]}"
                )
            else:
                fuzzy_fields_db_names.append(
                    self.__db_names[camel_to_snake(fuzzy_field)]
                )
        conditions.append(
            f"({' OR '.join(
                self._build_fuzzy_conditions(
                    [x for x in fuzzy_fields_db_names],
                    get_value(sub_values, "term", str),
                    get_value(sub_values, "threshold", int, 5),
                )
            )
            })"
        )

    def _get_value_validation_sql(self, field: str, value: Any):
        value = self._get_value_sql(value)
        field_selector = (
            f"{self._table_name}.{field}"
            if not field.startswith(self._table_name)
            else field
        )
        if field in self.__foreign_tables:
            field_selector = self.__db_names[field]

        if value == "NULL":
            return f"{field_selector} IS NULL"
        return f"{field_selector} = {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:
        """
        attr = db_name.split(".")[-1]

        if attr in self.__date_attributes:
            db_name = self._attr_from_date_to_char(db_name)

        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" if sql_value else f"{db_name} IS NOT NULL"
        elif operator == "isNotNull":
            return f"{db_name} IS NOT NULL" if sql_value else f"{db_name} IS 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}")

    @staticmethod
    def _attr_from_date_to_char(attr: str) -> str:
        return f"TO_CHAR({attr}, 'YYYY-MM-DD HH24:MI:SS.US TZ')"

    def _build_order_by(self, sorts: AttributeSorts) -> (str, list[str]):
        """
        Build SQL order by clause from the given sorts
        :param sorts:
        :return:
        """
        external_field_table_deps = []
        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__

                if attr in self.__foreign_tables:
                    foreign_table = self.__foreign_tables[attr]
                    f_sorts, eftd = self._build_foreign_order_by(
                        foreign_table, direction
                    )
                    if eftd:
                        external_field_table_deps.extend(eftd)

                    sort_clauses.extend(f_sorts)
                    continue

                external_fields_table_name = self._get_external_field_key(attr)
                if external_fields_table_name is not None:
                    external_fields_table = self._external_fields[
                        external_fields_table_name
                    ]
                    db_name = f"{external_fields_table.table_name}.{attr}"
                    external_field_table_deps.append(external_fields_table.table_name)
                else:
                    db_name = self.__db_names[attr]
                sort_clauses.append(f"{db_name} {direction.upper()}")

        return ", ".join(sort_clauses), external_field_table_deps

    def _build_foreign_order_by(
        self, table: str, direction: dict
    ) -> (list[str], list[str]):
        """
        Build SQL order by clause for foreign key references
        :param table: Foreign table name
        :param direction: Sort direction
        :return: List of order by clauses
        """
        external_field_table_deps = []
        sort_clauses = []
        for attr, sub_direction in direction.items():
            if isinstance(attr, property):
                attr = attr.fget.__name__

            if attr in self.__foreign_tables:
                foreign_table = self.__foreign_tables[attr]
                f_sorts, eftd = self._build_foreign_order_by(foreign_table, direction)
                if eftd:
                    external_field_table_deps.extend(eftd)

                sort_clauses.extend(f_sorts)
                continue

            external_fields_table_name = self._get_external_field_key(attr)
            if external_fields_table_name is not None:
                external_fields_table = self._external_fields[
                    external_fields_table_name
                ]
                db_name = f"{external_fields_table.table_name}.{attr}"
                external_field_table_deps.append(external_fields_table.table_name)
            else:
                db_name = f"{table}.{attr.lower().replace('_', '')}"
            sort_clauses.append(f"{db_name} {sub_direction.upper()}")

        return sort_clauses, external_field_table_deps

    @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"
