# 
import re                               # 
import json                             # 
import string                           # 
import random                           # 
import hashlib                          # 
import requests                         # 
from   time             import    sleep # 
from   pprint           import   pprint # 
from   generic.gn_mysql import gn_mysql # 
from   generic.gn_request   import   Request # 
from datetime import datetime           #
import copy

class gn_api (object) : 
    
    def __init__(self, api:str, nmlkgrupo:str, debug:bool):
        """Construtor
        
        Arguments:
            api {str} -- api que esta usando essa clase generica, esse nome serve para encontrar os arquivos, mapa e classe
        """
        self.apiName    = api 
        self.jsonWs     = {}
        self.config     = {} 
        self.sql        = "" 
        self.db         = self.db or False 
        self.tableNames = {} 
        self.nmlkgrupo  = nmlkgrupo
        self.debug      = debug
        
        
        # prepara as variaveis de classe que vem da classe filho
        self.prepareParamns()
        
        # Faz o processamento
        self.process()
        
        
        
        
    def prepareParamns(self):
        """ Parametros """
        
        # tempo de espera entre chamadas
        if not hasattr(self,'request_sleep') :
            self.request_sleep = False 
            
        # Para envar item a item
        if not hasattr(self,'sendOneByOne') :
            self.sendOneByOne = False 
            
        # base da url, concatenado com as posicoes do mapa
        if not hasattr(self,'request_urlBase') :
            self.request_urlBase = False 
            
        # headers de chamada
        if not hasattr(self,'request_headers') :
            self.request_headers = False 
            
            
            
            
            
        
        
    def process(self):
        """Faz o processamento"""
        self.loadFile()
        self.percorrerRotas()
        
        
        
        
    def loadFile(self):
        """Busca e abre o arquivo de definicoes da api"""
        #! caminho para usar no linux
        path = "/var/www/html/webservice/gn_integracao/" + self.apiName + "/map.json"

        #! caminho para usar no Windows - Modo Debug
        if self.debug is True:
            path = "./" + self.apiName + "/map.json"
        
        with open(path , 'r', encoding='utf8') as jsonFile:
            conteudo = jsonFile.read()
            # conteudo = conteudo.encode(encoding='UTF-8',errors='strict')
        self.jsonWs = json.loads( conteudo )
        self.config = self.jsonWs['config']
        
        
    
    def percorrerRotas(self):
        """Percorre rota a rota"""
        ws      = self.jsonWs['rotes']
        
        
        for dados in ws :
            
            self.atualWs = dados
            
            if 'active' in dados and dados['active'] == False : 
                continue
                
            if self.method == 'SEND':
                if  'POST' not in dados['methods'] \
                and 'PUT'  not in dados['methods']:
                    continue
                    
                
            
            if self.rote != 'ALL' : 
                if self.rote != dados['rote']:
                    continue
                    
                
            
            if 'GET' in dados['methods'] :
                #sql = getattr(self, dados["custom_get"])()
                self.method_get()
                continue
                
            
            sql = self.dePara()
            
            sql = """ 
                SELECT *
                FROM ({SQL}) AS TEMPTB
                WHERE GENERIC_METHOD IS NOT NULL
            """.format(SQL = sql)
            resultado = self.selectDb(sql)
            
            # customisacao
            if 'custom_send' in dados:
                resultado = getattr(self, dados["custom_send"])(resultado)
                # pass
                
            else:
                self.enviar(resultado, dados['rote'])
                
            if 'extra_process' in dados:
                getattr(self, dados["extra_process"])(dados)
            
        
        
    def dePara(self):
        # fields=[]
        # self.table = ""
        # self.atualDados = dados
        # where = ['1=1']
        # TODO montar esquema para where
        
        sql = """
            {_select}
            {_from}
            {_where}
            {_groupby}
            {_orderBy}
        """.format(
            _select  = self.mountSelect  ( [] ) , # 
            _from    = self.mountFrom    (    ) , # 
            _where   = self.mountWhere   ( [] ) , # 
            _groupby= self.mountGroupBy (    )  ,  # 
            _orderBy = self.mountOrderBy (    )   # 
        )
        
        sql = self.posProcessSql(sql)
        
        self.println(sql)
        
        
        return sql
        
        
        
        
        
    
    
    def mountSelect(self, fields:list=[]) -> str : 
        """Monta o SELECT para o sql 
        
        Keyword Arguments:
            fields {list} -- [Lista de campos, passado por paramentro para poder extender mais facilmente] (default: {[]})
        
        Returns:
            str -- [Retorna o script assim: "SELECT CAMPO1, CAMPO2, CAMPO3"]
        """
        # Percorre os campos declarados no mapa
        for dado in self.atualWs['fields']:
            # processa cada item baseado no tipo, dbfield, default, _custom ...
            fields.append( self.processItem(dado) )
            
        # junta tudo com virgula
        fields = (","+" "*18).join(fields)
        # 
        return "SELECT {0}".format(fields)
        
        



    def mountFrom(self) -> str : 
        #!arrumar aqui esta com erro, quando nao encontra dados no DExPARA
        _from = ""
        
        joins = []
        if "joins" in self.atualWs :
            for join in self.atualWs["joins"]:
                joins.append(join['script'].replace(
                    ' {0} '.format(join['tbName']) , 
                    ' {0} AS {1} '.format(
                        join['tbName'],
                        self.randName(join['tbName']
                    )) 
                ))
                
            
            _from =  "\n".join(joins)
        # 
        return  """
            FROM {0} AS {1} 
            {2} 
        """.format(
            self.atualWs['table'],
            self.randName(self.atualWs['table']),
            _from
        )
        
        
        
        
        
        
        
    
    
    
    def mountWhere(self, whereFields:list=[]) -> str : 
        
        where = [ "1=1" ] 
        
        if 'where' in self.atualWs:
            where += self.atualWs['where']
        # 
        
        where += whereFields
        # 
        where = ' AND ' .join(where)
        # 
        return " WHERE {} ".format(where)
        
    
    
    
    def mountOrderBy(self) -> str : 
        """Monta a condicao orderby
        
        Returns:
            str -- OrderBy
        """
        orderBy = ""
        if 'orderby' in self.atualWs:
            orderBy = ' '.join(self.atualWs['orderby'])
            orderBy = " ORDER BY {0} ".format(orderBy)
        return orderBy
        
    
    def mountGroupBy(self) -> str : 
        """Monta a condicao groupBy
        
        Returns:
            str -- groupBy
        """
        groupBy = ""
        if 'groupBy' in self.atualWs:
            groupBy = ' '.join(self.atualWs['groupBy'])
            groupBy = " GROUP BY {0} having count(*) > 1".format(groupBy)
        return groupBy
        
    
    
    def randName(self, table:str) -> str:
        """Generate a random string of fixed length
        
        Arguments:
            table {str} -- [description]
        
        Returns:
            str -- [description]
        """
        stringLength = 10
        
        if table in self.tableNames:
            return self.tableNames[table]
            
        # gera um nome aleatorio
        aleatorios = [random.choice(string.ascii_uppercase) for i in range(stringLength)]
        randNm = ''.join(aleatorios)
        randNm = '/*{table}*/{randNm}'.format(table=table, randNm=randNm)
        
        if randNm in self.tableNames.values() :
            randNm = self.randName(table)
            
        else:
            #self.tableNames.append(randNm)
            self.tableNames[table] = randNm
            
        
        return randNm
        
    
    
    
    def processItem(self, dado:list ) -> str:
        """ Monta o formado do select """
        
        field = ""
        
        if   'default' in dado:
            field = self.processItem_default(dado)
            
        if 'dbfield' in dado:
            field = self.processItem_dbfield(dado)
            
        if '_custom' in dado:
            field = self.processItem__custom(dado)
            
        
        
        field = "{0} AS '{1}' ".format(field, dado["para"])
        
        # formatacao
        field = "    {0}\n".format(field)
        
        return field
        
        
        
    # ''''''
    def processItem_default(self, field:{'dbfield':str, 'para':str, 'type':str ,'desc': str}) -> str:
        """Faz o processamento de valores do tipo defaults
        
        Arguments:
            field {object} -- Objeto com os dados da coluna {'dbfield':str, 'para':str, 'type':str ,'desc': str} 
        
        Returns:
            str -- string do canpo tratado
        """
        
        # lista com as variacoes de null
        nulsVariations = ['null', '\'null\'', 'NULL', '\'NULL\'']
        
        # pega o valor a ser trabalhado
        value = field["default"]
        
        # se vier null, troca por NULL maiusculo para que o banco entenda
        if value in nulsVariations:
            retorno = "NULL"
            
        # se nao for nul, faz outro tratamento
        else:
            # se for booleano
            if field['type'] == 'Boolean':
                retorno =  "{0}" .format(value)
                
            # se for qualquer outro tipo de dado, coloca entre aspas e eh sucesso
            else:
                retorno = "'{0}'".format(value)
                
            
        # volta para ser coloccno no script
        return retorno
        
        
        
        
        
    def processItem_dbfield(self, field):
        
        field = field['dbfield']
        
        self.table = field.split('.')[0].strip()
        
        # se nao tiver mais, soh retorna
        if '+' not in field :
            return field
        
        # se tiver +, faz o processamento
        partes = field.split('+')
        retornoPartes = []
        
        cast      = lambda text : 'CAST({0} AS CHAR)'.format(text.strip())
        concat    = lambda text : 'CONCAT({0})'.format(text)
        separator = lambda prts : " , ".join(prts)
        
        for parte in partes:
            retornoPartes.append( cast(parte) )
            
        retorno = separator(retornoPartes)
        retorno = concat(retorno)
        
        '''
        return 'CONCAT( CAST( {0} AS CHAR) )'.format(
                    " AS CHAR) , ' ' , CAST( ".join(
                        field.split('+')
                    )
                )
        '''
        
        return retorno 
        
        
        
        
        
    def processItem__custom(self, field):
        return getattr(self, field["_custom"])(field)
        
        
        
        
    def posProcessSql(self, sql:str) -> str:
        
        
        for table in self.tableNames:
            
            sql = re.sub(
                r'{0}\.'.format(table),
                self.randName(table) + ".",
                sql
            )
            
        '''
        regex = r'{0}\.'.format(self.atualWs['table'])
        sql = re.sub(
            regex, 
            self.randName(self.atualWs['table']) + "." ,
            sql
        )
        '''
        return sql
        
        
    def selectDb(self, sql):
        #print(sql) # pprint
        #exit()
        resultado = self.getDb().select(sql)
        return resultado 
        
        
        
    def aplicarLists(self, item):
        """ TODO Baita gambiarra, favore voltar e fazer direito isso """
        # mascaras de lista 
        for campo in self.atualWs['fields'] :
            if campo['type'] == "List" : 
                if "ListTemplate" in campo:
                    decoded  = json.dumps(campo["ListTemplate"])
                    
                    print (item[campo["para"]])
                    replaced = ""
                    if item[campo["para"]] :
                        replaced = decoded.replace("{0}", item[campo["para"]])
                    
                    if not replaced or '""' in replaced  :
                        replaced = '[]'
                        
                    item[campo["para"]] = json.loads(replaced)
                    
                
            
        # remove os NULLs
        for chave in item:
            #pprint(item[chave])
            if item[chave] == 'NULL' or item[chave] == '\'NULL\'' : 
                item[chave] = ''
            
        
        return item
        
        
        
    def enviar(self, resultado, rota):
        
        # envia um a um
        if self.sendOneByOne:
            self.make_sendOneByOne(resultado, rota)
            
        # ou todos de uma vez
        else :
            pass
            
        
        
            
    def make_sendOneByOne(self, dados, rota:str):
        # self.atualWs['fields']
        
        # percorre um a um
        for index, linha in enumerate(dados) :
            
            if "excluido" in linha                      \
                and linha['excluido']                   \
                and linha['GENERIC_METHOD'] == 'POST' :
                continue
            #
            # if index == 1 :
            linha    = self.aplicarLists(linha)
            
            if self.debug is True:
                #pprint(linha)
                self.println(linha)
                
            self.savedData = copy.deepcopy(linha)
            self.send(rota, linha, linha['GENERIC_METHOD'])
            
        
        
        
    def send(self, rota:str, data:object, method:'POST'):
        
        identifier = ''
        
        if 'send' in self.atualWs and self.atualWs['send'] == False:
            return 
        
        if method == "PUT" :
            if not self.config['identifier'] in data :
                return
                
            identifier = "/"+ data[self.config['identifier']]
            pass
        
        try: 
            del data[self.config['identifier']]
        except :
            pass            
        
        
        url = self.request_urlBase + rota + identifier
        self.println(url)
        
        
        data    = json.dumps(data)
        headers = self.request_headers
        
        self.sleep()
        req = requests.request(
            method           , 
            url              , 
            data=data        , 
            headers=headers    
        )
        
        
        
        self.posRequest(req, rota, data)
        
        return req
        
    
    
    def posRequest(self, req:object, rota, data):
        self.println(req.status_code)
        self.println(req.headers    )
        self.println(req.content    )
        
        
        
        
    def sleep(self):
        if self.request_sleep != False:
            sleep(self.request_sleep)
            
        
    
    
    
    def getDb(self):
        if self.db == False :
            self.db = gn_mysql(
                self.nmlkgrupo , 
                self.database_db,
                self.debug
            ) 
            
        
        return self.db
        
        
        
        
    def md5_object(self, obj):
        """ Methodo para convercao de um objeto para MD5
            o hash se baseia apenas nos valores, nao nas chaves
            as chaves sao despresadas nesse processo
            Params:
                obj { Object } -- Objeto a ser gerado o hash
        """
        instanceMd5 = hashlib.md5()     ## instancia do md5                        ##
        values = obj.values()           ## Remove os valores do objeto             ##
        values = sorted(values)         ## ordenado                                ##
        values = json.dumps(values)     ## cobnerte para json                      ##
        values = values.encode('utf-8') ## Forca o encode para utf8                ##
        instanceMd5.update(values)      ## passa o valor para a insrtancia do md5  ##
        return instanceMd5.hexdigest()  ## retorna o valor                         ##
        
    
    
    def println(self, thePrint) :
        
        if self.debug :
            print(thePrint)
        else :
            self.writeLog(str(thePrint), self.config["path_log"])
            
        
    def timeNow(self):
        return datetime.strftime(datetime.now(),'%d/%m/%Y %H:%M:%S')
    
    def createLog(self, status_code, mensagem):
        flag = "w+" if status_code == '000' else "a+"
        conteudo = "{timeNow};{status_code};{mensagem}\n".format(timeNow=self.timeNow(),status_code=status_code,mensagem=mensagem)
        self.writeLog(conteudo, self.saida, flag)
        
    def writeLog(self, content="", file_name="", flag = "a+") :
        with open(file_name, flag, encoding='utf8') as log :
            log.write(content) 
            
        
    def success(self, data):
        
        sql = """
                SELECT * FROM 
                    TBRELACIONAID 
                WHERE 
                    1   =   1 
                AND 
                    NMTABELA = '{table}' 
                AND 
                    IDAPI = '{id_api}' 
                AND 
                    IDSOFTDIB = '{id_sd}'
            """.format(
                    table   = data['NMTABELA']
                ,   id_api  = data['IDAPI']
                ,   id_sd   = data['IDSOFTDIB']
            )
        select = self.getDb().select(sql)
        
        
        if len(select) == 0 :
            #dataFields['IDAPI'] = idapi
            self.getDb().insert( "TBRELACIONAID", data)
            
        elif len(select) and select[0]['HASHMD5'] != data['HASHMD5'] :
            self.getDb().update( "TBRELACIONAID", data, "IDAPI='{}'".format(data['IDAPI']))
            
        self.current_data = {}
        
    def exists(self, field ,table, where = ""):
        """ 
            #verifica se um registro ja existe na tabela
        """
        
        if len(where) > 0:
            where = 'WHERE {where}'.format(where=where)
        
        sql = """
        SELECT 
            COUNT({field}) AS qtd 
        FROM {table}
            {where}""".format(field=field,table=table,where=where)
        
        
        exists = self.getDb().select(sql)
        return  True if exists[0]['qtd'] > 0 else False
        
    def jsonValueByPath(self, path ,json):
        
        Json = JsonPath(path=path, json=json)
        return Json.value
        
        
    
    def __getattr__(self, name):
        def error__(*args, **kwargs):
            print ("\"%s\" Nao eh um metodo da classe \"%s\"" % (name, self.__class__.__name__))
            return False
        return error__
        
class JsonPath():
    
    def __init__(self, **data):
        
        self._path   = re.sub(r'\s|\t|\r|\n',"",data['path'])
        self._json   = data['json']
        self.value  = list()
        
        self.process()
        print(self.value)

    def __repr__(self):
        return self.value
        
        
    def process(self):
        self.dict   = self.explode(self._path,"||")
        _filter     = self.filter_values(self.dict)
        if len(_filter) :
            self.value = _filter[0]
        else:
            self.value = ''
            
        
    def explode(self, str_to_split, separator) :
        paths = str_to_split.split(separator)
        return list(map(self.execute, paths))
        
    
    def validate(self, value):
        return not isinstance(value, dict) and not isinstance(value, list)
        
        
    def filter_values(self, values) :
        return list(filter(lambda value: value if value else '',values))
        
    def execute(self, path):
        value = ''
        try:
            if "+" in path:
                plus    = self.explode(path,"+")
                _filter = self.filter_values(plus)
                value   = " ".join(map(str, plus)) if len(plus) == len(_filter) else ''
            else:
                value = self.goThroughPath(path)
            
        except ValueError as error:
            print("error: "+ error)
        return value
        
        
    
    def goThroughPath(self, path):
        path_splited  = path.split("/")
        current_value = self.tryPath(self._json, path_splited[0])
        
        if self.validate(current_value):
            current_value = current_value
            
        else:
            current_value = self.getValue(path_splited, current_value)
            
        return current_value
        
        
    def getValue(self, path_splited, initial_value):
        
        value = initial_value
        for key in path_splited[1:]:
            try:
                key = int(key)
                
            
            except:
                pass
                
            
            value = self.tryPath(value, key)
            
        return value
        
        
    
    def tryPath(self, arr, key) :
        try:
            return arr[key]
        except:
            print("""
            ###### path pode estar errado #######
                PATH : {path}
                KEY  : {key}
            """.format(path=arr, key=key))
            
            return ''
            

    