# pylint: disable=missing-function-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=line-too-long
# pylint: disable=unused-import


# from typing import Iterable, OrderedDict, Union
# from collections import OrderedDict
import re
import sqlite3
import sys
from typing import Iterable
import mysql.connector as _mysqlConnnector
from mysql.connector.errors import ProgrammingError
import traceback as _traceback
import colemen_utils as c
import volent.settings as _settings
import volent.settings.types as _t
import volent.exceptions as _e
from volent.mixins.MySQLGeneratorMixin import MySQLGeneratorMixin as _MySQLGeneratorMixin

_log = c.con.log


class DatabaseConnection(_MySQLGeneratorMixin):
    '''Class used as a mixin to allow database connections and the general CRUD procedures.'''
    creds = None
    connection = None
    cursor = None


    def __init__(self):
        self.creds = {}


    def get_databases_with_creds(self)->Iterable[str]:
        '''Retrieve a list of database names that have credentials in the environment.'''
        dbs = []
        for db in self.database_names:
            db_creds = c.build.get_environ(db,None)
            if db_creds is None:
                c.con.log(f"Failed to locate credentials for database: {db}","error")
                c.con.log(f"Add credentials to the environment in order for a connection to be established.","error")
                c.con.log('Example: tmp_creds = {    "database": "database_name",    "user": "username",    "password": "ps",    "host": "162.850.216.128"}',"error")
                continue
            else:
                dbs.append(db)
        return dbs



    def connect(self, db_creds:dict=None):
        '''
            Sets up the database connection with the initial settings.

            If the DB_CREDENTIALS are provided, it attempts to connect to a mysql database.

            ----------

            Keyword Arguments
            -----------------
            `DB_PATH` {string}
                The filepath to the sqlite database

            [`create`=True] {bool}
                If True and SQLite database does not exist yet, create the file.

            `DB_CREDENTIALS` {dict}
                The credentials to connect to the mysql database
                {
                    "user":"string",
                    "password":"string",
                    "host":"string",
                    "database":"string"
                }

            Return {bool}
            ----------
                True upon success, false otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 04-19-2021 08:04:18
            `version`: 1.0
        '''
        connect_success = False
        from volent.Database import Database as _Database
        if isinstance(self,_Database):
            db_creds = c.build.get_environ(self.name,None)
            if db_creds is None:
                c.con.log(f"Failed to locate credentials for database: {db}","error")
            else:
                if len(db_creds.keys()) == 1:
                    connect_success = self.__connect_to_sqlite(db_creds)
                    if connect_success:
                        c.con.log(f"Successfully connected to {db_creds['database']}","green")
                    return connect_success
                connect_success = self.__connect_to_my_sqldb(db_creds)
                if connect_success:
                    # _traceback.print_stack()
                    c.con.log(f"Successfully connected to {db_creds['database']}","green")
                    # connect_success = True
                    return True

        else:
            from volent.Volent import Volent as _bv
            if isinstance(self,_bv):
                for db in self.databases:
                    result = db.connect()
                    if result is False:
                        c.con.log(f"Failed to connect to {db.name}","warning")
                        connect_success = False
                        return False
                return True




        # if _settings.globe.flavor == "mysql":
        #     if db_creds is None:
        #         db_creds = self.credentials()
        #     if isinstance(db_creds,(dict)):
        #         connect_success = self.__connect_to_my_sqldb(db_creds)
        #         if connect_success:
        #             c.con.log(f"Successfully connected to {db_creds['database']}","green")

        return connect_success

    def __validate_db_credentials(self,creds:dict):
        '''
                Validates that all of the db_credentials are provided.

                ----------

                Return {bool}
                ----------------------
                True upon success, false otherwise.

                Meta
                ----------
                `author`: Colemen Atwood
                `created`: 04-19-2021 08:23:40
                `memberOf`: colemen_database
                `version`: 1.0
                `method_name`: __validate_db_credentials
        '''
        # if 'db_credentials' in self.data:
        error_array = []
        # creds = self.data['db_credentials']
        if 'user' not in creds:
            error_array.append('user is not provided in db_credentials')
        if 'password' not in creds:
            error_array.append('password is not provided in db_credentials')
        if 'host' not in creds:
            error_array.append('host is not provided in db_credentials')
        if 'database' not in creds:
            error_array.append('database is not provided in db_credentials')
        if len(error_array) == 0:
            # print("Successfully validated db_credentials")
            return True
        return False

        print("Credentials are needed to connect to the Mysql Database.")
        return False

    def __connect_to_my_sqldb(self,creds:dict):
        '''
            Attempts to connect to a mysql database.

            ----------

            Return {bool}
            ----------------------
            True upon success, false otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 04-19-2021 08:23:40
            `memberOf`: colemen_database
            `version`: 1.0
            `method_name`: __connect_to_my_sqldb
        '''

        connect_success = False
        if self.connection is not None:
            return True

        if self.__validate_db_credentials(creds) is True:
            _settings.globe.flavor = "mysql"
            self.connection = None
            try:

                self.connection = _mysqlConnnector.connect(
                    user=creds['user'],
                    password=creds['password'],
                    host=creds['host'],
                    database=creds['database']
                )
                self.cursor = self.connection.cursor(
                    buffered=True,
                    dictionary=True
                )

                if self.connection.is_connected():
                    # print("Successfully connected to mysql database")
                    connect_success = True

            except ProgrammingError as error:
                c.con.log(error.msg,"red")

        return connect_success


    def __connect_to_sqlite(self,creds:dict):
        '''
            Attempts to connect to a mysql database.

            ----------

            Return {bool}
            ----------------------
            True upon success, false otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 04-19-2021 08:23:40
            `memberOf`: colemen_database
            `version`: 1.0
            `method_name`: __connect_to_my_sqldb
        '''

        connect_success = False
        if self.connection is not None:
            return True
        _settings.globe.flavor = "sqlite"
        db_name = creds['database']
        file_name = f"{db_name}.db"
        self.connection = sqlite3.connect(file_name)
        self.cursor = self.connection.cursor()

        if c.file.exists(file_name):
            connect_success = True

        return connect_success



    @property
    def is_connected(self):
        if self.connection is not None:
            return True
        return False

    def run(self, sql:str, args=False,foreign_key_checks=True):
        '''
            Executes a query on the database.

            ----------

            Arguments
            -------------------------
            `sql` {string}
                The sql query to execute.

            `args` {list}
                A list of arguments to apply to the sql query

            [`foreign_key_checks`=True] {bool}
                If False, the database will be instructed to skip foreign_key_checks.

            Return {bool}
            ----------------------
            True upon success, false otherwise.

            if multiple statements are provided it will return True if ALL statements execute successfully.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 04-19-2021 10:07:54
            `memberOf`: colemen_database
            `version`: 1.0
            `method_name`: run
        '''

        # if foreign_key_checks is False:
            # self.execute_single_statement("SET FOREIGN_KEY_CHECKS = 0;")
            # sql = f"SET FOREIGN_KEY_CHECKS = 0;\n{sql}\nSET FOREIGN_KEY_CHECKS = 0;"
        statements = sql
        success = False
        # print(f"executing: {sql}")
        # if the sql is a string, split it into a list of statements
        if isinstance(sql, (str)):
            statements = _to_statement_list(sql)
        # print(f"statements:{statements}")
        if len(statements) > 1:
            # print(f"Multiple statements [{len(statements)}] found in sql.")
            success = True
            for statement in statements:
                res = self.execute_single_statement(statement, args,foreign_key_checks=foreign_key_checks)
                if res is False:
                    success = False


        if len(statements) == 1:
            return self.execute_single_statement(sql, args,foreign_key_checks=foreign_key_checks)
        return success


    def run_select(self,sql:str,args=False,default=None,limit=None,offset=None):
        '''
            Execute a select query on the database.

            ----------

            Arguments
            -------------------------
            `sql` {str}
                The Select query to execute.

            `args` {list,dict}
                The arguments to use in parameterized placeholders

            Keyword Arguments
            -------------------------
            [`default`=None] {any}
                The default value to return in the event of an error.

            [`limit`=None] {int}
                The maximum number of results to return

            [`offset`=None] {int}
                The index offset to apply to the query.

            Return {any}
            ----------------------
            The results of the query if successful, the default value otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 12-09-2022 11:12:16
            `memberOf`: MySQLDatabase
            `version`: 1.0
            `method_name`: run_select
            * @xxx [12-09-2022 11:15:35]: documentation for run_select
        '''
        # default = _obj.get_kwarg(['default'],None,None,**kwargs)
        # limit = _obj.get_kwarg(['limit'],None,(int),**kwargs)
        # offset = _obj.get_kwarg(['offset'],None,(int),**kwargs)

        sql = _paginate_select_query(sql,limit,offset)
        if isinstance(args,(dict)):
            sql = _format_query_params(sql,args)
        # print(f"sql:{sql}")
        if self.is_connected is False:
            c.con.log("Not Connected to a database dork.","warning")
        # if self.connect() is False:
            return default

        # _log(f"sql:{sql}","cyan")
        # _log(f"args:{args}","cyan")

        if self.run(sql,args):
            return self.fetchall()
        return default


    def run_select_internal(self,sql:str,select:_t.query_type,args=False,default=None,limit=None,offset=None):
        '''
            Execute a select query on the database.

            ----------

            Arguments
            -------------------------
            `sql` {str}
                The Select query to execute.

            `args` {list,dict}
                The arguments to use in parameterized placeholders

            Keyword Arguments
            -------------------------
            [`default`=None] {any}
                The default value to return in the event of an error.

            [`limit`=None] {int}
                The maximum number of results to return

            [`offset`=None] {int}
                The index offset to apply to the query.

            Return {any}
            ----------------------
            The results of the query if successful, the default value otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 12-09-2022 11:12:16
            `memberOf`: MySQLDatabase
            `version`: 1.0
            `method_name`: run_select
            * @xxx [12-09-2022 11:15:35]: documentation for run_select
        '''
        # default = _obj.get_kwarg(['default'],None,None,**kwargs)
        # limit = _obj.get_kwarg(['limit'],None,(int),**kwargs)
        # offset = _obj.get_kwarg(['offset'],None,(int),**kwargs)

        sql = _paginate_select_query(sql,limit,offset)
        if isinstance(args,(dict)):
            sql = _format_query_params(sql,args)
        # print(f"sql:{sql}")
        if self.is_connected is False:
            c.con.log("Not Connected to a database dork.","warning")
        # if self.connect() is False:
            return default

        # _log(f"sql:{sql}","cyan")
        # _log(f"args:{args}","cyan")


        self.cursor.row_factory = sqlite3.Row
        if self.run(sql,args):
            rows = self.cursor.fetchall()
            # print(rows)
            dicts = self._to_dict(rows)
            return dicts
            # contents = self.cursor.fetchall()
            # print(f"selects: ")
            # print(select._selects)
            # print(contents)
            # if len(select._selects) == 0:
            #     out = []
            #     for x in contents:
                    
                
            return self.fetchall()
        return default



    def close(self):
        '''
            Close the connection to the mySQL database.

            ----------

            Return {None}
            ----------------------
            None

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 06-05-2022 19:19:32
            `memberOf`: DatabaseManager
            `version`: 1.0
            `method_name`: close
            * @xxx [06-05-2022 19:20:10]: documentation for close
        '''

        self.connection.close()
        self.connection = None
        self.cursor = None


    # def __sqlite_execute_single_statement(self, sql:str, args=False,isTimeoutRetry=False,foreign_key_checks=True):


    def execute_single_statement(self, sql:str, args=False,isTimeoutRetry=False,foreign_key_checks=True):
        '''
            Executes a single SQL query on the database.

            ----------

            Arguments
            -------------------------
            `sql` {string}
                The SQL to be executed.

            `args` {list}
                A list of arguments for parameter substitution.

            Return {bool}
            ----------------------
            True upon success, false otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 12-09-2021 09:19:40
            `memberOf`: colemen_database
            `version`: 1.0
            `method_name`: execute_single_statement
        '''
        success = False
        if self.cursor is None or self.connection is None:
            print("Not connected to a database, aborting query.")
            if self.data['credentials'] is not None:
                self.connect()
        try:
            if foreign_key_checks is False:
                if _settings.globe.flavor == "mysql":
                    self.execute_single_statement("SET FOREIGN_KEY_CHECKS = 0;")
                if _settings.globe.flavor == "sqlite":
                    self.execute_single_statement("PRAGMA foreign_keys = OFF;")

            if args is False:
                # print(f"executing sql: ",sql)
                self.cursor.execute(sql)
            else:

                args = _sanitize_args(args)
                self.cursor.execute(sql, args)

                # print(f"result: ",result)

            self.connection.commit()
            # self.connection.
            success = True
            if foreign_key_checks is False:
                if _settings.globe.flavor == "mysql":
                    self.execute_single_statement("SET FOREIGN_KEY_CHECKS = 1;")
                if _settings.globe.flavor == "sqlite":
                    self.execute_single_statement("PRAGMA foreign_keys = ON;")


        except _mysqlConnnector.errors.IntegrityError as e:
            # _log(f"{_traceback.format_exc()}","error")
            # _log(f"SQL: {sql}","error")
            self._parse_interface_error(e)

        except _mysqlConnnector.errors.InterfaceError as e:
            if isTimeoutRetry is True:
                # _log(f"{_traceback.format_exc()}","error")
                # _log(f"SQL: {sql}","error")
                self._parse_interface_error(e)
            if isTimeoutRetry is False:
                self.cursor = None
                self.connection = None
                self.connect()
                return self.execute_single_statement(sql,args,True)

        except _mysqlConnnector.errors.DatabaseError as e:
            _log(f"{_traceback.format_exc()}","error")
            _log(f"SQL: {sql}","error")
            self._parse_interface_error(e)


        # except sqlite3.Warning as error:
        #     _log(f"Warning: {error}","error")
        #     _log(_traceback.format_exc(),"error")

        # except sqlite3.OperationalError as error:
        #     _log(f"Fatal Error: {error}","error")
        #     _log(_traceback.format_exc(),"error")

        except AttributeError:
            _log(f"{_traceback.format_exc()}\n","error")
            _log(f"{print(sys.exc_info()[2])}\n\n","error")
            _log(f"SQL: \033[38;2;(235);(64);(52)m{sql}")

        return success

    def _parse_interface_error(self,e):
        if "duplicate" in e.msg.lower():
            # Duplicate entry 'act_succesboobers' for key 'uq_hash_id'
            raise _e.DuplicateEntryError(e)
        # print(f"error:{e}")
        # print(f"e.args: {e.args}")
        # print(f"e.errno: {e.errno}")
        # print(f"e.msg: {e.msg}")
        # print(f"e.sqlstate: {e.sqlstate}")

    def run_from_list(self, query_list,**kwargs):
        '''
            Execute SQL statements from a list.

            ----------

            Arguments
            -------------------------
            `query_list` {list}
                A list of query statements to execute.

            Keyword Arguments
            -------------------------
            [`disable_restraints`=True] {bool}
                If True, temporarily disable foreign_key_checks while executing the queries

            Return {bool}
            ----------------------
            True upon success, false otherwise.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 06-05-2022 16:32:58
            `memberOf`: DatabaseManager
            `version`: 1.0
            `method_name`: run_from_list
            * @xxx [06-05-2022 16:36:56]: documentation for run_from_list
        '''

        disable_foreign_key_restraints = c.obj.get_kwarg(['disable key restraints','disable restraints'],True,(bool),**kwargs)
        # disable_foreign_key_restraints = True
        # if 'DISABLE_KEY_RESTRAINTS' in kwargs:
        #     if kwargs['DISABLE_KEY_RESTRAINTS'] is False:
        #         disable_foreign_key_restraints = False
        if disable_foreign_key_restraints is True:
            self.run("SET foreign_key_checks = 0;")

        success = True
        for idx,que in enumerate(query_list):
            print(f"{idx}/{len(query_list)}",end="\r",flush=True)
            success = self.run(que)
            if success is False:
                break

        if disable_foreign_key_restraints is True:
            self.run("SET foreign_key_checks = 1;")
        return success

    def run_multi(self, sql:str, args):
        sql = sql.replace(";", ";STATEMENT_END")
        statements = sql.split('STATEMENT_END')
        for s in statements:
            if len(s) > 0:
                # print(f"query: {s}")
                self.run(s, args)

    def fetchall(self):
        '''
            Executes the fetchall method on the database and converts the result to a dictionary.

            ----------


            Return {dict|list}
            ----------------------
            If there is more than one result, it returns a list of dicts.
            If there is only one result, it returns a single dictionary.

            Meta
            ----------
            `author`: Colemen Atwood
            `created`: 06-02-2022 13:58:55
            `memberOf`: DatabaseManager
            `version`: 1.0
            `method_name`: fetchall
            * @xxx [06-02-2022 13:59:37]: documentation for fetchall
        '''
        # def dict_factory(cursor, row):
        #     d = {}
        #     for idx, col in enumerate(cursor.description):
        #         d[col[0]] = row[idx]
        #     return d
        # return dict_factory(self.cursor,self.cursor.fetchall())
        # colname = [ d[0] for d in query.description ]
        # result_list = [ dict(zip(colname, r)) for r in query.fetchall() ]
        
        result = self.cursor.fetchall()
        # print(f"fetchall::result : {result}")
        return result
        return self._to_dict(self.cursor.fetchall())

    def fetchone(self):
        """ DOCBLOCK {
                "class_name":"Database",
                "method_name":"fetchone",
                "author":"Colemen Atwood",
                "created": "04-19-2021 08:04:18",
                "version": "1.0",
                "description":"Executes the fetchone method on the database.",
                "returns":{
                    "type":"dict",
                    "description":"The result of the fetchone command"
                }
            }"""
        r = self.cursor.fetchone()
        return r

    def _to_dict(self, result):
        # print(f"_to_dict:resultType: {type(result)}")
        # print(f"_to_dict:result : {result}")
        if isinstance(result, (list)):
            new_data = []
            for row in result:
                tmp = {}
                # @Mstep [IF] if the row is a tuple
                # this means its probably the response of the sqlite last insert id.
                if isinstance(row,(tuple)):
                    if len(row) == 1:
                        row = row[0]
                    return row
                # print(f"row:{row.keys()}")
                for col in row.keys():
                    tmp[col] = row[col]
                new_data.append(tmp)
            return new_data
        # if isinstance(result, sqlite3.Row):
        #     new_data = {}
        #     for col in result.keys():
        #         new_data[col] = result[col]
        #     return new_data





# def _to_statement_list(sql):
#     sql = sql.replace(";", ";STATEMENT_END")
#     statements = sql.split('STATEMENT_END')
#     output = [x.strip() for x in statements if len(x.strip()) > 0]
#     return output


def _to_statement_list(sql):

    def format_triggers(sql):
        '''
        This will locate trigger create statements and modify the sql to ignore
        the internal colons.

        This is required in order for the statement splitting by colons to work,
        other wise is will split the trigger into two separate statements.
        '''
        sline_sql = sql.replace("\n","__NEW_LINE__")

        matches = re.findall(r'(CREATE\s*TRIGGER.*END;)',sline_sql)
        for match in matches:
            target = match.replace("__NEW_LINE__","\n")
            new = match.replace("END;","__TRIGGER_END__")
            new = new.replace(";", "__SINGLE_COLON__")
            new = new.replace("__TRIGGER_END__","END;")
            new = new.replace("__NEW_LINE__","\n")
            sql = sql.replace(target,new)
        return sql

    sql = format_triggers(sql)
    sql = sql.replace(";", ";STATEMENT_END")
    sql = sql.replace("__SINGLE_COLON__", ";")

    statements = sql.split('STATEMENT_END')
    # print(f"sql:{sql}")
    # _ = [print(f"{'-'*80}\n{x}") for x in statements]

    # print(f"statements:{statements}")
    # exit()
    # statements = []
    # if ";REAL_STATEMENT_END" in sql:
    #     st = sql.split('REAL_STATEMENT_END')
        # for s in st:


    output = [x.strip() for x in statements if len(x.strip()) > 0]
    return output

def _sanitize_args(args):
    if isinstance(args,(dict)):
        output = {}
        for k,v in args.items():
            output[k] = c.string.sanitize_quotes(v)
        return output
    if isinstance(args,(list)):
        output = []
        for v in args:
            output.append(c.string.sanitize_quotes(v))
        return output



def _paginate_select_query(sql:str,limit:int=None,offset:int=None)->str:
    '''
        Apply a limit and offset value to a select query statement.
        ----------

        Arguments
        -------------------------
        `sql` {str}
            The sql statement to modify

        [`limit`=None] {int}
            The limit to apply to the results.

        [`offset`=None] {int}
            The offset to apply to the results.


        Return {str}
        ----------------------
        The sql statement with a limit and offset value applied.

        If the limit/offset if invalid no pagination is added.

        Meta
        ----------
        `author`: Colemen Atwood
        `created`: 12-09-2022 11:09:53
        `memberOf`: MySQLDatabase
        `version`: 1.0
        `method_name`: _paginate_select_query
        * @xxx [12-09-2022 11:11:52]: documentation for _paginate_select_query
    '''
    if limit is None and offset is None:
        return sql
    if isinstance(limit,(str)):
        limit = re.sub(r'[^0-9]',"",limit)
        if len(limit) == 0:
            return sql
        limit = int(limit)


    if isinstance(offset,(str)):
        offset = re.sub(r'[^0-9]',"",offset)
        if len(offset) == 0:
            offset = None
        else:
            offset = int(offset)

    if limit == 0:
        limit = 1

    if offset is not None:
        if offset < 1:
            offset = None

    sql = c.string.strip(sql,[";"],"right")
    sql = re.sub(r'limit\s*[0-9]*\s*(,|offset)\s*(:?[0-9\s]*)?',"",sql,re.MULTILINE | re.IGNORECASE)

    limit_string = f"LIMIT {limit}"
    offset_string = ""
    if offset is not None:
        offset_string = f"OFFSET {offset}"
    paginate = f"{limit_string} {offset_string}"
    sql = f"{sql} {paginate}"
    return sql


def _format_query_params(sql:str,args:dict)->str:
    '''
        Format an SQL query's parameters to use the python named template format.

        This will only replace matches that have a corresponding key in the args dictionary.

        Parameters can begin with a dollar sign or colon.


        SELECT * from blackholes WHERE hash_id=$hash_id

        SELECT * from blackholes WHERE hash_id=%(hash_id)s

        ----------

        Arguments
        -------------------------
        `sql` {str}
            The sql string to format.

        `args` {dict}
            The dictionary of parameter values.


        Return {str}
        ----------------------
        The sql statement with parameters replaced.

        Meta
        ----------
        `author`: Colemen Atwood
        `created`: 12-09-2022 10:39:24
        `memberOf`: MySQLDatabase
        `version`: 1.0
        `method_name`: _format_query_params
        * @xxx [12-09-2022 10:43:23]: documentation for _format_query_params
    '''
    if isinstance(args,(dict)) is False:
        return sql
    # args = sorted(args.items(), key=lambda x: x[1], reverse=True)
    arg_keys = list(args.keys())
    arg_keys.sort(key=len, reverse=True)
    for k in arg_keys:
    # for k,v in args.items():
        sql = re.sub(fr'[$:]{k}',f"%({k})s",sql)

    # matches = re.findall(r'[$:]([a-z_0-9]*)',sql,re.IGNORECASE)
    # if isinstance(matches,(list)):
    #     for match in matches:
    #         if match in args:
    #             sql = sql.replace(f"${match}",f"%({match})s")

    return sql



