
"""
Funções para facilitar a implementação de um ETL, subindo o nível de abstração
para um job de carga de dados.

Se parâmetro '-v' (de verbose) for passado na linha de comando ao executar o
script de carga, a quantidade de registros lidos e gravados será impressa no
console.
"""

import sys
from datetime import datetime

import blipy.tabela_entrada as te
import blipy.tabela_saida as ts
import blipy.func_transformacao as ft

from enum import Enum, auto
from blipy.planilha import Planilha


# Tipos de estratégias de gravação no banco
class TpEstrategia(Enum):
    # deleta todas as linhas da tabela antes de inserir as novas linhas
    DELETE      = auto()    
    # simplesmente insere as novas linhas
    INSERT      = auto()    
    # trunca a tabela antes de inserir as novas linhas
    TRUNCATE    = auto()    


class Job():
    def __init__(self, nome_job):
        self.__verbose = False
        if len(sys.argv) == 2:
            if sys.argv[1] == "-v":
                self.__verbose = True
 
        self.__nome = nome_job
 
        print()
        print("====== Job " + self.__nome + " iniciado " + "=======")
        print("-----> Horário de início:  " +  \
                str(datetime.now().replace(microsecond=0)))

    def __del__(self):
        print("====== Job " + self.__nome + " finalizado " + "=====")
        print("-----> Horário de término: " +  \
                str(datetime.now().replace(microsecond=0)))

    def importa_tabela_por_sql(self, conn_entrada, conn_saida, sql_entrada,
            nome_tabela_saida, cols_saida, estrategia=TpEstrategia.DELETE):
        """
        Importação do resultado de uma consulta sql para o banco de dados. A
        tabela de destino é limpa e carregada de novo (considera-se que são poucas
        linhas). Qualquer erro dispara uma exceção.

        Args:
        conn_entrada: conexão com o esquema de entrada de dados (geralmente
                      o staging)
        conn_saida: conexão com o esqumea de saída (geralmente a produção)
        sql_entrada: consulta sql que irá gerar os dados de entrada
        nome_tabela_saida: nome da tabela de saida
        cols_saida: lista das colunas que serão gravadas na tabela 
                    de saida, com o nome da coluna, seu tipo e a função de
                    transformanção a ser aplicada (função de transformação é
                    opcional; se não informado, faz só uma cópia do dado)
        estrategia: estratégia de gravação que será utilizada (enum
                    TpEstrategia)
        """

        tab_entrada = te.TabelaEntrada(conn_entrada)
        tab_entrada.carrega_dados(sql_entrada)

        self.__grava_tabela(    tab_entrada, 
                                conn_saida, 
                                nome_tabela_saida, 
                                cols_saida, 
                                estrategia)

    def importa_tabela_por_nome(self, conn_entrada, conn_saida, 
            nome_tabela_entrada, nome_tabela_saida, cols_entrada, cols_saida,
            filtro_entrada="", estrategia=TpEstrategia.DELETE):
        """
        Importação de uma tabela para o banco de dados. A tabela de destino é limpa
        e carregada de novo (considera-se que são poucas linhas). Qualquer erro
        dispara uma exceção.

        Args:
            conn_entrada: conexão com o esquema de entrada de dados (geralmente
                          o staging)
            conn_saida:  conexão com o esqumea de saída (geralmente a produção)
            nome_tabela_entrada: nome da tabela de entrada
            nome_tabela_saida: nome da tabela de saida
            cols_entrada: lista dos nomes das colunas que serão buscadas na 
                          tabela de entrada
            cols_saida: lista das colunas que serão gravadas na tabela 
                        de saida, com o nome da coluna, seu tipo e a função de
                        transformanção a ser aplicada (função de transformação
                        é opcional; se não informado, faz só uma cópia do dado)
            filtro_entrada: filtro opcional dos registros da tabela de entrada,
                            no formato de uma cláusula WHERE de SQL, sem a
                            palavra WHERE
            estrategia: estratégia de gravação que será utilizada (enum
        TpEstrategia)
        """

        tab_entrada = te.TabelaEntrada(conn_entrada)
        tab_entrada.carrega_dados(
                nome_tabela_entrada, 
                cols_entrada, 
                filtro=filtro_entrada)

        self.__grava_tabela(    tab_entrada, 
                                conn_saida, 
                                nome_tabela_saida, 
                                cols_saida, 
                                estrategia)

    def importa_planilha(self, conn_saida, 
        nome_tab_saida, cols_saida, cols_entrada=None,
        estrategia=TpEstrategia.DELETE,
        io=None, sheet_name=0, header=0, skipfooter=0, 
        names=None, index_col=None, usecols=None,
        skiprows=None, nrows=None, na_values=None, engine='openpyxl'):
        """
        Importação de uma tabela para o banco de dados. A tabela de destino é limpa
        e carregada de novo (considera-se que são poucas linhas). Qualquer erro
        dispara uma exceção.

        Args:
            conn_saida:     conexão com o esqumea de saída (geralmente a produção)
            nome_tab_saida: nome da tabela de saida
            cols_saida:     lista das colunas que serão gravadas na tabela 
                            de saida, com o nome da coluna, seu tipo e a função de
                            transformanção a ser aplicada (funão de
                            transformação é opcional; se não
                            informado, faz só uma cópia do dado)
            cols_entrada:   lista com o índice (zero-based) das colunas que
                            serão utilizadas da planilha. Se None, utiliza
                            todas as colunas, na ordem da planilha.  Esse
                            parâmetro é importante quando se quer que uma única
                            coluna da planilha seja fonte de entrada para mais
                            de uma coluna de saída, já que a relação entre
                            cols_saida e cols_entrada tem que ser de 1 para 1.
                            Por exemplo, se a primeira coluna da planilha for
                            fonte de entrada para a primeira e terceira colunas
                            da tabela de saída, esse parâmetro deve ser [0, 1,
                            0]
            estrategia:     estratégia de gravação que será utilizada (enum
                            TpEstrategia)

            io:             caminho (path) da planilha, com nome da planilha
                            incluso ou uma URL
            sheet_name:     aba ou guia do Excel, pode ser o nome ou número
                            0-indexed
            header:         linhas de cabeçalho a serem ignoradas, 0-indexed
            skipfooter:     linhas de rodapé (final arquivo) a serem ignoradas,
                            0-indexed.
            names:          lista de nomes de colunas. Se arquivo não contém
                            header, deve-se obrigatoriamente setar header=None
            index_col:      coluna a ser usada como label das linhas
            usecols:        seleção de colunas. Tipos de seleção: 'A,B,C',
                            [0,1,2], ['ID','Coluna_1','Coluna_4']
            skiprows:       linhas a serem ignoradas no início do arquivo,
                            0-indexed
            nrows:          número de linhas a serem carregadas
            na_values:      lista de strings a serem consideradas como dados
                            não disponívels (NaN)
            engine:         o padrão é openpyxl. Existem outras para formatos
                            antigos ex .odf

        Obs.: os argumentos de io a engine são os mesmos do método read_excel do Pandas.
        """

        planilha = Planilha()
        planilha.carrega_dados(io, sheet_name, header, skipfooter, names, 
            index_col, usecols, skiprows, nrows, na_values, engine)

        if cols_entrada is not None:
            planilha.formata_colunas(cols_entrada)

        self.__grava_tabela(planilha, 
                            conn_saida, 
                            nome_tab_saida, 
                            cols_saida, 
                            estrategia)

    def __grava_tabela(self, 
            entrada, 
            conn_saida, 
            nome_tabela_saida, 
            cols_saida, 
            estrategia):
        """
        Grava uma tabela no banco de dados.

        Args:
        entrada: a fonte de entrada dos dados (tabela de banco, planilha etc.)
        conn_saida: conexão com o esqumea de saída (geralmente a produção)
        nome_tabela_saida: nome da tabela de saida
        cols_saida: lista das colunas que serão gravadas na tabela 
                    de saida, com o nome da coluna, seu tipo e a função de
                    transformanção a ser aplicada (opcional; se não informado,
                    faz só uma cópia do dado)
        estrategia: estratégia de gravação que será utilizada (enum
                    TpEstrategia)
        """

        cols = {}
        for item in cols_saida:
            if len(item) == 2:
                # função de transformanção não informada, faz só uma cópia do dado
                cols[item[0]] = ts.Coluna(item[0], item[1], ft.f_copia)
            else:
                # usa a função de transformanção informada
                cols[item[0]] = ts.Coluna(item[0], item[1], item[2])

        tab_saida = ts.TabelaSaida(nome_tabela_saida, cols, conn_saida)

        if estrategia == TpEstrategia.DELETE:
            conn_saida.apaga_registros(nome_tabela_saida)
        elif estrategia == TpEstrategia.TRUNCATE:
            conn_saida.trunca_tabela(nome_tabela_saida)
        elif estrategia == TpEstrategia.INSERT:
            pass
        else:
            raise NotImplementedError

        qtd_registros = 0
        while True:
            registro = entrada.le_prox_registro()
            if registro is None:
                break

            i = 0
            for k in cols.keys():
                tab_saida.col[k].calcula_valor( (registro[i],) )
                i += 1

            tab_saida.grava_registro()
            qtd_registros += 1
 
        if self.__verbose:
            if entrada.nome == "":
                # a consulta de entrada não leu uma só tabela, mas um
                # select provavelmente com joins de tabelas
                print(  str(qtd_registros) +  \
                        " \tregistros lidos dos dados de entrada e" 
                        " salvos na tabela " +  \
                        nome_tabela_saida)
            else:
                print(  str(qtd_registros) +  \
                        " \tregistros de entrada lidos de " + entrada.nome + \
                        " e salvos na tabela " +  \
                        nome_tabela_saida)

