
from bravaorm.utils.inflector import Inflector
from bravaorm.context.database import DataBase
import bravaorm.entity.datatype as datatype
from bravaorm.utils.log import *

import re

_ENTITY_PROPERTIES_CACHE = {}
_REGEX_TABLES_CACHE = {}
_REGEX_RELATIONSHIP = re.compile(r"(([a-z]|[A-Z]|[0-9]|\'|\*){1},{1}\ )")

class Connection():

    def __init__(self, db_user=None, db_password=None, db_host=None, db_port=None, db_database=None, db_ssl=False, db_ssl_ca=None, db_ssl_cert=None, db_ssl_key=None, db_charset='utf8', log_level='error', uncountable_words=None, irregular_words=None):
        try:

            Logging(log_level=log_level)

            self._inflector = Inflector(uncountable_words=uncountable_words, irregular_words=irregular_words)
            self._conn = DataBase(db_user, db_password, db_host, db_port,db_database, db_ssl, db_ssl_ca, db_ssl_cert, db_ssl_key, db_charset)

            self.__queue__ = {
                'add':[],
                'delete':[]
            }
        except Exception as e:
            raise e

    def __reset__(self):
        self._distinct = None
        self._select = None
        self._alias = None
        self._where = None
        self._orwhere = {}
        self._having = None
        self._orhaving = {}
        self._orderby = None
        self._groupby = None
        self._limit = None
        self._onjoin = None
        self._join = None
        self._inner = None
        self._include = None

    def __getattr__(self, name):
        try:
            if not name in self.__dir__():
                self._table = name
                self._class_name = self._inflector.classify(name)
                self._class = getattr(__import__(f'model.{self._class_name.lower()}', fromlist=[self._class_name]), self._class_name)
                self._klass = self._class()
                self._regex_replace_data = re.compile(rf"\S[a-zA-Z]+_{self._table}.[a-zA-Z]+_?[a-zA-Z]+ | {self._table}.[a-zA-Z]+_?[a-zA-Z]+")
                self.__reset__()
        except Exception as e:
            raise e
        return self

    def __get_entity_properties__(self, klass):
        class_name = klass.__class__.__name__
        if class_name not in _ENTITY_PROPERTIES_CACHE:
            _ENTITY_PROPERTIES_CACHE[class_name] = [field for field in klass.__dict__ if not field.startswith("_") and not klass[field].__class__.__name__.startswith("Obj")]
        return _ENTITY_PROPERTIES_CACHE[class_name]

    def __format_clause__(self, *clause):
        arr = []
        
        def split_safe(s, delimiter=','):
            result = []
            current = []
            in_parens = 0
            in_quote_single = False
            in_quote_double = False
            for char in s:
                if char == "(":
                    if not in_quote_single and not in_quote_double:
                        in_parens += 1
                elif char == ")":
                    if not in_quote_single and not in_quote_double:
                        in_parens -= 1
                elif char == "'":
                    if not in_quote_double:
                        in_quote_single = not in_quote_single
                elif char == '"':
                    if not in_quote_single:
                        in_quote_double = not in_quote_double
                elif char == delimiter:
                    if in_parens == 0 and not in_quote_single and not in_quote_double:
                        result.append("".join(current))
                        current = []
                        continue
                current.append(char)
            if current:
                result.append("".join(current))
            return result

        def PrependTable(condition):
            parts = re.split(r"('[^']*'|\"[^\"]*\"|%\([^\)]*\)s|`[^`]*`)", condition)
            keywords = {
                'AND', 'OR', 'IN', 'IS', 'NOT', 'NULL', 'LIKE', 'BETWEEN', 'REGEXP',
                'ASC', 'DESC', 'TRUE', 'FALSE', 'AS', 'ON', 'XOR', 'DIV', 'MOD'
            }
            pattern = re.compile(r'(?<!\.)\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*[\.\(\:])')
            
            def replacer(match):
                word = match.group(1)
                if word.isupper() or word.upper() in keywords:
                    return word
                return f"{self._table}.{word}"

            for i in range(0, len(parts), 2):
                parts[i] = pattern.sub(replacer, parts[i])
                
            return "".join(parts).strip()

        for c in clause:
            for s in split_safe(c):
                arr.append(PrependTable(s))

        return arr

    def __format_fields__(self, *fields):
        arr = []
        
        def split_safe(s, delimiter=','):
            result = []
            current = []
            in_parens = 0
            in_quote_single = False
            in_quote_double = False
            for char in s:
                if char == "(":
                    if not in_quote_single and not in_quote_double:
                        in_parens += 1
                elif char == ")":
                    if not in_quote_single and not in_quote_double:
                        in_parens -= 1
                elif char == "'":
                    if not in_quote_double:
                        in_quote_single = not in_quote_single
                elif char == '"':
                    if not in_quote_single:
                        in_quote_double = not in_quote_double
                elif char == delimiter:
                    if in_parens == 0 and not in_quote_single and not in_quote_double:
                        result.append("".join(current))
                        current = []
                        continue
                current.append(char)
            if current:
                result.append("".join(current))
            return result

        def PrependTable(condition):
            parts = re.split(r"('[^']*'|\"[^\"]*\"|%\([^\)]*\)s|`[^`]*`)", condition)
            keywords = {
                'AND', 'OR', 'IN', 'IS', 'NOT', 'NULL', 'LIKE', 'BETWEEN', 'REGEXP',
                'ASC', 'DESC', 'TRUE', 'FALSE', 'AS', 'ON', 'XOR', 'DIV', 'MOD', 
                'INTERVAL', 'MONTH', 'DAY', 'YEAR' , 'NOW', 'DATE_ADD', 'DATE_SUB', 
                'DATE_FORMAT', 'DATE', 'CURDATE', 'CURTIME', 'DATE_ADD', 'DATE_SUB',
                'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'
            }
            pattern = re.compile(r'(?<!\.)\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*[\.\(\:])')
            
            def replacer(match):
                word = match.group(1)
                if word.isupper() or word.upper() in keywords:
                    return word
                return f"{self._table}.{word}"

            for i in range(0, len(parts), 2):
                parts[i] = pattern.sub(replacer, parts[i])
                
            return "".join(parts).strip()

        for f in fields:
            for s in split_safe(f):
                arr.append(PrependTable(s))
        return arr

    def __valid_relationship__(self, table):
        if table in self._klass.__dict__ and self._klass[table].__class__.__name__.startswith("Obj"):
            return True
        else:
            raise Exception(f"{table} does not exist in relational list of {self._class_name}")

    def __format_relationship__(self, *tables):
        list_tables = []
        for table in tables:
            if len(_REGEX_RELATIONSHIP.findall(table)) > 0:
                for table_name in table.split(","):
                    if self.__valid_relationship__(table_name.strip()):
                        list_tables.append(table_name.strip())
            elif self.__valid_relationship__(table.strip()):
                list_tables.append(table.strip())
        return list_tables


    def __format_join_query__(self, joined, type):
        if self._select:
            _selects_stetaments = ', '.join([f"{c} AS '{c}'" for c in self._select if self.__is__join_select__(c)])
        else:
            _selects_stetaments = ''

        _joins_stetaments = []

        if joined:
            count = 0
            for join in joined:
                count += 1
                join_class = self._klass[join]()
                join_key = self._klass[join]

                if self._select:
                    if not all([f'{join}.{pk}' in _selects_stetaments for pk in join_class.__metadata__['pk']]):
                        _select_join_pks = ", ".join([f"{join}.{pk} AS '{join}.{pk}'" for pk in join_class.__metadata__['pk']])
                        _selects_stetaments = f"{_selects_stetaments}, {_select_join_pks}" if _selects_stetaments != '' else _select_join_pks
                else:
                    _select_join = ", ".join([f"{join}.{field} AS '{join}.{field}'" for field in self.__get_entity_properties__(join_class)])
                    _selects_stetaments = f"{_selects_stetaments}, {_select_join}" if _selects_stetaments != '' else _select_join


                # CODITIONS OF JOIN
                _coditions = []

                __conditions_join = [w for w in self._where if self.__is__join_or_include_codition__(join, w)] if self._where else []


                if self._klass[join].__class__.__name__ != "ObjListOfMany":
                    _base_join = f"({join}.{join_key.key} = {self._table}.{join_key.reference})"

                    if len(__conditions_join)>0:

                        # WHERE
                        _coditions.append(f"{_base_join.replace('()','').replace(')','')} AND {' AND '.join(__conditions_join)})")

                        # ORWHERE
                        _coditions.extend([c for c in [f" OR ( {_base_join.replace('(','').replace(')','')} AND {' AND '.join([w for w in conditions if self.__is__join_or_include_codition__(join, w)])})" for block, conditions in self._orwhere.items()] if c.strip() != 'OR ()'] if self._orwhere else [])

                    else:
                        _coditions.append(_base_join)


                    _joins_stetaments.append(f"{type} JOIN {join_key.table} AS {join} ON ({' '.join(_coditions)})")

                else:
                    # N TO N
                    _base_join = f"{join_key.keyname}.{join_key.key} = inter_{count}_{join_key.intermediate}.{join_key.rel_key}"

                    if len(__conditions_join)>0:
                        # WHERE
                        _coditions.append(f"{_base_join} AND {' AND '.join(__conditions_join)}")

                        # ORWHERE
                        _coditions.extend([c for c in [f" OR {_base_join} AND {' AND '.join([w for w in conditions if self.__is__join_or_include_codition__(join, w)])}" for block, conditions in self._orwhere.items()] if c.strip() != 'OR'] if self._orwhere else [])

                    else:

                        _coditions.append(_base_join)


                    _joins_stetaments.append(f"{type} JOIN {join_key.intermediate} AS inter_{count}_{join_key.intermediate} ON  inter_{count}_{join_key.intermediate}.{join_key.ref_key} = {self._table}.{join_key.reference} {type} JOIN {join_key.table} AS {join_key.keyname} ON {' '.join(_coditions)}")


        return _selects_stetaments, " ".join(_joins_stetaments)

    def __format_include_query__(self, obj_or_list, include):
        _include_key = self._klass[include]
        _include_class = self._klass[include]()
        
        args = {}

        # Check se estamos lidando com uma lista de objetos (para Bulk Eager Loading com IN)
        is_bulk = isinstance(obj_or_list, list)
        
        if is_bulk:
            ref_values = [str(o[_include_key.reference].value) for o in obj_or_list if o[_include_key.reference].value]
            if not ref_values: return None
        else:
            if not obj_or_list[_include_key.reference].value: return None

        if self._select:
            _include_select = ", ".join([f"{c.replace(f'{include}.', '')} AS '{c.replace(f'{include}.', '')}'" for c in self._select if self.__is__include_select__(include, c)])
            if not all([f"'{pk}'" in _include_select for pk in _include_class.__metadata__['pk']]):
                _pk_select = ", ".join([f"{pk} AS '{pk}'" for pk in _include_class.__metadata__['pk']])
                _include_select = f"{_include_select}, {_pk_select}" if _include_select else _pk_select
        else:
            _include_select = ", ".join([f"{field} AS '{field}'" for field in self.__get_entity_properties__(_include_class)])

        if is_bulk:
            param_keys = []
            for i, ref_val in enumerate(ref_values):
                param_key = f"__include_bulk_{i}"
                args[param_key] = ref_val
                param_keys.append(f"%({param_key})s")
            _val_str = ",".join(param_keys)
            _default_condition = f"{include}.{_include_key.key} IN ({_val_str})"
        else:
            param_key = f"__include_default_{_include_key.reference}"
            _default_condition = f"{include}.{_include_key.key} = %({param_key})s"
            args[param_key] = obj_or_list[_include_key.reference].value

        __conditions_include = " AND ".join([w for w in self._where if self.__is__join_or_include_codition__(include, w)]) if self._where else ""

        _where = []
        if len(__conditions_include)>0:
            _where.append(f"( {_default_condition} AND {__conditions_include} )")
            _orwhere = '' if len(self._orwhere) == 0 else ' OR '.join([f"( {_default_condition} AND {' AND '.join([w for w in conditions if self.__is__join_or_include_codition__(include, w)])} )" for block, conditions in self._orwhere.items()])
            if _orwhere != '':
                _where.append(_orwhere)
        else:
            _where.append(_default_condition)

        if _include_key.__class__.__name__ != "ObjListOfMany":
            stetament = f"SELECT {_include_select} FROM {include} WHERE {' OR '.join(_where)}"
            if not is_bulk:
                match = self._regex_replace_data.findall(stetament)
                for value in match:
                    if value.strip().startswith(self._table):
                        if '.' in value:
                            key = value.strip().split(".")[1]
                            # Use param placeholder
                            param_key = f"__include_{key}"
                            stetament = stetament.replace(value, f"%({param_key})s")
                            args[param_key] = obj_or_list[key].value
        else:
            _where = ""
            __conditions_include = " AND ".join([w for w in self._where if self.__is__join_or_include_codition__(include, w)]) if self._where else ""
            if len(__conditions_include)>0:
                _where = f"WHERE ({__conditions_include})"
                _orwhere = '' if len(self._orwhere) == 0 else ' OR '.join([f"({' AND '.join([w for w in conditions if self.__is__join_or_include_codition__(include, w)])} )" for block, conditions in self._orwhere.items()])
                if len(_orwhere)>0:
                    _where = f"{_where} OR {_orwhere}" # Corrigido typo "t{where}" original
                    
            if is_bulk:
                # We do not use parameterization for dynamic IN queries as easily right now, but for safety in the future we should.
                # Since bulk ids are ints typically, the risk is lower but we will param it.
                param_keys = []
                for i, ref_val in enumerate(ref_values):
                    param_key = f"__include_bulk_n2n_{i}"
                    args[param_key] = ref_val
                    param_keys.append(f"%({param_key})s")
                _val_str = ",".join(param_keys)
                join_condition = f"inter_{_include_key.intermediate}.{_include_key.ref_key} IN ({_val_str})"
            else:
                param_key = f"__include_n2n_{_include_key.reference}"
                join_condition = f"inter_{_include_key.intermediate}.{_include_key.ref_key} = %({param_key})s"
                args[param_key] = obj_or_list[_include_key.reference].value

            stetament = f"SELECT inter_{_include_key.intermediate}.{_include_key.ref_key} as '__bravaorm_parent_id__', {_include_select} FROM {_include_key.table} {include} INNER JOIN {_include_key.intermediate} inter_{_include_key.intermediate} ON (inter_{_include_key.intermediate}.{_include_key.rel_key} = {include}.{_include_key.key} AND {join_condition}) {_where}"
        return stetament, args


    def __is_join__(self, table_name):
        return (not self._join is None and table_name in self._join) or (not self._inner is None and table_name in self._inner)

    def __is_include__(self, table_name):
        return not self._include is None and table_name in self._include

    def __is__main_select__(self, field):
        # field might be e.g. "table.id" or "COUNT(table.id)"
        # We need to test if it belongs to a join or include
        # Since we ran PrependTable, we check if it started with self._table originally or not.
        # Actually, self._table is the main select.
        if '.' in field:
            table_name = field.split('.')[0].strip()
            # If it explicitly references a join or include, then it's not main
            # Wait, if field is COUNT(table.id), split('.')[0] is COUNT(table. That is not in join or include.
            return not self.__is_join__(table_name) and not self.__is_include__(table_name)
        else:
            return True

    def __is__join_select__(self, field):
        if '.' in field:
            table_name = field.split('.')[0].strip()
            return self.__is_join__(table_name)
        else:
            return False

    def __is__include_select__(self, include, field):
        if '.' in field:
            table_name = field.split('.')[0].strip()
            return table_name == include
        else:
            return False


    def __is__main_codition__(self, condition):
        if not self._join and not self._inner and not self._include:
            return True
        else:
            if not '.' in condition:
                return True
            else:
                is_include = any([self.__is__join_or_include_codition__(include, condition) for include in self._include]) if self._include else False
                return not is_include

    def __get_table_regex__(self, table):
        if table not in _REGEX_TABLES_CACHE:
            _REGEX_TABLES_CACHE[table] = re.compile(r"\w+"+ table + r"\.")
        return _REGEX_TABLES_CACHE[table]

    def __is__join_or_include_codition__(self, table, condition):
        if not '.' in condition:
            return False
        else:
            return f'{table}.' in condition and not self.__get_table_regex__(table).match(condition)

    def __expand_wildcards__(self, fields):
        if not fields:
            return fields
        
        expanded = []
        for f in fields:
            if f == '*' or f.endswith('.*'):
                if f == '*':
                    table_name = self._table
                else:
                    table_name = f[:-2]
                
                if table_name == self._table:
                    expanded.extend([f"{self._table}.{prop}" for prop in self.__get_entity_properties__(self._klass)])
                else:
                    # check joins/includes
                    try:
                        if self.__is_join__(table_name) or self.__is_include__(table_name):
                             klass_instance = self._klass[table_name]()
                             expanded.extend([f"{table_name}.{prop}" for prop in self.__get_entity_properties__(klass_instance)])
                        else:
                             expanded.append(f)
                    except:
                        expanded.append(f)
            else:
                expanded.append(f)
        return expanded

    def __format_select_query__(self):

        # SELECT BLOCK
        if self._select:
            self._select = self.__expand_wildcards__(self._select)
            _select = ', '.join([f"{c} AS '{c.replace(self._table+'.', '')}'" for c in self._select if self.__is__main_select__(c)])
            # IF NOT PRIMARY KEY ON SELECT, APPEND IT
            if not all([f'{self._table}.{pk}' in _select for pk in self._klass.__metadata__['pk']]):
                _select_pk = ', '.join([f"{self._table}.{pk} AS '{pk}'" for pk in self._klass.__metadata__['pk']])
                _select = f"{_select}, {_select_pk}" if _select != '' else _select_pk
        else:
            _select = ", ".join([f"{self._table}.{field} AS '{field}'" for field in self.__get_entity_properties__(self._klass)])


        _distinct = f'DISTINCT({",".join(self._distinct)})' if self._distinct else None

        _select = f"{_select}, {_distinct}" if _distinct else f"{_select}"

        _alias = ", ".join([f"{alias[0]} AS '{alias[1]}'" for alias in self._alias]) if self._alias else None

        _select = f"{_select}, {_alias}" if _alias else f"{_select}"

        _select_join, _join = self.__format_join_query__(self._join, 'LEFT')
        _select_join = f', {_select_join}' if _select_join != '' else ''

        _select_inner, _inner = self.__format_join_query__(self._inner, 'INNER')
        _select_inner = f', {_select_inner}' if _select_inner != '' else ''

        _onjoin =  " ".join(self._onjoin) if self._onjoin else ''

        # CONDITION BLOCK
        _where = [w for w in self._where if self.__is__main_codition__(w)] if self._where else []
        _where = f" WHERE ({' AND '.join(_where)})" if len(_where) > 0 else ''
        _orwhere = ''.join([c for c in [f" OR ({' AND '.join([w for w in conditions if self.__is__main_codition__(w)])})" for block, conditions in self._orwhere.items()] if c.strip() != 'OR ()']) if self._orwhere else ''

        # GROUP BLOCK
        _groupby =  '' if not self._groupby else f" GROUP BY ({self._groupby})"

        # HAVING BLOCK
        _having = '' if not self._having else f" HAVING ({' AND '.join(self._having)})"
        _orhaving = '' if len(self._orhaving) == 0 else  ''.join([f" OR ({' AND '.join([w for w in conditions])})" for block, conditions in self._orhaving.items()])

        # ORDER BLOCK
        _orderby = '' if not self._orderby else f" ORDER BY {','.join(self._orderby)}"

        # LIMIT BLOCK
        _limit = '' if not self._limit else f" LIMIT {self._limit[0]},{self._limit[1]}"

        _stetament =  f"SELECT {_select} {_select_join} {_select_inner} FROM {self._table} {_join} {_inner} {_onjoin} {_where}{_orwhere}{_groupby}{_having}{_orhaving}{_orderby}{_limit}"

        return  _stetament


    def __format_count_query__(self):

        # SELECT BLOCK
        if self._select:
            self._select = self.__expand_wildcards__(self._select)
            _select = 'COUNT(*)' if not self._select else f"COUNT({', '.join(self._select)})"
        else:
            _select = 'COUNT(*)'

        _distinct = f'DISTINCT({",".join(self._distinct)})' if self._distinct else None

        _select = f"{_select}, {_distinct}" if _distinct else f"{_select}"

        _alias = ", ".join([f"{alias[0]} AS '{alias[1]}'" for alias in self._alias]) if self._alias else None

        _select = f"{_select}, {_alias}" if _alias else f"{_select}"

        _select_join, _join = self.__format_join_query__(self._join, 'LEFT')
        _select_join = f', {_select_join}' if _select_join != '' else ''

        _select_inner, _inner = self.__format_join_query__(self._inner, 'INNER')
        _select_inner = f', {_select_inner}' if _select_inner != '' else ''

        _onjoin =  " ".join(self._onjoin) if self._onjoin else ''

        # CONDITION BLOCK
        _where = [w for w in self._where if self.__is__main_codition__(w)] if self._where else []
        _where = f" WHERE ({' AND '.join(_where)})" if len(_where) > 0 else ''
        _orwhere = ''.join([c for c in [f" OR ({' AND '.join([w for w in conditions if self.__is__main_codition__(w)])})" for block, conditions in self._orwhere.items()] if c.strip() != 'OR ()']) if self._orwhere else ''

        # GROUP BLOCK
        _groupby =  '' if not self._groupby else f" GROUP BY ({self._groupby})"

        # HAVING BLOCK
        _having = '' if not self._having else f" AND ({' AND '.join(self._having)})"
        _orhaving = '' if len(self._orhaving) == 0 else  ''.join([f" OR ({' AND '.join([w for w in conditions])})" for block, conditions in self._orhaving.items()])

        # ORDER BLOCK
        _orderby = '' if not self._orderby else f" ORDER BY {','.join(self._orderby)}"

        # LIMIT BLOCK
        _limit = '' if not self._limit else f" LIMIT {self._limit[0]},{self._limit[1]}"

        _stetament =  f"SELECT {_select} FROM {self._table} {_join} {_inner} {_onjoin} {_where}{_orwhere}{_groupby}{_having}{_orhaving}{_orderby}{_limit}"

        return  _stetament

    def __format_object_delete_query__(self, obj):
        if all([ key in obj.__metadata__['data'] for key in obj.__metadata__['pk']]):
            keys = [f"{key}='{obj.__metadata__['data'][key]}'" for key in obj.__metadata__['pk']]
            table =  self._inflector.tableize(obj.__class__.__name__)
            return f"DELETE FROM {table} WHERE { ' AND '.join(keys) }"
        else:
            raise Exception(f"Can't delete object {obj.__class__.__name__} whitout primary key defined")

    def __format_delete_query__(self):
        _where = [w for w in self._where if self.__is__main_codition__(w)]
        return f"DELETE FROM {self._table} WHERE ({' AND '.join(_where)})"

    def __format_object_insert_query__(self, obj):
        table =  self._inflector.tableize(obj.__class__.__name__)
        keys_list = [key for key, value in obj.__metadata__['data'].items() if value is not None]
        keys = ', '.join(keys_list)
        values = ', '.join([f'%({key})s' for key in keys_list])
        on_keys = ', '.join([f"{key}=VALUES({key})" for key in keys_list])
        return f"INSERT INTO {table} ({keys}) VALUES ({values}) ON DUPLICATE KEY UPDATE {on_keys}"

    def select(self, *fields):
        self._select = self.__format_fields__(*fields)
        return self

    def distinct(self, *fields):
        self._distinct = self.__format_fields__(*fields)
        return self

    def alias(self, condition, name):
        if isinstance(name, str) and isinstance(condition, str):
            if "." in name and "." not in condition:
                condition, name = name, condition
                
        # Format the condition safely just in case it's a bare column
        formatted_condition = self.__format_fields__(condition)[0] if condition else condition

        if not self._alias:
            self._alias = [(formatted_condition, name)]
        else:
            self._alias.append((formatted_condition, name))
        return self

    def where(self, *clause):
        self._where = self.__format_clause__(*clause)
        return self

    def orwhere(self, *clause):
        if not self._where:
            raise Exception("where('condition') must be used befere orwhere('condition')")
        self._orwhere[len(self._orwhere)] = self.__format_clause__(*clause)
        return self

    def groupby(self, *fields):
        # Allow multiple fields just like other clauses
        self._groupby = ', '.join(self.__format_fields__(*fields))
        return self

    def having(self, *clause):
        self._having = self.__format_clause__(*clause)
        return self

    def orhaving(self, *clause):
        if not self._having:
            raise Exception("having('condition') must be used befere orhaving('condition')")
        self._orhaving[len(self._orhaving)] = self.__format_clause__(*clause)
        return self

    def orderby(self, *fields):
        self._orderby = self.__format_clause__(*fields)
        return self

    def limit(self, start, end):
        self._limit = (start, end)
        return self
        
    def on(self, select, jointype, table, condition):
        if "," in select:
            expression = select.split(',')
            for exp in expression:
                self.alias(exp, exp.split('.')[-1])
        elif not "," in select:
            self.alias(select, select.split('.')[-1])

        if self._onjoin is None:
            self._onjoin = []

        self._onjoin.append(f"{jointype.upper()} JOIN {table} on {condition}")
        return self

    def join(self, *tables):
        self._join = self.__format_relationship__(*tables)
        return self

    def inner(self, *tables):
        self._inner = self.__format_relationship__(*tables)
        return self

    def include(self, *tables):
        self._include = self.__format_relationship__(*tables)
        return self

    def execute(self, query, args=None, class_name=None):
        result = self._conn.fetchall(query, args)
        if class_name:
            _class = getattr(__import__(f'model.{class_name.lower()}', fromlist=[class_name]), class_name)
            _klass = _class()
            result = datatype.ListType(_class).add([_class(_connection=self, **item) for item in result])
        return result

    @property
    def fetch(self):
        query = self.__format_select_query__()
        return self._conn.fetchall(query)

    @property
    def all(self):
        Debug("Formating Query")
        query = self.__format_select_query__()
        result = self._conn.fetchall(query)
        result = datatype.ListType(self._class).add([self._class(aliases=self._alias, _connection=self, **item) for item in result])
        
        if self._include and len(result) > 0:
            for _include in self._include:
                # Eagerly load all relationships using IN clauses to avoid N+1 queries
                _stetament_include, _include_args = self.__format_include_query__(result, _include)
                if _stetament_include:
                    _include_result = self._conn.fetchall(_stetament_include, _include_args)
                    
                    # Group results by parent reference key in-memory
                    grouped_results = {}
                    _include_key = self._klass[_include]
                    
                    # If ObjListOfMany, the parent id is piped out as __bravaorm_parent_id__ from the query format
                    _group_key = '__bravaorm_parent_id__' if _include_key.__class__.__name__ == "ObjListOfMany" else _include_key.key

                    for child in _include_result:
                        parent_id = child.get(_group_key)
                        if _group_key == '__bravaorm_parent_id__':
                            # Drop the helper column so it doesn't pollute hydration
                            del child[_group_key]
                        
                        if parent_id not in grouped_results:
                            grouped_results[parent_id] = []
                        grouped_results[parent_id].append(child)
                    
                    for item in result:
                        item[_include].__reset__()
                        item.__metadata__['relasionships'][_include] = None if not 'List' in item[_include].__class__.__name__ else []
                        parent_val = item[_include_key.reference].value
                        if parent_val in grouped_results:
                            item.add(_include, grouped_results[parent_val])
                            
            for item in result:
                item.__metadata__['status'] = 'loaded'
        return result

    @property
    def first(self):
        Debug("Formating Query")
        self.limit(0,1)
        query = self.__format_select_query__()
        result = self._conn.fetchall(query)

        item = self._class(aliases=self._alias, _connection=self, **result[0]) if result else None

        if item and self._include:
            for _include in self._include:
                item[_include].__reset__()
                item.__metadata__['relasionships'][_include] = None if not 'List' in item[_include].__class__.__name__ else []
                _stetament_include, _include_args = self.__format_include_query__(item, _include)
                if _stetament_include:
                    _include_result = self._conn.fetchall(_stetament_include, _include_args)
                    item.add(_include, _include_result)
        if item:
            item.__metadata__['status'] = 'loaded'

        return item


    @property
    def count(self):
        Debug("Formating Query")
        query = self.__format_count_query__()
        result = self._conn.fetchall(query)
        return result[0]["COUNT(*)"] if len(result) > 0  else 0

    def __commit__(self, obj, action):
        [ v.remove(obj) for k, v in self.__queue__.items() if k != action and obj in v]
        if obj and  obj.__class__.__module__.startswith("model."):
            if not obj in self.__queue__[action]:
                self.__queue__[action].append(obj)
            obj.__metadata__['connection'] = self
        return self

    def add(self, obj):
        return self.__commit__(obj, 'add')

    def delete(self, obj=None):
        if obj:
            self.__commit__(obj, 'delete')
            obj.__metadata__['status'] = 'deleted'
            return obj
        else:
            if self._where:
                self._conn.save(self.__format_delete_query__(), {})
                self._conn.commit()
                return None
            else:
                raise Exception("delete method requires where condition or object")

    def __save__object__(self, obj_list, _processed=None):
        if _processed is None:
            _processed = set()
            
        Debug("Saving Objects (Bulk)")
        
        items_to_process = []
        for obj in obj_list:
            if id(obj) not in _processed:
                _processed.add(id(obj))
                items_to_process.append(obj)
                
        if not items_to_process:
            return
            
        # Pre-process 'Obj' relationships to link their IDs
        for obj in items_to_process:
            if len(obj.__metadata__['relasionships']) > 0:
                for rel in list(obj.__metadata__['relasionships']):
                    if obj[rel].__class__.__name__ == 'Obj':
                        _rel_obj = obj[rel].value
                        if _rel_obj:
                            _reference = obj[rel].reference
                            _rel_pk = _rel_obj.__metadata__['pk'][0]
                            _rel_pk_value = getattr(_rel_obj, _rel_pk)
                            if _rel_pk_value is not None:
                                setattr(obj, _reference, _rel_pk_value)

        # Group objects by table type and key signature for bulk insertion
        grouped_inserts = {}
        for obj in items_to_process:
            if obj.__metadata__.get('status') not in ('created', 'modified'):
                continue
            table = self._inflector.tableize(obj.__class__.__name__)
            keys_tuple = tuple(sorted([key for key, value in obj.__metadata__['data'].items() if value is not None]))
            group_key = f"{table}_{'-'.join(keys_tuple)}"
            if group_key not in grouped_inserts:
                grouped_inserts[group_key] = {'table': table, 'items': [], 'keys_list': keys_tuple}
            grouped_inserts[group_key]['items'].append(obj)
            
        for group_key, group_data in grouped_inserts.items():
            table = group_data['table']
            items = group_data['items']
            keys_list = group_data['keys_list']
            if not items: continue
            
            # Use the first item to reference primary key definitions
            _sample_obj = items[0]
            
            _pk_key = _sample_obj.__metadata__['pk'][0]
            keys = ', '.join(keys_list)
            values = ', '.join([f'%({key})s' for key in keys_list])
            on_keys = ', '.join([f"{key}=VALUES({key})" for key in keys_list])
            
            _bulk_query = f"INSERT INTO {table} ({keys}) VALUES ({values}) ON DUPLICATE KEY UPDATE {on_keys}"
            
            # Map values
            data_list = []
            for _obj in items:
                data_list.append({key:value for key, value in _obj.__metadata__['data'].items() if value is not None})
                
            # Execute Bulk Insert
            _last_row_id = self._conn.save_many(_bulk_query, data_list)

            # Update Metadata states
            current_id = _last_row_id
            for _obj in items:
                _pk_value = _obj.__getattribute__(_pk_key)
                status_obj = 'inserted'
                # MySQL returns the first generated ID of a bulk insert. Following inserts are contiguous.
                if _pk_value is not None and ( (isinstance(_pk_value, int) and _pk_value > 0) or (isinstance(_pk_value, str) and _pk_value.strip() != "")):
                    status_obj = 'updated'
                else:
                    # Somente atribui sequenciação Auto-Increment para tabelas numéricas válidas retornadas pelo MySQL
                    if current_id and isinstance(current_id, int):
                        _obj.__setattr__(_pk_key, current_id)
                        current_id += 1
                _obj.__metadata__['status'] = status_obj

        # Process relationships using original algorithm for safety, but could be grouped later
        def do_relacional(_rel, _obj, _sub_item):
            Debug("Making Relasionship")
            _reference = _obj[rel].reference
            _ref_key = _obj[rel].ref_key
            _rel_key = _obj[rel].rel_key
            _key = _obj[rel].key
            _relacional_query = f"INSERT INTO {_obj[rel].intermediate} ({_ref_key}, {_rel_key}) VALUES (%({_ref_key})s, %({_rel_key})s) ON DUPLICATE KEY UPDATE {_ref_key}=VALUES({_ref_key}), {_rel_key}=VALUES({_rel_key})"
            _relacional_data = {
                _ref_key : _obj[_reference].value,
                _rel_key : _sub_item[_key].value
            }
            self._conn.save(_relacional_query, _relacional_data)

        # Loop to process relationships
        for obj in items_to_process:
            if len(obj.__metadata__['relasionships'] )> 0:
                for rel in obj.__metadata__['relasionships']:
                    if obj[rel].__class__.__name__ == 'ObjList':
                        for sub_item in obj[rel].value:
                            sub_item.__setattr__(obj[rel].key, obj[obj[rel].reference].value)
                            if sub_item.__metadata__.get('status') in ('created', 'modified'):
                                self.__save__object__([sub_item], _processed=_processed) # Recurse individually for safety on nested relationships
                    if obj[rel].__class__.__name__ == 'ObjListOfMany':
                        for sub_item in obj[rel].value:
                            do_relacional(rel, obj, sub_item)


    def set(self, field, value):
        _where = [w for w in self._where if self.__is__main_codition__(w)] if self._where else []
        _where = f" WHERE ({' AND '.join(_where)})" if len(_where) > 0 else ''
        _orwhere = ''.join([c for c in [f" OR ({' AND '.join([w for w in conditions if self.__is__main_codition__(w)])})" for block, conditions in self._orwhere.items()] if c.strip() != 'OR ()']) if self._orwhere else ''
        query = f"UPDATE {self._table} SET {field}=%(update_{field})s {_where}{_orwhere}"
        self._conn.save(query, {f"update_{field}": value})
        self._conn.commit()

    def update(self, **kwargs):
        _where = [w for w in self._where if self.__is__main_codition__(w)] if self._where else []
        _where = f" WHERE ({' AND '.join(_where)})" if len(_where) > 0 else ''
        _orwhere = ''.join([c for c in [f" OR ({' AND '.join([w for w in conditions if self.__is__main_codition__(w)])})" for block, conditions in self._orwhere.items()] if c.strip() != 'OR ()']) if self._orwhere else ''
        
        update_clauses = []
        update_data = {}
        for k, v in kwargs.items():
            param_key = f"update_{k}"
            update_clauses.append(f"{k}=%({param_key})s")
            update_data[param_key] = v
            
        update = ",".join(update_clauses)
        query = f"UPDATE {self._table} SET {update} {_where}{_orwhere}"
        self._conn.save(query, update_data)
        self._conn.commit()

    def save(self):

        # DELETE OBJECTS
        Debug("Process Delete Queue")
        for obj_delete in self.__queue__['delete']:
            _delete_query = self.__format_object_delete_query__(obj_delete)
            self._conn.save(_delete_query, {})
            obj_delete.__metadata__['status'] = 'deleted'
            #obj_delete.__init__()
        self.__queue__['delete'] = []

        # ADD/UPDATE OBJECTS
        Debug("Process Insert/Update Queue")
        if self.__queue__['add']:
            # Send the entire list to be batched by __save__object__
            self.__save__object__(self.__queue__['add'])
        self.__queue__['add'] = []

        self._conn.commit()

    def close(self):
        self._conn.close()
