import os

import time

import copy

import hashlib

import base64

import datetime

from typing import List, Dict

from enum import Enum

import requests

from nsj_gcf_utils.json_util import convert_to_dumps, json_loads, json_dumps

from nsj_integracao_api_client.infra.injector_factory import InjectorFactory

from nsj_rest_lib.dto.dto_base import DTOBase
from nsj_rest_lib.descriptor.dto_field import DTOField
from nsj_rest_lib.descriptor.dto_list_field import DTOListField

from nsj_rest_lib.dao.dao_base import DAOBase


from debug_utils import _du


class Environment(Enum):
    LOCAL = "LOCAL"
    DEV = "DEV"
    QA = "QA"
    PROD = "PROD"

entidades_integracao: List[str] = [
    # --- apenas testes ---
    ## "persona.valestransportespersonalizadostrabalhadores"
    # --- Dimensoes ---
    'ns.gruposempresariais',
    'ns.empresas',
    'ns.estabelecimentos',
    'ns.configuracoes',
    'financas.bancos',
    'persona.faixas',
    'ns.obras',
    'persona.lotacoes',
    'ponto.regras',# ns.empresas
    'persona.sindicatos',
    'ns.feriados',
    'persona.instituicoes',
    'persona.eventos',
    'persona.tiposdocumentoscolaboradores',
    'persona.tiposhistoricos',
    'persona.tiposanexos',
    'persona.processos',
    'persona.ambientes',
    'persona.condicoesambientestrabalho',
    'persona.departamentos',
    'persona.funcoes',
    'persona.jornadas',
    'persona.horarios',
    'persona.horariosespeciais',
    'persona.cargos',
    'persona.niveiscargos',
    'persona.tiposfuncionarios',
    'persona.trabalhadores',
    'persona.dependentestrabalhadores',
    'persona.escalasfolgastrabalhadores',
    'persona.beneficios',
    'persona.concessionariasvts',
    'persona.tarifasconcessionariasvtstrabalhadores',
    'persona.configuracoesordemcalculomovimentos',
    'persona.configuracoesordemcalculomovimentosponto',
    'persona.historicos',
    'persona.medicos',
    'persona.rubricasponto',
    #'persona.rubricasapontamento',só na web???
    # Fatos
    'persona.compromissostrabalhadores',
    'persona.convocacoestrabalhadores',
    'persona.dispensavalestransportestrabalhadores',
    'persona.emprestimostrabalhadores',
    'persona.historicosadiantamentosavulsos',
    'persona.adiantamentosavulsos',
    'persona.membroscipa',
    'persona.reajustessindicatos',
    'persona.reajustestrabalhadores',
    'ponto.compensacoeslancamentos',
    'ponto.pagamentoslancamentos',
    'persona.admissoespreliminares',# --resolver fk solicitacoesadmissoes
    'persona.avisosferiastrabalhadores', # resolver kf solicitacoesferias
    'persona.pendenciaspagamentos',
    'persona.documentoscolaboradores',
    'persona.faltastrabalhadores', # resolver fk solicitacoesfaltas
    'persona.mudancastrabalhadores',
    'ponto.diascompensacoestrabalhadores',
    'persona.afastamentostrabalhadores',
    'ponto.atrasosentradascompensaveistrabalhadores',
    'ponto.saidasantecipadascompensaveistrabalhadores',
    'persona.beneficiostrabalhadores',
    'persona.movimentosponto',
    'persona.movimentos',
    #'persona.calculostrabalhadores' Não será reativada
]

TAMANHO_PAGINA: int = 100

# Flags trace
_E_SEND_DATA = False
_E_CHECK_INT = False

_entidades_particionadas_por_grupo = ['ns.empresas', 'ns.configuracoes']

_entidades_particionadas_por_empresa = [
    'ns.configuracoes',
    'persona.movimentosponto',
    'ns.estabelecimentos',
    'persona.trabalhadores',
    'persona.processos',
    'persona.jornadas',
    'persona.ambientes',
    'persona.funcoes',
    'persona.cargos',
    #'persona.beneficios',#'persona.lotacoes'Removido pois existem entidades dependentes que não são são particionadas (persona.beneficiostrabalhadores)
    'persona.configuracoesordemcalculomovimentos',
    'persona.configuracoesordemcalculomovimentosponto',
    'persona.membroscipa',
    'persona.movimentos',
    'persona.rubricasponto',
    'persona.condicoesambientestrabalho',
    'persona.tiposfuncionarios',
    'persona.horarios',
    'persona.admissoespreliminares',
    'persona.eventos',
    #'persona.lotacoes'Removido pois existem entidades dependentes que não são são particionadas (persona.beneficiostrabalhadores)
]

_entidades_particionadas_por_estabelecimento = [
    'ns.configuracoes',
    'ns.obras',
    'persona.movimentosponto',
    'persona.trabalhadores',
    'ns.configuracoes',
    'persona.processos',
    'persona.jornadas',
    'persona.ambientes',
    'persona.funcoes',
    'persona.cargos',
    #'persona.beneficios',#'persona.lotacoes'Removido pois existem entidades dependentes que não são são particionadas (persona.beneficiostrabalhadores)
    'persona.configuracoesordemcalculomovimentos',
    'persona.configuracoesordemcalculomovimentosponto',
    'persona.membroscipa',
    'persona.movimentos',
    'persona.rubricasponto',
    'persona.condicoesambientestrabalho',
    'persona.tiposfuncionarios',
    'persona.horarios',
    'persona.admissoespreliminares',
    'persona.eventos',
    #'persona.lotacoes' Removido pois existem entidades dependentes que não são são particionadas (persona.beneficiostrabalhadores)
]


AUTH_HEADER = 'X-API-Key'


def medir_tempo(alias=None):
    def decorator(func):
        def wrapper(*args, **kwargs):
            inicio = time.perf_counter()
            resultado = func(*args, **kwargs)
            fim = time.perf_counter()
            nome = alias if alias else func.__name__
            duracao = fim - inicio
            if duracao < 60:
                print(f"\033[94m{nome} executado em {duracao:.3f} segundos\033[0m")
            elif duracao < 3600:
                minutos = duracao // 60
                segundos = duracao % 60
                print(f"\033[94m{nome} executado em {minutos:.0f} minutos e {segundos:.3f} segundos\033[0m")
            else:
                horas = duracao // 3600
                minutos = (duracao % 3600) // 60
                segundos = duracao % 60
                print(f"\033[94m{nome} executado em {horas:.0f} horas, {minutos:.0f} minutos e {segundos:.3f} segundos\033[0m")
            return resultado
        return wrapper
    return decorator


class IntegradorService():
    _injector : InjectorFactory
    _api_key: str = None
    _tenant: int = None
    _dao_intg: 'IntegracaoDAO' = None
    _filtros_particionamento: list = None
    _env : Environment = Environment.PROD
    _save_point: dict = {
        #"persona.trabalhadores" : "11e32b78-f02c-46b4-81c0-a61447fffe41"
    }
    _ignored_fields : list = ["tenant", "lastupdate"]

    def __init__(self, injector: InjectorFactory, log, env : Environment = Environment.PROD):
        self._injector = injector
        self._log = log
        self._env = env

        self._carregar_savepoint()

    def _carregar_savepoint(self):
        try:
            with open('savepoint.json', 'r') as f:
                self._save_point = json_loads(f.read())
                entidade_salva = list(self._save_point.keys())[0]
                self._log.mensagem(f"Savepoint carregado para : {entidade_salva }")
        except FileNotFoundError:
            self._save_point = {}

    def _trace_envio(self, filename, content):
        _du.conditional_trace(
            condition=_E_SEND_DATA,
            func=_du.save_to_file,
            filename=filename,
            content=content
        )

    def _trace_check(self, filename, content):
        _du.conditional_trace(
            condition=_E_CHECK_INT,
            func=_du.save_to_file,
            filename=filename,
            content=content
        )

    def _fields_to_load(self, dto_class) -> dict:

        fields = {}
        fields.setdefault("root", set(dto_class.fields_map.keys()))

        for _related_entity, _related_list_fields in dto_class.list_fields_map.items():
            fields["root"].add(_related_entity)
            fields.setdefault(_related_entity, set())
            _related_fields = _related_list_fields.dto_type.fields_map.keys()
            for _related_field in _related_fields:
                fields["root"].add(f"{_related_entity}.{_related_field}")
                fields[_related_entity].add(_related_field)

        return fields


    def _integracao_dao(self):
        if self._dao_intg is None:
            self._dao_intg = self._injector.integracao_dao()
        return self._dao_intg


    def _url_base(self) -> str:
        if self._env == Environment.LOCAL:
            return "http://localhost:5000/integracao-pessoas-api/66"
        elif self._env == Environment.DEV:
            return "https://api.nasajon.dev/integracao-pessoas-api/66"
            #return "http://localhost:5000/integracao-pessoas-api/66"
        elif self._env == Environment.QA:
            return "https://api.nasajon.qa/integracao-pessoas-api/66"
            #return "http://localhost:5000/integracao-pessoas-api/66"
        elif self._env == Environment.PROD:
            #return "http://localhost:5000/integracao-pessoas-api/66"
            return "https://api4.nasajon.app/integracao-pessoas-api/66"
        else:
            raise ValueError(f"Ambiente desconhecido: {self._env}")


    def _url_diretorio(self) -> str:
        if self._env == Environment.LOCAL:
            return "https://dir.nasajon.dev"
        elif self._env == Environment.DEV:
            return "https://dir.nasajon.dev"
        elif self._env == Environment.QA:
            return "https://dir.nasajon.qa"
        elif self._env == Environment.PROD:
            return "https://diretorio.nasajon.com.br"
        else:
            raise ValueError(f"Ambiente desconhecido: {self._env}")

    def _decode_token(self, token):
        data = token.split('.')[1]
        padding = '=' * (4 - len(data) % 4)
        str_token = base64.b64decode(data + padding).decode('utf-8')
        return  json_loads(str_token)


    @property
    def api_key(self):

        if self._api_key is None:
            self._api_key = self._integracao_dao().recuperar_token()

        return self._api_key


    @property
    def tenant(self):

        if self._tenant is None:
            decoded_token = self._decode_token(self.api_key)
            self._tenant = decoded_token["tenant_id"]

        return self._tenant

    def _tratar_resposta(self, response, dict_data):
        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                if isinstance(_json_response, dict):
                    _message = _json_response['message'] if 'message' in _json_response else ''
                else:
                    _message = _json_response[0]['message'] if 'message' in _json_response[0] else ''
            else:
                _message = response.text
            raise Exception(f"""Erro ao enviar dados ao servidor:
            Endpoint: {response.url}
            Status: {response.status_code} - {response.reason}
            Mensagem: {_message}""", convert_to_dumps(dict_data))

    def _enviar_dados(self, dict_data, acao):
        """
        """
        self._trace_envio(f"trace/send_data_{acao}_{_du.time()}.json", json_dumps(dict_data))

        upsert = True #False if (acao in ["processos"] and self._env == Environment.PROD) else True

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: self.api_key})

        if upsert:
            response = s.put(f'{self._url_base()}/{acao}?upsert=true', json=convert_to_dumps(dict_data))

            if response.status_code == 413:
                for _item in dict_data:
                    response = s.put(f'{self._url_base()}/{acao}?upsert=true', json=convert_to_dumps(_item))
                    self._tratar_resposta(response, dict_data)
            else:
                self._tratar_resposta(response, dict_data)

        else:
            for _item in dict_data:
                response = s.post(f'{self._url_base()}/{acao}', json=convert_to_dumps(_item))
                if response.status_code < 200 or response.status_code > 299:
                    if 'application/json' in response.headers.get('Content-Type', '') and \
                    isinstance(response.json(), list) and 'message' in response.json()[0] and \
                    ('_bt_check_unique' in response.json()[0].get('message', '') or response.status_code == 409):
                    #     continue
                    # else:
                        raise Exception(f"""Erro ao enviar dados ao servidor:
                        Endpoint: {response.url}
                        Status: {response.status_code} - {response.reason}
                        Mensagem: {response.text}""", convert_to_dumps(dict_data))


    def _apagar_dados(self, dict_data, acao):
        """
        """
        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: self.api_key})
        response = s.delete(f'{self._url_base()}/{acao}?tenant={self.tenant}', json=convert_to_dumps(dict_data))

        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                if isinstance(_json_response, dict):
                    _message = _json_response['message']if 'message' in _json_response else ''
                else:
                    _message = _json_response[0]['message'] if 'message' in _json_response[0] else ''

            else:
                _message = response.text
            raise Exception(f"""Erro ao apagar dados ao servidor:
            Endpoint: {response.url}
            Status: {response.status_code} - {response.reason}
            Mensagem: {_message}""")


    def _gerar_token_tenant(self, chave_ativacao: str) -> str:
        s = requests.Session()
        s.headers.update({
            'Content-Type':'application/x-www-form-urlencoded',
            'Accept':'application/json'
        })
        response = s.post(
            f'{self._url_diretorio()}/v2/api/gerar_token_ativacao_sincronia/',
            data={"codigo_ativacao": chave_ativacao})

        if response.status_code == 200:
            _json = response.json()
            if "apiKey" in _json:
                return _json["apiKey"]
            else:
                raise Exception(f'Retorno desconhecido:{_json}')

        if response.status_code < 200 or response.status_code > 299:
            if 'application/json' in response.headers.get('Content-Type', ''):
                _json_response = response.json()
                _message = _json_response['message'] if 'message' in _json_response else ''
            else:
                _message = response.text
            raise Exception(f"""Erro ao enviar dados ao servidor:
            Endpoint: {response.url}
            Status: {response.status_code} - {response.reason}
            Mensagem: {_message}""")


    def consultar_integridade_de(self, acao: str, filtros: dict, detalhar_diferencas: bool):

        filtros_str = None
        if filtros:
            filtros_str = ("&".join(
                [ f"{_chave}={filtros[_chave]}" for _chave in filtros.keys() ]
            ))

        s = requests.Session()
        s.headers.update({'Content-Type':'application/json', AUTH_HEADER: self.api_key})
        _url = (
            f'{self._url_base()}/{acao}/verificacao-integridade?tenant={self.tenant}&source={detalhar_diferencas}'
            f'{"&" + filtros_str if filtros_str else ""}'
        )
        response = s.get(_url)
        response_content = response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text

        if response.status_code < 200 or response.status_code > 299:
            if isinstance(response_content, dict):
                _message = response_content.get('message', '')
            else:
                _message = response_content
            raise Exception(f"""Erro ao consultar a integridade no servidor:
            Endpoint: {response.url}
            Status: {response.status_code} - {response.reason}
            Mensagem: {_message}""")
        return response_content


    def _integracao_foi_configurada(self):
        return self._integracao_dao().integracao_configurada()


    def _validar_grupos_empresariais(self, grupos) -> List[Dict[str, str]]:

        grupos_cadastrados = self._integracao_dao().listar_grupos_empresariais(grupos)
        _cods = [grupo['codigo'] for grupo in grupos_cadastrados]
        _grupos_faltantes = [grupo for grupo in grupos if grupo not in _cods]
        assert len(_grupos_faltantes)==0, f"Grupo(s) '{','.join(_grupos_faltantes)}' não encontrado(s)."
        return grupos_cadastrados


    def executar_instalacao(self, chave_ativacao: str, grupos: List[str]):

        assert chave_ativacao, "Chave de ativação não pode ser vazia."
        self._log.mensagem(f"Executando instalação com a chave de ativação: {chave_ativacao}")

        assert not self._integracao_foi_configurada(), "Integração já instalada anteriormente."
        _token: str = self._gerar_token_tenant(chave_ativacao)
        _tenant = _token.split('.')[1]
        padding = '=' * (4 - len(_tenant) % 4)
        _tenant_str = base64.b64decode(_tenant + padding)
        decoded_token = json_loads(_tenant_str.decode('utf-8'))

        if grupos:
            grupos_cadastrados = self._validar_grupos_empresariais(grupos)
        else:
            grupos_cadastrados = self._integracao_dao().listar_grupos_empresariais()

        _ids  = [str(grupo['grupoempresarial']) for grupo in grupos_cadastrados]

        self._integracao_dao().registrar_grupos_empresariais(_ids)

        self._integracao_dao().registra_token_tenant(_token)

        self._log.mensagem(f"Instalação efetuada com sucesso para o tenant '{decoded_token['tenant_id']}'.")


    def ativar_grupos_empresariais(self, grupos: List[str]):

        assert self._integracao_foi_configurada(), "Integração não configurada!"
        assert grupos, "Grupos não podem ser vazios!"

        if grupos:
            grupos_cadastrados = self._validar_grupos_empresariais(grupos)
        else:
            grupos_cadastrados = self._integracao_dao().listar_grupos_empresariais()

        _ids  = [grupo['grupoempresarial'] for grupo in grupos_cadastrados]

        self._integracao_dao().registrar_grupos_empresariais(_ids)

        self._log.mensagem(f"Grupos empresariais ativados: '{','.join(grupos)}'.")


    def desativar_grupos_empresariais(self, grupos: List[str]):

        assert self._integracao_foi_configurada(), "Integração não configurada!"
        assert grupos, "Grupos não podem ser vazios!"

        grupos_cadastrados = self._validar_grupos_empresariais(grupos)

        _ids  = [grupo['grupoempresarial'] for grupo in grupos_cadastrados]

        self._integracao_dao().desativar_grupos_empresariais(_ids)

        self._log.mensagem(f"Grupos empresariais desativados: '{','.join(grupos)}'.")


    def _filtro_particionamento_de(self, entidade: str):

        if self._filtros_particionamento is None:
            _dados_part = self._integracao_dao().listar_dados_particionamento()

            self._filtros_particionamento = [
                {'grupoempresarial' : ",".join(list(map(lambda i: str(i["grupoempresarial"]), _dados_part)))},
                {'empresa' : ",".join(list(map(lambda i: str(i["empresa"]), _dados_part)))},
                {'estabelecimento' : ",".join(list(map(lambda i: str(i["estabelecimento"]), _dados_part)))}
            ]

        if entidade in _entidades_particionadas_por_grupo:
            return  self._filtros_particionamento[0]

        if entidade in _entidades_particionadas_por_empresa:
            return self._filtros_particionamento[1]

        if entidade in _entidades_particionadas_por_estabelecimento:
            return self._filtros_particionamento[2]


    def _dto_to_api(
        self,
        campos: Dict[str, List[str]],
        data: List[DTOBase]
    ) -> List[dict]:
        # Converte os objetos DTO para dicionários e adiciona o tenant
        transformed_data = []
        for dto in data:
            dto.tenant = self.tenant
            dto_dict = dto.convert_to_dict(campos)
            if "created_by" in dto_dict and not dto_dict["created_by"] is None:
                dto_dict["created_by"] = {"id": dto_dict["created_by"]}
            transformed_data.append(dto_dict)

        return transformed_data


    def _save_point_for(self, tabela: str):
        return self._save_point.get(tabela, None)

    def _do_save_point(self, tabela: str, chave):
        self._save_point[tabela] = chave
        with open('savepoint.json', 'w') as f:
            f.write(f'{{ "{tabela}": "{chave}" }} ' if chave else f'{{ "{tabela}": null }} ')

    def _save_point_clear(self):
        self._save_point.clear()
        if os.path.exists('savepoint.json'):
            os.remove('savepoint.json')


    @medir_tempo("Carga inicial")
    def executar_carga_inicial(self):

        assert self._integracao_foi_configurada(), "Integração não configurada!"

        _dao = self._integracao_dao()

        self._log.mensagem(f"{len(entidades_integracao)} entidades para processar.")

        entidades_carga_inicial = copy.copy(entidades_integracao)
        if self._save_point:
            for entidade in entidades_integracao:
                if not entidade in self._save_point:
                    entidades_carga_inicial.remove(entidade)
                else:
                    break

        for entidade in entidades_carga_inicial:

            _idx = entidades_integracao.index(entidade) + 1
            self._log.mensagem(f"Efetuando carga {entidade}, {_idx} de {len(entidades_integracao)}.")
            _count = 0

            # Carregar dados paginados para integrar
            service = self._injector.service_for(entidade, True)
            fields = self._fields_to_load(service._dto_class)
            filters = self._filtro_particionamento_de(entidade)
            search_query = None

            pagina = 0
            self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Extraindo dados para carga.")
            while True:
                current_after = self._save_point_for(entidade)
                _data = service.list(
                        current_after,
                        TAMANHO_PAGINA,
                        fields,
                        None,
                        filters,
                        search_query=search_query,
                    )

                _count = _count + len(_data)

                if len(_data)==0:
                    if current_after is None:
                        self._log.mensagem("Sem dados para transferir, indo adiante...")
                    else:
                        self._log.mensagem("Entidade integrada com sucesso.")
                    break

                self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {_count} registros...")

                dict_data = self._dto_to_api(fields, _data)

                # Mandar a bagatela por apis
                # if entidade == 'persona.lotacoes':
                #     pagina = pagina +1
                #     with open(f'{entidade.split(".")[1]}_data_{pagina}.json', 'w') as f:
                #         d = json_dumps(dict_data)
                #         f.write(d)

                self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Enviando dados para a api.")
                self._enviar_dados(dict_data, entidade.split('.')[1])

                # Aponta a leitura para a próxima página
                _last = _data[-1]
                self._do_save_point(entidade, getattr(_last, _last.pk_field))

            _dao.atualiza_ultima_integracao(entidade)
            self._save_point_clear()

        print("\033[92mCarga inicial finalizada com sucesso!\033[0m")


    @medir_tempo("Integração")
    def executar_integracao(self):

        assert self._integracao_foi_configurada(), "Integração não configurada!"

        _dao = self._integracao_dao()

        entidades_pendentes = _dao.listar_entidades_pendentes_integracao()

        entidades_pendentes = {entidade: entidades_pendentes[entidade] for entidade in entidades_integracao if entidade in entidades_pendentes.keys()}

        self._log.mensagem(f"{len(entidades_pendentes)} entidades para processar." if entidades_pendentes else "Nenhuma entidade para processar.")
        _resumo = {}

        for entidade, data_ultima_integracao in entidades_pendentes.items():

            _idx = list(entidades_pendentes.keys()).index(entidade) + 1
            self._log.mensagem(f"Integrando {entidade}, {_idx} de {len(entidades_pendentes)}.")
            _count = 0

            # Carregar dados paginados para integrar
            service = self._injector.service_for(entidade, True)
            current_after = None
            fields = self._fields_to_load(service._dto_class) #tornar publico
            filters = self._filtro_particionamento_de(entidade)
            search_query = None
            _acao = entidade.split('.')[1]

            # Dados criados apos data_ultima_integracao
            # filtro_criacao = filters.copy() if filters else {}
            # filtro_criacao['created_at'] = data_ultima_integracao
            # while True:

            #     data = service.list(
            #             current_after,
            #             TAMANHO_PAGINA,
            #             fields,
            #             None,
            #             filtro_criacao,
            #             search_query=search_query,
            #         )

            #     if len(data)==0:
            #         self._log.mensagem("Sem dados para criar, indo adiante...")
            #         break

            #     # Convertendo para o formato de dicionário (permitindo omitir campos do DTO) e add tenant
            #     dict_data = self._dto_to_api(fields, data)

            #     # Mandar a bagatela por apis
            #     self._enviar_dados(dict_data, _acao)

            #     # Aponta a leitura para a próxima página
            #     _last = data[-1]
            #     current_after = getattr(_last, _last.pk_field)

            # Dados alterados apos data_ultima_integracao
            filtro_atualizacao = filters.copy() if filters else {}
            filtro_atualizacao['lastupdate'] = data_ultima_integracao
            while True:

                _data = service.list(
                        current_after,
                        TAMANHO_PAGINA,
                        fields,
                        None,
                        filtro_atualizacao,
                        search_query=search_query,
                    )

                _count = _count + len(_data)

                if len(_data)==0:
                    if current_after is None:
                        self._log.mensagem("Sem dados para atualizar, indo adiante...")
                    else:
                        self._log.mensagem("Entidade integrada com sucesso.")
                    break

                self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {_count} registros...")
                _resumo[entidade] = _count

                # Convertendo para o formato de dicionário (permitindo omitir campos do DTO) e add tenant
                dict_data = self._dto_to_api(fields, _data)

                # Mandar a bagatela por apis
                self._enviar_dados(dict_data, _acao)

                # Aponta a leitura para a próxima página
                _last = _data[-1]
                current_after = getattr(_last, _last.pk_field)

            # Dados excluidos apos data_ultima_integracao
            _coluna_id = service._dto_class.fields_map[service._dto_class.pk_field].entity_field
            para_apagar = _dao.listar_dados_exclusao(_coluna_id, entidade, data_ultima_integracao)
            if para_apagar:
                _resumo[entidade] = _resumo.get(entidade, 0) + len(para_apagar)
                self._apagar_dados(para_apagar, _acao)

            _dao.atualiza_ultima_integracao(entidade)

        print("\033[92mIntegração finalizada com sucesso!\033[0m")
        if _resumo:
            print(f"\033[92mResumo da integração:\033[0m {', '.join(f'{k}: {v}' for k, v in _resumo.items())}")


    def integrity_fields(self, dto) -> dict:
        fields = {"root": set()}

        for _field_name in sorted(dto.integrity_check_fields_map.keys()):

            if _field_name in self._ignored_fields:
                continue

            _field_obj = dto.integrity_check_fields_map[_field_name]

            if isinstance(_field_obj, DTOField):
                fields["root"].add(_field_name)
                continue

            if isinstance(_field_obj, DTOListField):
                fields["root"].add(_field_name)
                fields.setdefault(_field_name, set())

                for _related_field in sorted(_field_obj.dto_type.integrity_check_fields_map.keys()):
                    if not _related_field in self._ignored_fields:
                        fields["root"].add(f"{_field_name}.{_related_field}")
                        fields[_field_name].add(_related_field)

        return fields
        #TODO Tranformar em uma função da biblioteca do rest lib
        # fields = {}
        # fields["root"] = {field for field in dto.fields_map.keys() if not field in ["tenant", "lastupdate"]}

        # for _related_entity, _related_list_fields in dto.list_fields_map.items():
        #     fields["root"].add(_related_entity)
        #     fields.setdefault(_related_entity, set())
        #     _related_fields = _related_list_fields.dto_type.fields_map.keys()
        #     for _related_field in _related_fields:
        #         if not _related_field in ["tenant", "lastupdate"]:
        #             fields["root"].add(f"{_related_entity}.{_related_field}")
        #             fields[_related_entity].add(_related_field)

        # return fields

    def tratar_campos_comparacao(self, dados: dict, campos_ignorados: list):

        keys_to_delete = []
        for chave, valor in dados.items():

            # Remove timezone para comparação
            if isinstance(valor, (datetime.datetime, datetime.date)):
                dados[chave] = valor.replace(microsecond=0, tzinfo=None)

            # Ignora campos não úteis
            if chave in campos_ignorados:
                keys_to_delete.append(chave)

            # Aplica regras em sublistas
            if isinstance(valor, list):
                valor.sort(key=lambda x: x['id'])
                for item in valor:
                    self.tratar_campos_comparacao(item, campos_ignorados)

        for chave in keys_to_delete:
            del dados[chave]


    def converte_dados_para_hash(self, dto, integrity_fields):

        data = dto.convert_to_dict(integrity_fields)

        self.tratar_campos_comparacao(data, self._ignored_fields)

        concatenated_valors = ''.join(
            str(data[chave]) for chave in sorted(data.keys())
        )

        data['tenant'] = self.tenant

        return {
            'id': str(data[dto.pk_field]),
            'hash': hashlib.sha256(concatenated_valors.encode('utf-8')).hexdigest(),
            '_source': data,
            '_source_hash': concatenated_valors
        }


    def comparar_dados(self, dados_referencia, dados_comparacao):

        if dados_referencia['campos']['_'] != dados_comparacao['campos']['_']:
            print(f"\033[91mExistem diferenças entre os campos comparados:\r\n\r\nLocal: {dados_referencia['campos']['_']}\r\n\r\nWeb  : {dados_comparacao['campos']['_']}\033[0m")

        if dados_referencia['registros'] != dados_comparacao['registros']:
            print(f"\033[91mExistem diferenças nas quantidades de dados:\r\n\r\nLocal: {dados_referencia['registros']}\r\n\r\nWeb  : {dados_comparacao['registros']}\033[0m")

        # Índices para facilitar busca por ID
        idx_referencia = {item['id']: item for item in dados_referencia['dados']}
        idx_comparacao = {item['id']: item for item in dados_comparacao['dados']}

        # Inicializar listas de mudanças
        _criar = []
        _atualizar = []
        _excluir = []
        _diff = {}

        # Verificar itens nos dados de referência
        for item_id, item_ref in idx_referencia.items():
            if item_id not in idx_comparacao:
                # Criar se não existe nos dados de comparação
                _criar.append(item_ref['_source'])
            elif item_ref['hash'] != idx_comparacao[item_id]['hash']:
                # Atualizar se o hash é diferente
                _atualizar.append(item_ref['_source'])
                # Adiciona para exibir os dados puros se disponível
                if '_source' in idx_comparacao[item_id]:
                    _diff[item_ref['_source_hash']] = idx_comparacao[item_id]['_source']

        # Verificar itens nos dados de comparação
        for item_id in idx_comparacao.keys():
            if item_id not in idx_referencia:
                # Excluir se não existe em A
                _excluir.append(idx_comparacao[item_id]['id'])

        return _criar, _atualizar, _excluir, _diff

    @medir_tempo("Verificação de integridade")
    def executar_verificacao_integridade(
        self,
        parar_caso_diferencas : bool = False,
        detalhar_diferencas: bool = False,
        corrigir_auto: bool = False,
        tenant: int = 0
    ):

        assert self._integracao_foi_configurada(), "Integração não configurada!"
        if corrigir_auto:
            assert self.tenant==tenant, "Tenant informado para correção não é igual ao configurado"

        _dao = self._integracao_dao()

        self._log.mensagem(f"{len(entidades_integracao)} entidades para verificar integridade.")

        _diferencas = False
        _idx = 0
        for entidade in reversed(entidades_integracao):

            if entidade == 'persona.trabalhadores':
                continue

            _idx += 1
            self._log.mensagem(f"Verificando integridade {entidade}, {_idx} de {len(entidades_integracao)}.")

            # Carregar dados paginados para integrar
            service = self._injector.service_for(entidade, False)

            _count = 0
            current_after = None
            fields = self._fields_to_load(service._dto_class)
            filters = self._filtro_particionamento_de(entidade)
            search_query = None
            _integrity_fields = self.integrity_fields(service._dto_class)
            _dados_locais = []

            self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Extraindo dados para comparação.")
            while True:

                _data = service.list(
                    current_after,
                    TAMANHO_PAGINA,
                    fields,
                    None,
                    filters,
                    search_query=search_query,
                )

                _count = _count + len(_data)

                if len(_data)==0:
                    break

                self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {_count} registros...")

                # Aponta a leitura para a próxima página
                _last = _data[-1]
                current_after = getattr(_last, _last.pk_field)

                # Convertendo para o formato de dicionário (permitindo omitir campos do DTO) e add tenant
                _cp_fields = copy.deepcopy(_integrity_fields)
                while _data:
                    dto = _data.pop(0)
                    _dados_locais.append(self.converte_dados_para_hash(dto, _cp_fields))


            _dados_locais = {
                'registros' : _count,
                'campos': {
                    "_": ",".join(sorted(_integrity_fields['root'])),
                },
                'dados': _dados_locais
            }

            self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Consultando dados da api.")


            # captura os dados de integridade da entidade
            _acao = entidade.split('.')[1]
            _dados_remotos = self.consultar_integridade_de(_acao, filters, detalhar_diferencas)


            self._log.mensagem(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Comparando dados.")

            # Compara os dados e obtem o que se deve fazer
            para_criar, para_atualizar, para_apagar, _diff = self.comparar_dados(_dados_locais, _dados_remotos)

            if para_criar:
                print(f"\r\nPara criar -> {len(para_criar)}\r\n")
                if corrigir_auto:
                    print(f"\r\nCriando dados em {entidade}.\r\n")
                    self._enviar_dados(para_criar, _acao)

            if para_atualizar:
                print(f"\r\nPara atualizar -> {len(para_atualizar)}\r\n")
                if _diff:
                    _i : int = 0
                    for _chave, _valor in _diff.items():
                        _i  += 1
                        #print(f"\r\n{_chave}<=>{_valor}\r\n")
                        self._trace_check(f"trace/integridade_{_acao}_{_i}_LOCAL.txt", _chave)
                        self._trace_check(f"trace/integridade_{_acao}_{_i}_REMOTE.txt", _valor)
                        #_du.save_to_file(f"trace/integridade_{_acao}_UPDATE.json", json_dumps(para_atualizar) )
                if corrigir_auto:
                    print(f"\r\nAtualizando dados em {entidade}.\r\n")
                    self._enviar_dados(para_atualizar, _acao)

            if para_apagar:
                print(f"\r\nPara apagar -> {len(para_apagar)}\r\n")
                if corrigir_auto:
                    print(f"\r\nRemovendo dados em {entidade}.\r\n")
                    self._apagar_dados(para_apagar, _acao)

            if not _diferencas:
                _diferencas = para_criar or para_atualizar or para_apagar

            if parar_caso_diferencas and (para_criar or para_atualizar or para_apagar) and not corrigir_auto:
                break

        if _diferencas:
            print("\033[93mOcorreram diferenças na checagem da integridade, verifique a saída.\033[0m")

        if not _diferencas:
            print("\033[92mVerificação finalizada sem diferenças!\033[0m")
