from typing import List, Dict, Callable, Iterator

from collections import defaultdict

import warnings

import time

import os

import copy

import hashlib

import datetime

from zoneinfo import ZoneInfo

import colorama

import uuid

import sentry_sdk

import tzdata

from nsj_gcf_utils.json_util import json_loads, json_dumps, JsonLoadException

from nsj_rest_lib.service.service_base import ServiceBase
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.exception import AfterRecordNotFoundException

from sqlalchemy.exc import ProgrammingError

from nsj_integracao_api_client.infra.telemetria import (
    TelemetriaService, obter_telemetria, inicializar_telemetria
)

from nsj_integracao_api_client.service.integrador_cfg import (
    _entidades_filtros_integracao,  _entidades_integracao,
      _entidades_particionadas_por_empresa,
    _entidades_particionadas_por_estabelecimento, _entidades_particionadas_por_grupo,
    _cfg_filtros_to_dto_filtro, _ignorar_integridade, _entidades_blob,
    medir_tempo, Environment, TAMANHO_PAGINA, _E_CHECK_INT,
    TipoVerificacaoIntegridade
)

from nsj_integracao_api_client.service.cfg.tribo_pessoas import (
    _entidades_meurh_trab, _entidades_ponto, _entidades_avaliacao, _entidades_portal
)
from nsj_integracao_api_client.service.cfg.tribo_materiais import _entidades_tribo_materiais
from nsj_integracao_api_client.service.cfg.tribo_servicos import (
    _entidades_tribo_servicos, _entidades_atendimento
)

from nsj_integracao_api_client.infra.api_client import ApiClient

from nsj_integracao_api_client.infra.token_service import TokenService

from nsj_integracao_api_client.infra.injector_factory import InjectorFactory

from nsj_integracao_api_client.dao.integracao import IntegracaoDAO

from nsj_integracao_api_client.infra.debug_utils import DebugUtils as _du


colorama.init(autoreset=True)

out_func: Callable = print

_is_console = False


class IntegradorService():

    _injector : InjectorFactory = None

    _dao_intg: IntegracaoDAO = None

    _tz_br: ZoneInfo = None

    _token_service: TokenService = None

    _api_client: ApiClient = None
    
    _web_data_service = None  # Adapter para acesso direto ao banco Web

    _api_key: str = None

    _tenant: int = None

    # Caso se deseje usar um tenant diferente do especificado no token
    _forced_tenant: int = None

    _filtros_particionamento: list = None

    _save_point: dict = {}

    _ignored_fields : list = ["tenant", "lastupdate"]

    _detalhar_diferencas: bool

    _trace: bool

    _interromper_execucao: bool

    _em_execucao: bool

    _menos_log: bool

    _adicionar_data_log: bool

    _env: Environment

    _telemetria: TelemetriaService = None

    _cache_entidades_integracao : List[str] = []

    def __init__(
        self,
        injector: InjectorFactory,
        log,
        env: Environment = Environment.PROD,
        forced_tenant: int = None,
        menos_log: bool = False,
        adicionar_data_log: bool = False
    ):
        self._injector = injector
        env_cfg = self._integracao_dao().recuperar_configuracao_ambiente()
        self._env = Environment(env_cfg) if env_cfg else env
        self._logger = log
        self._forced_tenant = forced_tenant
        self._tz_br = ZoneInfo("America/Sao_Paulo")
        self._detalhar_diferencas = False
        self._trace = False
        self._api_client = ApiClient(env)
        self._token_service = TokenService()
        self._interromper_execucao = False
        self._em_execucao = False
        self._menos_log = menos_log
        self._adicionar_data_log = adicionar_data_log
        self.atualizar_entidades_integracao()

        if env == Environment.PROD:

            emp = {}
            try:
                emp = self._integracao_dao().recuperar_dados_empresa_licenciamento()
            except Exception:
                pass

            inicializar_telemetria(
                url_api="http://telemetria.nasajon.com.br/api/events",
                empresadetentora=f"{emp['Codigo']}-{emp['Nome']}" if emp else "Indefinida",
                cnpjdetentora=emp.get("Cnpj","Indefinido")
            )
            self._telemetria = obter_telemetria()


    def _log(self, msg):
        if self._adicionar_data_log and msg.replace("\n", "").replace("\r", "") != "":
            msg = f"[{datetime.datetime.now(self._tz_br).strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
        self._logger.mensagem(msg)


    def _processa_mensagens(self, msgs: Iterator[str]):
        for msg in msgs:
            if not self._menos_log:
                self._log(msg)


    def _is_missing_table_error(self, ex: ProgrammingError) -> bool:
        detail = None

        if getattr(ex, 'orig', None) is not None and getattr(ex.orig, 'args', None):
            detail = ex.orig.args[0]
        elif getattr(ex, 'args', None):
            detail = ex.args[0]

        if isinstance(detail, dict):
            return detail.get('C') == '42P01'

        return False


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


    def _trace_check(self, filename, content):

        _du.conditional_trace(
            condition=_E_CHECK_INT or self._trace,
            func=_du.save_to_file,
            filename=filename,
            content=content
        )


    def _detail_check(self, filename, content):

        _du.conditional_trace(
            condition=_E_CHECK_INT or self._detalhar_diferencas,
            func=_du.save_to_file,
            filename=filename,
            content=content
        )


    def _entidades_sistema(self, cod_sistema: str)-> list:

        match cod_sistema:
            case 'meurh' | 'meutrabalho': return _entidades_meurh_trab
            case 'ponto': return _entidades_ponto
            case 'portal': return _entidades_portal
            case 'avaliacao': return _entidades_avaliacao
            case 'pedidos' | 'estoqueapi': return _entidades_tribo_materiais
            case 'servicos' | 'crm' | 'gp': return _entidades_tribo_servicos
            case 'atendimento': return _entidades_atendimento
            case _:  return []


    def _carrega_entidades_sistemas_contratados(self)-> list:
        """
        Carrega as entidades disponíveis para integração de acordo com os sistemas contratados pelo tenant.
        Consulta a API para obter os sistemas contratados e, para cada sistema, obtém a lista de entidades
        correspondentes. Retorna uma lista única de entidades que devem ser integradas.
        """

        ambiente = self._api_client.sistemas_contratados_ambiente(self.api_key, self.tenant)

        _entidades = set()
        if 'sistemas' in ambiente:
            for sistema in ambiente['sistemas']:
                _entidades |= set(self._entidades_sistema(sistema))

        return list(_entidades)


    def _entidades_integracao(self)-> list:
        """
        Retorna a lista de entidades para integrar de acordo com a ordenação lógica das entidades configuradas
        """

        if not self._cache_entidades_integracao:

            _entidades_contratacoes = self._carrega_entidades_sistemas_contratados()
            self._cache_entidades_integracao = [_entidade for _entidade in _entidades_integracao if _entidade in _entidades_contratacoes]

        return self._cache_entidades_integracao


    def _fields_to_load_erp(self, service: ServiceBase):

        _campos_erp = set([
            _field['column_name'] for _field in self._integracao_dao().listar_campos_entidade(service._entity_class.table_name)
        ])

        _root_fields = sorted(set(service._dto_class.fields_map.keys()) & _campos_erp)

        fields = {}
        fields.setdefault("root",set(_root_fields))

        for _related_entity, _related_list_fields in service._dto_class.list_fields_map.items():
            fields["root"].add(_related_entity)
            fields.setdefault(_related_entity, set())

            _campos_erp_related = set([
                _field['column_name'] for _field in self._integracao_dao().listar_campos_entidade(_related_list_fields.entity_type.table_name)
            ])
            _related_fields = sorted(set(_related_list_fields.dto_type.fields_map.keys()) & _campos_erp_related)
            for _related_field in _related_fields:
                fields["root"].add(f"{_related_entity}.{_related_field}")
                fields[_related_entity].add(_related_field)

        return fields


    def _fields_to_load(self, dto_class: DTOBase) -> dict:
        warnings.warn("_fields_to_load está depreciado e será removido em breve.", DeprecationWarning)

        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


    @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._forced_tenant is not None:
            return self._forced_tenant

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

        return self._tenant


    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 atualizar_entidades_integracao(self):
        """Atualiza a lista de entidades para integração."""
        
        try:
            
            entidades_registrar = self._entidades_integracao()
            entidades_registradas_list = self._integracao_dao().listar_entidades_integracao()
            # Extrai o nome se vier como dict
            entidades_registradas = []
            
            for e in entidades_registradas_list:
                
                if isinstance(e, dict):
                    nome = e.get('nome') or e.get('entidade') or str(e)
                    entidades_registradas.append(nome)
                else:
                    entidades_registradas.append(str(e))

            # Normaliza para comparação
            entidades_registradas_norm = set(e.strip().lower() for e in entidades_registradas)

            for entidade in entidades_registrar:
                if entidade.strip().lower() not in entidades_registradas_norm:
                    self._log(f"Entidade '{entidade}' não está registrada no banco!")
                    self._integracao_dao().registra_entidade_integracao(entidade)
                    self._log(f"Entidade '{entidade}' registrada com sucesso.")

        except Exception as e:
            self._log(f"Erro ao atualizar entidades de integração: {str(e)}")
            sentry_sdk.capture_exception(e)

    def registrar_entidades_integracao(self):
        for entidade in _entidades_integracao:

            self._integracao_dao().registra_entidade_integracao(entidade)

            _dto = self._injector.dto_for(entidade, False)

            for field in _dto.list_fields_map.values():
                _sub_entity = field.entity_type.table_name
                self._integracao_dao().registra_entidade_integracao(_sub_entity)


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

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

        assert not self._integracao_foi_configurada(), "Integração já instalada anteriormente."
        
        # Valida grupos empresariais localmente antes de chamar a API
        if grupos:
            grupos_cadastrados = self._validar_grupos_empresariais(grupos)
            grupos_para_ativar = grupos  # Usa os códigos/UUIDs originais
        else:
            grupos_cadastrados = self._integracao_dao().listar_grupos_empresariais()
            grupos_para_ativar = [grupo['codigo'] for grupo in grupos_cadastrados]
        
        # Primeiro gera o token para obter o tenant_id
        _token: str = self._api_client.gerar_token_tenant(chave_ativacao)
        decoded_token = self._token_service.decode_token(_token)
        tenant_id = decoded_token['tenant_id']
        
        self._log(f"Token gerado para tenant {tenant_id}")

        if self._web_data_service:
            # Modo servidor: chama a API de ativação
            try:
                self._log(f"Ativando integração via API para tenant {tenant_id}...")
                resultado = self._api_client.ativar_integracao(
                    tenant=tenant_id,
                    chave_ativacao=chave_ativacao,
                    grupos_empresariais=grupos_para_ativar,
                    api_key=_token
                )
                
                self._log(f"Instalação efetuada com sucesso para o tenant '{tenant_id}'.")
                self._log(f"Grupos ativados: {resultado.get('grupos_ativados', 0)}")
                self._log(f"Entidades registradas: {resultado.get('entidades_registradas', 0)}")
                
                self.atualizar_entidades_integracao()
                
            except Exception as e:
                self._log(f"Erro ao ativar integração: {str(e)}")
                raise
        else:
            # Modo utilitário (desktop/JobManager): operações diretas no banco
            try:
                self._integracao_dao().begin()

                _ids = [grupo['id'] for grupo in grupos_cadastrados]
                self._integracao_dao().registrar_grupos_empresariais(_ids)
                self._log(f"Grupos empresariais registrados: {len(_ids)}")

                self._integracao_dao().registra_token_tenant(_token)
                self._log("Token de integração registrado.")

                self._integracao_dao().commit()
            except Exception as e:
                self._integracao_dao().rollback()
                self._log(f"Erro ao ativar integração: {str(e)}")
                raise

            self.registrar_entidades_integracao()
            self._log(f"Entidades de integração registradas.")

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


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

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

        # Valida grupos empresariais localmente
        if grupos:
            grupos_cadastrados = self._validar_grupos_empresariais(grupos)
            grupos_para_ativar = grupos  # Usa os códigos/UUIDs originais
        else:
            grupos_cadastrados = self._integracao_dao().listar_grupos_empresariais()
            grupos_para_ativar = [grupo['codigo'] for grupo in grupos_cadastrados]

        if self._web_data_service:
            # Modo servidor: usa a API
            self._log(f"Ativando grupos empresariais via API: {grupos_para_ativar}")
            resultado = self._api_client.ativar_grupos_empresariais(
                tenant=self._tenant,
                grupos=grupos_para_ativar,
                api_key=self._api_key
            )
            
            self._log(f"Grupos empresariais ativados via API: '{','.join(grupos_para_ativar)}'.")
            self._log(f"Total ativados: {resultado.get('total_ativados', 0)}")
        else:
            # Modo utilitário (desktop/JobManager): acesso direto ao banco
            _ids = [grupo['id'] for grupo in grupos_cadastrados]
            self._integracao_dao().registrar_grupos_empresariais(_ids)
            
            if grupos:
                self._log(f"Grupos empresariais ativados (modo local): '{','.join(grupos)}'.")
            else:
                _codigos = [grupo['codigo'] for grupo in grupos_cadastrados]
                self._log(f"Grupos empresariais ativados (modo local): '{','.join(_codigos)}'.")


    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!"

        # Valida grupos empresariais localmente
        grupos_cadastrados = self._validar_grupos_empresariais(grupos)
        grupos_para_desativar = grupos  # Usa os códigos/UUIDs originais

        if self._web_data_service:
            # Modo servidor: usa a API
            self._log(f"Desativando grupos empresariais via API: {grupos_para_desativar}")
            resultado = self._api_client.desativar_grupos_empresariais(
                tenant=self._tenant,
                grupos=grupos_para_desativar,
                api_key=self._api_key
            )
            
            self._log(f"Grupos empresariais desativados via API: '{','.join(grupos)}'.")
            self._log(f"Total desativados: {resultado.get('total_desativados', 0)}")
        else:
            # Modo utilitário (desktop/JobManager): acesso direto ao banco
            _ids = [grupo['id'] for grupo in grupos_cadastrados]
            self._integracao_dao().desativar_grupos_empresariais(_ids)
            
            self._log(f"Grupos empresariais desativados (modo local): '{','.join(grupos)}'.")


    def desativar_integracao(self):
        """
        Desativa a integração removendo o token de acesso.
        """
        assert self._integracao_foi_configurada(), "Integração não configurada!"

        if self._web_data_service:
            # Modo servidor: usa a API
            self._log("Desativando integração via API...")
            self._api_client.desativar_integracao(
                tenant=self._tenant,
                api_key=self._api_key
            )
            self._log("Integração desativada com sucesso via API.")
        else:
            # Modo utilitário (desktop/JobManager): acesso direto ao banco
            try:
                self._integracao_dao().begin()
                self._integracao_dao().remove_token_tenant()
                self._integracao_dao().commit()
                
                self._log("Integração desativada com sucesso (modo local).")
                
            except Exception as db_error:
                self._integracao_dao().rollback()
                self._log(f"Erro ao desativar integração via banco: {str(db_error)}")
                raise


    def _filtro_particionamento_de(self, entidade: str):

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

            assert _dados_part, "Não existem entidades empresariais cadastradas para integração!"

            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]

        return {}


    def _filtros_integracao_cfg(self, entidade: str):

        _filtro_salvo = self._integracao_dao().filtros_integracao_entidade(entidade)

        if not isinstance(_filtro_salvo, list):
            self._log(f"O filtro salvo para a entidade '{entidade}' deve ser uma lista, {_filtro_salvo} fornecido.")
            _filtro_salvo = []

        _filtros = (
            _entidades_filtros_integracao[entidade] +
            _filtro_salvo
        )
        #Garante unicidade do campo no filtro
        dict_filtros = {}
        for _item in _filtros:
            if "campo" in _item and "valor" in _item and "operador" in _item:
                dict_filtros[_item['campo']] = copy.copy(_item)
            else:
                self._log(f"O filtro salvo para a entidade '{entidade}' deve conter campo,valor e operador': {_item} fornecido.")

        return list(dict_filtros.values())


    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)

            # Implementado devido a dualidade de conversão de campos json na API que podem ser convertidos com str/dict

            if "created_by" in dto_dict and not dto_dict["created_by"] is None:
                if not isinstance(dto_dict["created_by"], dict):
                    try:
                        _value_dict = json_loads(dto_dict["created_by"])
                        dto_dict["created_by"] = _value_dict
                    except (TypeError, ValueError, JsonLoadException):
                        dto_dict["created_by"] = {"id": dto_dict["created_by"]}

            if "updated_by" in dto_dict and not dto_dict["updated_by"] is None:
                if not isinstance(dto_dict["updated_by"], dict):
                    try:
                        _value_dict = json_loads(dto_dict["updated_by"])
                        dto_dict["updated_by"] = _value_dict
                    except (TypeError, ValueError, JsonLoadException):
                        dto_dict["updated_by"] = {"id": dto_dict["updated_by"]}
            
            # Adicionando o tenant pois agora o restlib não considera a coluna no ambiente desktop.       
            if 'tenant' not in dto_dict:
                dto_dict['tenant'] = self.tenant
                
            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', encoding='utf-8') 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')


    def interromper_execucao(self):
        self._interromper_execucao = True
        self._em_execucao = False


    def em_execucao(self):
        return self._em_execucao


    def _atualiza_ultima_integracao(self, entidade: str, filtros : list):
        self._integracao_dao().atualiza_ultima_integracao(entidade, filtros)

        _dto = self._injector.dto_for(entidade, False)

        for field in _dto.list_fields_map.values():
            _sub_entity = field.entity_type.table_name
            self._integracao_dao().atualiza_ultima_integracao(_sub_entity, [])


    def _atualiza_data_ultima_integracao(self, entidade: str, data: datetime.datetime, filtros: list):
        self._integracao_dao().atualiza_data_ultima_integracao(entidade, data, filtros)

        _dto = self._injector.dto_for(entidade, False)

        for field in _dto.list_fields_map.values():
            _sub_entity = field.entity_type.table_name
            self._integracao_dao().atualiza_data_ultima_integracao(_sub_entity, data, [])


    def _enviar_dados(self, dict_data: list, entidade: str, parar_caso_erros: bool = False) -> list:
        _erros = []
        try:
            # Usa WebDataService se disponível (acesso direto ao banco Web), senão usa ApiClient (HTTP)
            if self._web_data_service:
                self._web_data_service.upsert_dados(entidade=entidade, dados=dict_data)
            else:
                self._api_client.enviar_dados(dict_data, entidade, self.api_key)
        except Exception:
            # Se o envio em bloco falhar, tenta o envio item por item
            for _item in dict_data:
                try:
                    if self._web_data_service:
                        self._web_data_service.upsert_dados(entidade=entidade, dados=[_item])
                    else:
                        self._api_client.enviar_dados([_item], entidade, self.api_key)
                except Exception as e:
                    if parar_caso_erros:
                        raise e
                    else:
                        self._log("")
                        self._log(f"\n{'-'*80}\nErro:\n{str(e)}\n{'-'*80 if _item == dict_data[-1] else ''}")
                        self._log("")
                        _erros.append(e)
                        sentry_sdk.capture_exception(e)
        return _erros


    def _apagar_dados_bulk(self, dict_data: list, entidade: str, parar_caso_erros: bool = False):
        _erros = []
        try:
            # Tenta o envio em bloco
             self._api_client.apagar_dados_bulk(dict_data, entidade, self.api_key, self.tenant)
        except Exception as e:
            if parar_caso_erros:
                raise e
            else:
                self._log("")
                self._log(f"\n{'-'*80}\nErro:\n{str(e)}\n{'-'*80}")
                self._log("")
                _erros.append(e)
                sentry_sdk.capture_exception(e)


    def _apagar_dados(self, dict_data: list, entidade: str, parar_caso_erros: bool = False) -> list:
        _erros = []
        try:
            # Tenta o envio em bloco
            mensagens = self._api_client.apagar_dados(dict_data, entidade, self.api_key, self.tenant)
            self._processa_mensagens(mensagens)
        except Exception:
            # Se o envio em bloco falhar, tenta o envio item por item
            for _item in dict_data:
                try:
                    mensagens = self._api_client.apagar_dados([_item], entidade, self.api_key, self.tenant)
                    self._processa_mensagens(mensagens)
                except Exception as e:
                    if parar_caso_erros:
                        raise e
                    else:
                        self._log("")
                        self._log(f"\n{'-'*80}\nErro:\n{str(e)}\n{'-'*80 if _item == dict_data[-1] else ''}")
                        self._log("")
                        _erros.append(e)
                        sentry_sdk.capture_exception(e)

        return _erros


    def _enviar_blobs(self, ids: list, files: list ,entidade: str, campo: str, parar_caso_erros: bool = False):
        _erros = []
        try:
            # Tenta o envio em bloco
            self._api_client.enviar_blobs(ids, files ,entidade, campo, self.tenant, self.api_key)
        except Exception:
            # Se o envio em bloco falhar, tenta o envio item por item
            for i, file in enumerate(files):
                try:
                    self._api_client.enviar_blobs([ids[i]], [file] ,entidade, campo, self.tenant, self.api_key)
                except Exception as e:
                    if parar_caso_erros:
                        raise e
                    else:
                        self._log("")
                        self._log(f"\n{'-'*80}\nErro:\n{str(e)}\n{'-'*80}")
                        self._log("")
                        _erros.append(e)
                        sentry_sdk.capture_exception(e)

        return _erros


    def _processar_blobs(self, entidade: str, service, dict_data: list, avaliar_diferencas: bool = True, parar_caso_erros: bool = False):

        if entidade in _entidades_blob:

            if service is None:
                service = self._injector.service_for(entidade, True)

            _campos = _entidades_blob[entidade]
            _ids = [i['id'] for i in dict_data]
            _pk_banco = service._entity_class.pk_field

            self._log(f"Verificando se houve alterações nos blobs da entidade {entidade}.")

            for _campo in _campos:

                _ids_blobs = []
                _file_blobs = []

                if avaliar_diferencas:
                    _ids_to_send_blob = self._api_client.consultar_hash_blob(_ids, entidade, _campo, self.tenant, self.api_key)

                    _hashes_blob_remoto = _ids_to_send_blob['result']

                    _hashes_blob_local = self._integracao_dao().listar_blobs_entidade(_pk_banco, _campo, entidade, _ids)

                    # comparar md5
                    for blob_local in _hashes_blob_local:
                        id_local = blob_local['id']
                        hash_local = blob_local['hash']
                        # Procura o hash remoto correspondente
                        hash_remoto = next((item['hash'] for item in _hashes_blob_remoto if item['id'] == id_local), None)
                        if hash_remoto != hash_local:
                            _conteudo =   blob_local['blob'] if blob_local['blob'] is not None else b""
                            _ids_blobs.append(blob_local['id'])
                            _file_blobs.append(('files', (blob_local['id'], _conteudo, 'application/octet-stream')))
                else:
                    _hashes_blob_local = self._integracao_dao().listar_blobs_entidade(_pk_banco, _campo, entidade, _ids)
                    for blob_local in _hashes_blob_local:
                        _conteudo =   blob_local['blob'] if blob_local['blob'] is not None else b""
                        _ids_blobs.append(blob_local['id'])
                        _file_blobs.append(('files', (blob_local['id'], _conteudo, 'application/octet-stream')))


                self._log(f"Enviando blobs do campo {_campo} para a api.")
                if _ids_blobs:
                    self._enviar_blobs(
                        _ids_blobs,
                        _file_blobs,
                        entidade,
                        _campo,
                        parar_caso_erros
                    )


    def _telemetria_inicio_carga_inicial(
            self,
            correlation_id: str,
            qtd_entidades: int
        ):
        if self._telemetria:
            self._telemetria.evento_inicio_carga(
                correlation_id=correlation_id,
                entidades_processar=qtd_entidades,
                tenant=self.tenant,
                ambiente=self._env.value
            )


    def _telemetria_carga_entidade(
            self,
            correlation_id: str,
            entidade: str,
            ordem: int,
            total_atualizacoes: int
        ):
        # removido estava gerando excesso de métricas
        pass
        # if self._telemetria:
        #     self._telemetria.evento_carga_entidade(
        #         correlation_id=correlation_id,
        #         entidade=entidade,
        #         ordem_processamento=ordem,
        #         total_atualizacoes=total_atualizacoes
        #     )


    def _telemetria_fim_carga_inicial(
            self,
            correlation_id: str,
            qtd_entidades: int,
            qtd_registros: int,
            duracao_ms: int,
            status:str
        ):
        if self._telemetria:
            self._telemetria.evento_fim_carga(
                correlation_id=correlation_id,
                entidades_processadas=qtd_entidades,
                total_registros=qtd_registros,
                duracao_total_ms=duracao_ms,
                status=status
            )


    def _telemetria_inicio_integracao(
            self,
            correlation_id: str,
            qtd_entidades: int
        ):
        if self._telemetria:
            self._telemetria.evento_inicio_integracao(
                correlation_id=correlation_id,
                entidades_pendentes=qtd_entidades,
                tenant=self.tenant,
                ambiente=self._env.value
            )


    def _telemetria_integracao_entidade(
            self,
            correlation_id: str,
            entidade: str,
            ordem: int,
            total_exclusoes: int,
            total_atualizacoes: int,
            data_ultima_integracao: datetime.datetime,
        ):
        # removido estava gerando excesso de métricas
        pass
        # if self._telemetria:
        #     self._telemetria.evento_integracao_entidade(
        #         correlation_id=correlation_id,
        #         entidade=entidade,
        #         ordem_processamento=ordem,
        #         total_exclusoes=total_exclusoes,
        #         total_atualizacoes=total_atualizacoes,
        #         data_ultima_integracao=data_ultima_integracao
        #     )


    def _telemetria_fim_integracao(
            self,
            correlation_id: str,
            entidades_processadas: int,
            total_atualizacoes: int,
            total_exclusoes: int,
            duracao_ms: int,
            status:str
        ):
        if self._telemetria:
            self._telemetria.evento_fim_integracao(
                correlation_id=correlation_id,
                entidades_processadas=entidades_processadas,
                total_atualizacoes=total_atualizacoes,
                total_exclusoes=total_exclusoes,
                duracao_total_ms=duracao_ms,
                status=status
            )


    def _telemetria_inicio_verificacao_integridade(self):
        if self._telemetria:
            self._telemetria.evento_inicio_verificacao(
                correlation_id=None,
                entidades_verificar=15,
                tenant=self.tenant,
                ambiente=self._env.value,
                tipo_verificacao="HASH"
            )


    def _telemetria_fim_verificacao_integridade(self):
        if self._telemetria:
            self._telemetria.evento_fim_verificacao(
                correlation_id=None,
                entidades_verificadas=15,
                total_diferencas=30,
                duracao_total_ms=300000
            )


    def _status_execucao(self, erros: bool, _parar_caso_erro: bool, interrompido: bool):
        if interrompido:
            return "INTERROMPIDO"

        if not erros:
            return "FINALIZADO"

        if erros and _parar_caso_erro:
            return "FALHA"

        if erros and not _parar_caso_erro:
            return "FINALIZADO_COM_ERROS"


    @medir_tempo("Carga inicial")
    def executar_carga_inicial(self, entidades: list, parar_caso_erros: bool = False):
        self._interromper_execucao = False
        self._em_execucao = True

        inicio = time.perf_counter()
        _count_total = 0
        correlation_id = str(uuid.uuid4())

        _erros_envio = []
        entidades_carga_inicial = []

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

            _dao = self._integracao_dao()

            assert _dao.existem_grupos_empresariais_integracao_ativos(), "Nenhum grupo empresarial ativo para integração."
            assert _dao.listar_dados_particionamento(), "Entidades empresariais inválidas para integração."

            self._log(f"Executando carga inicial para o Tenant: {self.tenant} .")
            self._log(f"{len(self._entidades_integracao())} entidades para processar.")

            entidades_carga_inicial = copy.copy(self._entidades_integracao())

            # Remover entidades que nao devem ser processadas
            if entidades:
                for entidade in entidades:
                    assert entidade in self._entidades_integracao(), f"Entidade '{entidade}' não consta como entidade para integração!"

                for entidade in self._entidades_integracao():
                    if not entidade in entidades:
                        entidades_carga_inicial.remove(entidade)

            # Remover entidades que ja foram processadas
            if not entidades:
                self._carregar_savepoint()
                if self._save_point:
                    for entidade in self._entidades_integracao():
                        if not entidade in self._save_point:
                            entidades_carga_inicial.remove(entidade)
                        else:
                            break

            self._telemetria_inicio_carga_inicial(correlation_id, len(entidades_carga_inicial))

            for entidade in entidades_carga_inicial:

                # if not entidade in ['persona.adiantamentosavulsos','persona.trabalhadores']:
                #     continue

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

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


                # Carregar dados paginados para integrar
                service = self._injector.service_for(entidade, True)
                fields = self._fields_to_load_erp(service)
                _filtros_particionamento = self._filtro_particionamento_de(entidade)
                _filtros_integracao_cfg = self._filtros_integracao_cfg(entidade)
                filters = _filtros_particionamento | _cfg_filtros_to_dto_filtro(_filtros_integracao_cfg)
                search_query = None

                #pagina = 0

                self._log("Extraindo dados para carga.")
                while True:

                    if self._interromper_execucao:
                        self._log("Processo interrompido pelo usuário.")
                        return

                    current_after = self._save_point_for(entidade)
                    try:
                        _data = service.list(
                                current_after,
                                TAMANHO_PAGINA,
                                fields,
                                None,
                                filters,
                                search_query=search_query,
                            )
                    except ProgrammingError as ex:
                        if self._is_missing_table_error(ex):
                            self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                            break
                        raise

                    _count = _count + len(_data)
                    _count_total = _count_total + _count

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

                    if self._interromper_execucao:
                        self._log("Processo interrompido pelo usuário.")
                        return

                    self._log(f"{_count} registro(s).")

                    dict_data = self._dto_to_api(fields, _data)

                    self._log("Enviando dados para a api.")
                    _erros_envio += self._enviar_dados(dict_data, entidade, parar_caso_erros)

                    self._processar_blobs(entidade, service, dict_data, False, parar_caso_erros)

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

                self._atualiza_ultima_integracao(entidade, _filtros_integracao_cfg)
                self._save_point_clear()
                self._telemetria_carga_entidade(
                    correlation_id,
                    entidade,
                    entidades_carga_inicial.index(entidade)+1,
                    _count
                )

            self._log(self._color("Carga inicial finalizada com sucesso!", "92", _is_console))

            if _erros_envio:
                self._log(self._color("Ocorreram erros que foram ignorados durante o processo, verifique:", 91, _is_console))
                self._log("")
                self._log("\n"+("-"*80)+"\n"+self._color("\n".join(str(e)+ "\n" + "-"*80 for e in _erros_envio), 91, _is_console))


        finally:
            fim = time.perf_counter()
            self._telemetria_fim_carga_inicial(
                correlation_id,
                len(entidades_carga_inicial),
                _count_total,
                (fim - inicio) * 1000,
                self._status_execucao(len(_erros_envio), parar_caso_erros, self._interromper_execucao)
            )
            self._em_execucao = False


    @medir_tempo("Integração")
    def executar_integracao_old(self):
        self._interromper_execucao = False
        self._em_execucao = True

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

            _dao = self._integracao_dao()

            assert _dao.existem_grupos_empresariais_integracao_ativos(), "Nenhum grupo empresarial ativo para integração."

            self._log(f"Executando integração para o Tenant: {self.tenant} .")

            entidades_pendentes_bd = _dao.listar_entidades_pendentes_integracao()

            # Não filtrar entidades filhas
            entidades_pendentes = {entidade: entidades_pendentes_bd[entidade] for entidade in _entidades_integracao if entidade in entidades_pendentes_bd.keys()}

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

            self._integracao_dir = f"integracao_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"

            for entidade, data_ultima_integracao in entidades_pendentes.items():


                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

                _idx = list(entidades_pendentes.keys()).index(entidade) + 1
                self._log(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_erp(service)
                _filtros_particionamento = self._filtro_particionamento_de(entidade)
                _filtros_integracao_cfg = self._filtros_integracao_cfg(entidade)
                filters = _filtros_particionamento | _cfg_filtros_to_dto_filtro(_filtros_integracao_cfg)
                search_query = None

                # 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:
                    for i in range(0, len(para_apagar), TAMANHO_PAGINA):
                        bloco_para_apagar = para_apagar[i:i+TAMANHO_PAGINA]
                        _resumo[entidade] = _resumo.get(entidade, 0) + len(bloco_para_apagar)
                        mensagens = self._api_client.apagar_dados(bloco_para_apagar, entidade, self.api_key, self.tenant)
                        self._processa_mensagens(mensagens)

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

                    if self._interromper_execucao:
                        self._log("Processo interrompido pelo usuário.")
                        return

                    try:
                        _data = service.list(
                                current_after,
                                TAMANHO_PAGINA,
                                fields,
                                None,
                                filtro_atualizacao,
                                search_query=search_query,
                            )
                    except ProgrammingError as ex:
                        if self._is_missing_table_error(ex):
                            self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                            break
                        raise

                    _count = _count + len(_data)

                    if len(_data)==0:
                        break

                    self._log(f"{_count} registro(s).")
                    _resumo[entidade] = _resumo.get(entidade, 0) + _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._api_client.enviar_dados(dict_data, entidade, self.api_key)

                    self._processar_blobs(entidade, service, dict_data)

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


                # Carrega os objetos pais a partir dos dados modificados nas entidades filhas
                lista_ids_entidades_atualizar = []
                for _chave, _campo_lista in service._dto_class.list_fields_map.items():
                    #_sub_dto      = _campo_lista.dto_type
                    _sub_entidade = _campo_lista.entity_type.table_name
                    _sub_data_ultima_integracao = entidades_pendentes_bd[_sub_entidade]
                    _campo_id_pai = _campo_lista.related_entity_field

                    #_coluna_id_filho = _sub_dto.fields_map[_sub_dto.pk_field].entity_field
                    para_apagar    = _dao.listar_dados_exclusao(_campo_id_pai, _sub_entidade, _sub_data_ultima_integracao)
                    para_atualizar = _dao.listar_dados_alteracao(_campo_id_pai, _sub_entidade, _sub_data_ultima_integracao)

                    lista_ids_entidades_atualizar = lista_ids_entidades_atualizar + para_apagar + para_atualizar


                if lista_ids_entidades_atualizar:

                    current_after = None
                    filtro_atualizacao = filters.copy() if filters else {}
                    filtro_atualizacao[_coluna_id] = ",".join([str(id) for id in lista_ids_entidades_atualizar if id is not None])

                    while True:

                        if self._interromper_execucao:
                            self._log("Processo interrompido pelo usuário.")
                            return

                        try:
                            _data = service.list(
                                    current_after,
                                    TAMANHO_PAGINA,
                                    fields,
                                    None,
                                    filtro_atualizacao,
                                    search_query=search_query,
                                )
                        except ProgrammingError as ex:
                            if self._is_missing_table_error(ex):
                                self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                                break
                            raise

                        _count = _count + len(_data)

                        if len(_data)==0:
                            break

                        self._log(f"{_count} registro(s).")
                        _resumo[entidade] = _resumo.get(entidade, 0) + _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._api_client.enviar_dados(dict_data, entidade, self.api_key)

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


                self._atualiza_ultima_integracao(entidade, _filtros_integracao_cfg)

            self._log("")
            self._log(self._color("Integração finalizada com sucesso!", 92, _is_console))
            if _resumo:
                self._log("")
                self._log(self._color(f"Resumo da integração: {', '.join(f'{k}: {v}' for k, v in _resumo.items())}", 92, _is_console))

        finally:
            self._em_execucao = False


    @medir_tempo("Integração")
    def executar_integracao(self, parar_caso_erros: bool = False):
        self._interromper_execucao = False
        self._em_execucao = True

        inicio = time.perf_counter()
        _idx = 0
        _exclusoes: int = 0
        _atualizacoes: int = 0
        correlation_id = str(uuid.uuid4())

        _erros_envio = []

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

            _dao = self._integracao_dao()

            assert _dao.existem_grupos_empresariais_integracao_ativos(), "Nenhum grupo empresarial ativo para integração."
            assert _dao.listar_dados_particionamento(), "Entidades empresariais inválidas para integração."

            self._log(f"Executando integração para o Tenant: {self.tenant} .")

            # @TODO usar uma forma de se evitar o long pooling sem usar triggers...
            entidades_pendentes_bd = _dao.listar_entidades_pendentes_integracao()

            # Não filtrar entidades filhas
            entidades_pendentes = {
                entidade: entidades_pendentes_bd[entidade]
                    for entidade in self._entidades_integracao() if entidade in entidades_pendentes_bd.keys()
            }

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

            if not entidades_pendentes:
                return

            self._integracao_dir = f"integracao_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"

            _data_integracao: Dict[str, datetime.datetime] = {}

            _total_passos = len(entidades_pendentes) * 2

            self._telemetria_inicio_integracao(correlation_id, len(entidades_pendentes))

            # Processa exclusões gerais
            self._log("")
            self._log("Iniciando integração das exclusões pendentes.")
            for entidade, data_ultima_integracao in reversed(entidades_pendentes.items()):

                # if not entidade in ['ns.enderecos']:
                #     continue

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

                _idx += 1
                if not self._menos_log:
                    self._log(f"{entidade}, {_idx} de {_total_passos}.")

                service = self._injector.service_for(entidade, True)

                _data_integracao[entidade] = datetime.datetime.now(self._tz_br)

                _coluna_id = service._dto_class.fields_map[service._dto_class.pk_field].entity_field
                # TODO: ajustar para o horário do servidor. Fiz esse recursos devido a diferenã de timestamp entre o rastro e o registro,
                #  o que fazia com que o rastros ficasse atrasados em relação aos registros e consequentemente não houvesse exclusão.
                data_ultima_integracao_ajustada = data_ultima_integracao - datetime.timedelta(hours=3)
                #print(data_ultima_integracao_ajustada)
                para_apagar = _dao.listar_dados_exclusao(_coluna_id, entidade, data_ultima_integracao_ajustada)
                if para_apagar:

                    for i in range(0, len(para_apagar), TAMANHO_PAGINA):
                        bloco_ids = para_apagar[i:i+TAMANHO_PAGINA]
                        _resumo[entidade] = _resumo.get(entidade, 0) + len(bloco_ids)
                        _resumo_exclusoes[entidade] = _resumo_exclusoes.get(entidade, 0) + len(bloco_ids)
                        self._apagar_dados_bulk(bloco_ids, entidade, parar_caso_erros)
                        _exclusoes += len(bloco_ids)

                    if not self._menos_log:
                        self._log("Entidade integrada com sucesso.")
                    self._log(f"{entidade}: {len(para_apagar)} registro(s).")
                else:
                    if not self._menos_log:
                        self._log("Sem dados para atualizar, indo adiante.")



            # Processa upserts
            self._log("")
            self._log("Iniciando integração das atualizações pendentes.")
            for entidade, data_ultima_integracao in entidades_pendentes.items():

                # if not entidade in ['ns.enderecos']:
                #     continue

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

                _idx += 1
                if not self._menos_log:
                    self._log(f"{entidade}, {_idx} de {_total_passos}.")
                _count = 0

                # Carregar dados paginados para integrar
                service = self._injector.service_for(entidade, True)
                last_current_after = None
                current_after = None
                fields = self._fields_to_load_erp(service)
                _filtros_particionamento = self._filtro_particionamento_de(entidade)
                _filtros_integracao_cfg = self._filtros_integracao_cfg(entidade)
                filters = _filtros_particionamento | _cfg_filtros_to_dto_filtro(_filtros_integracao_cfg)
                search_query = None


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

                    if self._interromper_execucao:
                        self._log("Processo interrompido pelo usuário.")
                        return

                    _data = []
                    try:
                        _data = service.list(
                                current_after,
                                TAMANHO_PAGINA,
                                fields,
                                None,
                                filtro_atualizacao,
                                search_query=search_query,
                            )

                    except AfterRecordNotFoundException as e:
                        current_after = last_current_after
                        continue

                    except ProgrammingError as ex:
                        if self._is_missing_table_error(ex):
                            self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                            break
                        raise

                    _count = _count + len(_data)

                    if len(_data)==0:
                        break

                    # if not self._menos_log:
                    #     self._log(f"{entidade}: {_count} registro(s)")

                    _resumo[entidade] = _resumo.get(entidade, 0) + _count
                    _resumo_atualizacoes[entidade] = _resumo_atualizacoes.get(entidade, 0) + _count
                    _atualizacoes += _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
                    _erros_envio += self._enviar_dados(dict_data, entidade, parar_caso_erros)

                    self._processar_blobs(entidade, service, dict_data, True, parar_caso_erros)

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


                # Carrega os objetos pais a partir dos dados modificados nas entidades filhas
                lista_ids_entidades_atualizar = []
                for _chave, _campo_lista in service._dto_class.list_fields_map.items():
                    _sub_entidade = _campo_lista.entity_type.table_name
                    _sub_data_ultima_integracao = entidades_pendentes_bd.get(_sub_entidade, data_ultima_integracao)
                    _campo_id_pai = _campo_lista.related_entity_field

                    para_apagar    = _dao.listar_dados_exclusao(_campo_id_pai, _sub_entidade, _sub_data_ultima_integracao)
                    para_atualizar = _dao.listar_dados_alteracao(_campo_id_pai, _sub_entidade, _sub_data_ultima_integracao)

                    lista_ids_entidades_atualizar = list(set(lista_ids_entidades_atualizar + para_apagar + para_atualizar))


                if lista_ids_entidades_atualizar:
                    self._log("Integrando modificações de entidades agregadas.")

                    last_current_after = None
                    current_after = None
                    filtro_atualizacao = filters.copy() if filters else {}
                    _coluna_id = service._dto_class.fields_map[service._dto_class.pk_field].entity_field

                    # A cada TAMANHO_PAGINA carrega um bloco de ids para evitar problemas de tamanho de query
                    for i in range(0, len(lista_ids_entidades_atualizar), TAMANHO_PAGINA):

                        bloco_ids = lista_ids_entidades_atualizar[i:i+TAMANHO_PAGINA]
                        # carrega os objetos filhos a partir dos ids do pai, isto é sua composição
                        filtro_atualizacao[_coluna_id] = ",".join([str(id) for id in bloco_ids if id is not None])

                        while True:

                            if self._interromper_execucao:
                                self._log("Processo interrompido pelo usuário.")
                                return

                            _data = []
                            try:

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

                            except AfterRecordNotFoundException as e:
                                current_after = last_current_after
                                continue

                            except ProgrammingError as ex:
                                if self._is_missing_table_error(ex):
                                    self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                                    break
                                raise

                            _count = _count + len(_data)

                            if len(_data)==0:
                                break

                            # if not self._menos_log:
                            #     self._log(f"{entidade}: {_count} registro(s)")
                            _resumo[entidade] = _resumo.get(entidade, 0) + _count
                            _resumo_atualizacoes[entidade] = _resumo_atualizacoes.get(entidade, 0) + _count
                            _atualizacoes += _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
                            _erros_envio += self._enviar_dados(dict_data, entidade, parar_caso_erros)

                            self._processar_blobs(entidade, service, dict_data, True, parar_caso_erros)

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

                if  _count == 0:
                    if not self._menos_log:
                        self._log("Sem dados para atualizar, indo adiante.")
                else:
                    if not self._menos_log:
                        self._log("Entidade integrada com sucesso.")
                    self._log(f"{entidade}: {_count} registro(s).")

                self._atualiza_data_ultima_integracao(entidade, _data_integracao[entidade], _filtros_integracao_cfg)

                self._telemetria_integracao_entidade(
                    correlation_id,
                    entidade,
                    _idx,
                    _resumo_exclusoes.get(entidade,0),
                    _resumo_atualizacoes.get(entidade,0),
                    _data_integracao[entidade]
                )

            self._log("")
            self._log(self._color("Integração finalizada com sucesso!", 92, _is_console))

            if _erros_envio:
                self._log(self._color("Ocorreram erros que foram ignorados durante o processo, verifique:", 91, _is_console))
                self._log("")
                self._log("\n"+("-"*80)+"\n"+self._color("\n".join(str(e)+ "\n" + "-"*80 for e in _erros_envio), 91, _is_console))

            if _resumo:
                self._log("")
                self._log(self._color(f"Resumo da integração: {', '.join(f'{k}: {v}' for k, v in _resumo.items())}", 92, _is_console))

        finally:
            fim = time.perf_counter()
            self._telemetria_fim_integracao(
                correlation_id,
                _idx,
                _atualizacoes,
                _exclusoes,
                (fim - inicio) * 1000,
                self._status_execucao(len(_erros_envio), parar_caso_erros, self._interromper_execucao)
            )
            self._em_execucao = False


    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


    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)):
                if valor.tzinfo is not None:
                    dados[chave] = valor.astimezone(self._tz_br).replace(microsecond=0, tzinfo=None)
                else:
                    dados[chave] = valor.replace(microsecond=0, tzinfo=None)


            if "created_by" in dados and not dados["created_by"] is None:
                if not isinstance(dados["created_by"], dict):
                    try:
                        _value_dict = json_loads(dados["created_by"])
                        dados["created_by"] = _value_dict
                    except (TypeError, ValueError, JsonLoadException):
                        dados["created_by"] = {"id": dados["created_by"]}

            if "updated_by" in dados and not dados["updated_by"] is None:
                if not isinstance(dados["updated_by"], dict):
                    try:
                        _value_dict = json_loads(dados["updated_by"])
                        dados["updated_by"] = _value_dict
                    except (TypeError, ValueError, JsonLoadException):
                        dados["updated_by"] = {"id": dados["updated_by"]}

            # 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_values = json_dumps(data)

        data['tenant'] = self.tenant

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

    def comparar_ids(self, dados_referencia, dados_comparacao):

        if dados_referencia['registros'] != dados_comparacao['registros']:
            self._log(self._color(f"Existem diferenças nas quantidades de dados:\r\n\r\nLocal: {dados_referencia['registros']}\r\n\r\nWeb  : {dados_comparacao['registros']}", 91, _is_console))


        # Índices para facilitar busca por ID (mantendo compatibilidade com a saída esperada do método)
        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 = []
        _excluir = []

        # 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'])

        # 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, _excluir


    def comparar_dados(self, dados_referencia, dados_comparacao):

        if dados_referencia['campos']['_'] != dados_comparacao['campos']['_']:
            self._log(self._color(f"Existem diferenças entre os campos comparados:\r\n\r\nLocal: {dados_referencia['campos']['_']}\r\n\r\nWeb  : {dados_comparacao['campos']['_']}", 91, _is_console ))

        if dados_referencia['registros'] != dados_comparacao['registros']:
            self._log(self._color(f"Existem diferenças nas quantidades de dados:\r\n\r\nLocal: {dados_referencia['registros']}\r\n\r\nWeb  : {dados_comparacao['registros']}", 91, _is_console))

        # Í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:List[tuple] = []

        # 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]:
                    a = json_loads(item_ref['_source_hash'])
                    b = json_loads(idx_comparacao[item_id]['_source'])
                    _diff.append((a,b))

        # 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


    def _log_integridade(self, msg):
        _du.save_to_file(f'{self._integridade_dir}/log_diferencas_integridade.log', msg)


    @property
    def last_integridade_log_path(self) -> str:
        """Retorna o caminho absoluto do arquivo de log da última verificação de integridade executada.
        Retorna None se nenhuma verificação foi executada ainda."""
        if not hasattr(self, '_integridade_dir') or not self._integridade_dir:
            return None
        return os.path.join(os.getcwd(), self._integridade_dir, 'log_diferencas_integridade.log')


    def _color(self, text, code, console):
        if console:
            return f"\033[{code}m{text}\033[0m"
        else:
            return text


    def _log_comparacao_objetos(self, id, obj1, obj2, caminho='', console=False):
        _out = self._log if console else self._log_integridade

        if isinstance(obj1, dict) and isinstance(obj2, dict):
            for k in set(obj1.keys()).union(obj2.keys()):
                self._log_comparacao_objetos(id, obj1.get(k), obj2.get(k), f"{caminho}.{k}" if caminho else k)
        elif isinstance(obj1, list) and isinstance(obj2, list):
            max_len = max(len(obj1), len(obj2))
            for i in range(max_len):
                item1 = obj1[i] if i < len(obj1) else None
                item2 = obj2[i] if i < len(obj2) else None
                _id = obj1[i]['id'] if not item1 is None else obj2[i]['id'] if not item2 is None else 'indefinido'
                self._log_comparacao_objetos(id, item1, item2, f"{caminho}[{_id}]")
        else:
            s1 = str(obj1)
            s2 = str(obj2)
            if s1 != s2:
                s1_pad = s1.ljust(25)
                s2_pad = s2.ljust(25)
                _id = str(id)

                _out(f"{_id:<40} {caminho:<40} {self._color(s1_pad, '31', console)} {self._color(s2_pad, '32', console)}")


    def _log_diferencas(self, entidade, data_ultima_integracao, console=False):
        _out = self._log if console else self._log_integridade
        _out("\r\n")
        _out("-" * 130)
        _out(f"Entidade: {self._color(entidade, '36', console)}")
        _out(f"Data da última integração: {self._color(data_ultima_integracao, '36', console)}")
        _out(f"{'ID':<40} {'Campo':<40} {'Local':<25} {'Nuvem':<25}")
        _out("-" * 130)


    def _log_cabecalho(self, entidade, data_ultima_integracao, console=False):
        _out = self._log if console else self._log_integridade
        _out("\r\n")
        _out("=" * 130)
        _out(f"Entidade: {self._color(entidade, '36', console)}")
        _out(f"Data da última integração: {self._color(data_ultima_integracao, '36', console)}")


    def _log_grupo(self, titulo, detalhado=False, console=False):
        _out = self._log if console else self._log_integridade
        _out("=" * 130)
        _out(f"{titulo}:")
        _out(f"{'ID':<40} {'Campo':<40} {'Local':<25} {'Nuvem':<25}" if detalhado else "ID")
        _out("=" * 130)


    def _log_id(self, _id, console=False):
        _out = self._log if console else self._log_integridade
        _out(f"{_id}")


    @medir_tempo("Verificação de integridade")
    def executar_verificacao_integridade(
        self,
        entidades: list,
        tipo_verificacao: TipoVerificacaoIntegridade = TipoVerificacaoIntegridade.HASH,
        parar_caso_diferencas : bool = False,
        detalhar_diferencas: bool = False,
        corrigir_auto: bool = False,
        tenant: int = 0,
        trace: bool = False,
        verificacao_rapida: bool = False
    ):
        self._interromper_execucao = False
        self._em_execucao = True

        try:

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

            _dao = self._integracao_dao()

            assert _dao.existem_grupos_empresariais_integracao_ativos(), "Nenhum grupo empresarial ativo para integração."
            assert _dao.listar_dados_particionamento(), "Entidades empresariais inválidas para integração."

            self._log(f"Executando verificação de integridade para o Tenant: {self.tenant} .")

            self._detalhar_diferencas = detalhar_diferencas

            self._trace = trace

            if corrigir_auto:
                assert self.tenant==tenant, "Tenant informado para correção não é igual ao configurado"

            self._integridade_dir = f"verificacao_integridade_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"

            # Remover entidades que nao devem ser processadas
            entidades_verificacao = copy.copy(self._entidades_integracao())
            if entidades:
                for entidade in entidades:
                    assert entidade in self._entidades_integracao(), f"Entidade '{entidade}' não consta como entidade para integração!"

                for entidade in self._entidades_integracao():
                    if not entidade in entidades:
                        entidades_verificacao.remove(entidade)


            self._log(f"{len(entidades_verificacao)} entidade(s) para verificar integridade.")

            _diferencas = False
            _idx = 0
            _resumo = defaultdict(list)
            _exclusoes_retentar = []
            _atualizacoes_retentar = []
            for entidade in entidades_verificacao:

                if  entidade in _ignorar_integridade:
                    self._log(f"Ignorando verificação de integridade para a entidade {entidade}.")
                    continue

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

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

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

                _count = 0
                current_after = None
                fields = self._fields_to_load_erp(service)
                _filtros_particionamento = self._filtro_particionamento_de(entidade)
                _filtros_integracao_cfg = self._filtros_integracao_cfg(entidade)
                filters = (
                    _filtros_particionamento
                    | _cfg_filtros_to_dto_filtro(_filtros_integracao_cfg)
                )
                
                # Adiciona filtros de data se necessário
                if verificacao_rapida:
                    
                    data_ate = datetime.datetime.now(self._tz_br) + datetime.timedelta(days=1)
                    data_de = data_ate - datetime.timedelta(days=7)
                    
                    filters = filters | {"data_de": self._format_data(data_de), "data_ate": self._format_data(data_ate)}

                search_query = None
                _integrity_fields = self.integrity_fields(service._dto_class)
                _dados_locais = []

                self._log("Extraindo dados para comparação.")
                pular_entidade = False
                while True:

                    if self._interromper_execucao:
                        self._log("Processo interrompido pelo usuário.")
                        return

                    try:
                        if (tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR) and not corrigir_auto:

                            _dao = self._injector.integridade_dao(service._entity_class)
                            _entity_filters = service._create_entity_filters(filters)
                            _data = _dao.list_ids_integridade(
                                after=current_after,
                                limit=TAMANHO_PAGINA,
                                order_fields=None,
                                filters=_entity_filters
                            )
                        else:

                            _data = service.list(
                                current_after,
                                TAMANHO_PAGINA,
                                fields,
                                None,
                                filters,
                                search_query=search_query,
                            )
                    except ProgrammingError as ex:
                        if self._is_missing_table_error(ex):
                            self._log(f"WARNING: {entidade}: tabela não encontrada no banco de dados. Pulando entidade.")
                            pular_entidade = True
                            break
                        raise

                    _count = _count + len(_data)

                    if len(_data)==0:
                        break

                    self._log(f"{_count} registro(s).")

                    # Aponta a leitura para a próxima página
                    _last = _data[-1]
                    if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR and not corrigir_auto:
                        current_after = _last
                    else:
                        current_after = getattr(_last, _last.pk_field)

                    # Convertendo para o formato de dicionário (permitindo omitir campos do DTO) e add tenant
                    if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR:

                        if corrigir_auto:
                            _data = self._dto_to_api(fields, _data)
                            while _data:
                                _item = _data.pop(0)
                                _dados_locais.append({
                                    'id': str(_item['id']),
                                    '_source': _item
                                })
                        else:
                            while _data:
                                _item = _data.pop(0)
                                _dados_locais.append({
                                    'id': str(_item),
                                    '_source': {'id' : _item }
                                })
                    else:

                        _cp_fields = copy.deepcopy(_integrity_fields)
                        while _data:
                            dto = _data.pop(0)
                            _dados_locais.append(self.converte_dados_para_hash(dto, _cp_fields))


                if pular_entidade:
                    continue

                if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR:
                    _campos = service._dto_class.pk_field
                else:
                    _campos = ",".join(sorted(_integrity_fields['root']))

                    #break

                _dados_locais = {
                    'registros' : _count,
                    'campos': {
                        "_": _campos,
                    },
                    'dados': _dados_locais
                }

                # captura os dados de integridade da entidade
                if self._web_data_service:
                    self._log("Consultando dados do banco Web (acesso direto).")
                else:
                    self._log("Consultando dados da API HTTP.")
                _dados = []
                _count = 0

                # Divide os filtros em lotes para evitar URLs muito longas (erro 414).
                # Cada lote tem seu próprio cursor de paginação (ultimo_id) independente,
                # evitando o erro 500 que ocorria ao usar um cursor de um lote em outro.
                if self._web_data_service:
                    lotes_filtros = [filters]  # WebDataService não sofre limite de URL
                else:
                    lotes_filtros = self._api_client._dividir_filtros_em_lotes(filters, tamanho_lote=20)

                for lote_filtros in lotes_filtros:
                    _ultimo_id = None  # cursor reiniciado a cada lote

                    while True:

                        if self._interromper_execucao:
                            self._log("Processo interrompido pelo usuário.")
                            return

                        try:
                            # Usa WebDataService se disponível, senão usa ApiClient
                            if self._web_data_service:
                                _dados_remotos = self._web_data_service.consultar_dados_integridade(
                                    entidade=entidade,
                                    campos=_campos,
                                    tipo_verificacao='ids' if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR else 'dados',
                                    filtros_cfg=_filtros_integracao_cfg,
                                    ultimo_id=_ultimo_id,
                                    limit=TAMANHO_PAGINA
                                )
                            else:
                                _dados_remotos = self._api_client.consultar_integridade_de(
                                    entidade, lote_filtros, _filtros_integracao_cfg,
                                    _ultimo_id, detalhar_diferencas, self.api_key, self.tenant,
                                    tipo_verificacao
                                )
                        except Exception as e:
                            self._log("")
                            self._log(f"\n{'-'*80}\nErro:\n{str(e)}\n{'-'*80}")
                            self._log("")
                            raise e

                        _count = _count + len(_dados_remotos['dados'])

                        if len(_dados_remotos['dados']) == 0:
                            break

                        self._log(f"{_count} registro(s).")

                        _dados = _dados + copy.copy(_dados_remotos['dados'])

                        # Cursor usa os dados da página atual (não o acumulado global),
                        # garantindo que seja um ID válido dentro do filtro do lote corrente.
                        if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR:
                            _ultimo_id = _dados_remotos['dados'][-1]
                        else:
                            _ultimo_id = _dados_remotos['dados'][-1]['id']

                _dados_remotos['dados'] = _dados
                _dados_remotos['registros'] = _count

                self._log("Comparando dados.")

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

                # Compara os dados e obtem o que se deve fazer
                para_criar, para_atualizar, para_apagar, _diff = [], [], [] ,[]

                if tipo_verificacao is TipoVerificacaoIntegridade.IDENTIFICADOR:
                    para_criar, para_apagar = self.comparar_ids(_dados_locais, _dados_remotos)
                else:
                    para_criar, para_atualizar, para_apagar, _diff = self.comparar_dados(_dados_locais, _dados_remotos)

                if para_criar or para_atualizar or para_apagar:
                    _resumo[entidade].append(self._color(f"Local: {_dados_locais['registros']}  Web: {_dados_remotos['registros']}", 93, _is_console))

                    _dt_ultima_integracao = self._dao_intg.data_ultima_integracao(entidade)
                    self._log_cabecalho(entidade, _dt_ultima_integracao, _is_console)

                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return

                if para_apagar:
                    _resumo[entidade].append(self._color(f"Para apagar -> {len(para_apagar)}", 93, _is_console))
                    self._log(self._color(f"\r\n{_resumo[entidade][-1]}\r\n", 93, _is_console))

                    self._log_grupo(f"Dados que serão excluídos: {len(para_apagar)}", False, _is_console)

                    if _diff:
                        for _desktop in para_apagar:
                            self._log_comparacao_objetos(_desktop, _desktop, None)
                    else:
                        for _desktop in para_apagar:
                            self._log_id(_desktop, _is_console)

                    if corrigir_auto:
                        self._log(f"\r\nRemovendo dados em {entidade}.\r\n")

                        for i in range(0, len(para_apagar), TAMANHO_PAGINA):
                            _dados_pagina = para_apagar[i:i+TAMANHO_PAGINA]

                            self._log(f"\r\nApagando página {i+1} de {len(para_apagar)-1}.\r\n")

                            try:
                                # Usa WebDataService se disponível, senão usa ApiClient
                                if self._web_data_service:
                                    ids_apagar = [item if isinstance(item, str) else item['id'] for item in _dados_pagina]
                                    self._web_data_service.deletar_dados_bulk(
                                        entidade=entidade,
                                        ids=ids_apagar
                                    )
                                else:
                                    self._api_client.apagar_dados_bulk(_dados_pagina, entidade, self.api_key, self.tenant)
                            except Exception:
                                _exclusoes_retentar.append((entidade, _dados_pagina))


                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return


                if para_criar:
                    _resumo[entidade].append(self._color(f"Para criar -> {len(para_criar)}", 93, _is_console))
                    self._log(self._color(f"\r\n{_resumo[entidade][-1]}\r\n", 93, _is_console))

                    self._log_grupo(f"Dados que serão criados: {len(para_criar)}", False, _is_console)

                    if _diff:
                        for _desktop in para_criar:
                            self._log_comparacao_objetos(_desktop['id'], _desktop, {})
                    else:
                        for _desktop in para_criar:
                            self._log_id(_desktop['id'], _is_console)

                    if corrigir_auto:
                        self._log(f"\r\nCriando dados em {entidade}.\r\n")

                        for i in range(0, len(para_criar), TAMANHO_PAGINA):
                            _dados_pagina = para_criar[i:i+TAMANHO_PAGINA]
                            try:
                                # Usa WebDataService se disponível, senão usa ApiClient
                                if self._web_data_service:
                                    self._web_data_service.upsert_dados(
                                        entidade=entidade,
                                        dados=_dados_pagina
                                    )
                                else:
                                    self._api_client.enviar_dados(_dados_pagina, entidade, self.api_key)
                            except Exception:
                                _atualizacoes_retentar.append((entidade, _dados_pagina))

                            self._processar_blobs(entidade, service, _dados_pagina, False)


                if self._interromper_execucao:
                    self._log("Processo interrompido pelo usuário.")
                    return


                if para_atualizar:
                    _resumo[entidade].append(self._color(f"Para atualizar -> {len(para_atualizar)}", 93, _is_console))
                    self._log(self._color(f"\r\n{_resumo[entidade][-1]}\r\n", 93, _is_console))

                    self._log_grupo(f"Dados que serão atualizados: {len(para_atualizar)}", _diff, _is_console)

                    if _diff:
                        _i : int = 0
                        for _desktop, _web in _diff:
                            _i  += 1
                            self._log_comparacao_objetos(_desktop['id'], _desktop, _web)
                            self._trace_check(f"{self._integridade_dir}/integridade_{entidade.replace('.','_')}_{_desktop['id']}_{_i}_LOCAL.txt", json_dumps(_desktop))
                            self._trace_check(f"{self._integridade_dir}/integridade_{entidade.replace('.','_')}_{_web['id']}_{_i}_REMOTE.txt", json_dumps(_web))
                    else:
                        for _desktop in para_atualizar:
                            self._log_id(_desktop['id'], _is_console)

                    if corrigir_auto:
                        self._log(f"\r\nAtualizando dados em {entidade}.\r\n")

                        for i in range(0, len(para_atualizar), TAMANHO_PAGINA):
                            _dados_pagina = para_atualizar[i:i+TAMANHO_PAGINA]
                            try:
                                # Usa WebDataService se disponível, senão usa ApiClient
                                if self._web_data_service:
                                    self._web_data_service.upsert_dados(
                                        entidade=entidade,
                                        dados=_dados_pagina
                                    )
                                else:
                                    self._api_client.enviar_dados(_dados_pagina, entidade, self.api_key)
                            except Exception:
                                _atualizacoes_retentar.append((entidade, _dados_pagina))

                            self._processar_blobs(entidade, service, _dados_pagina)


                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

            # Retentando correções que falharam em ordem reversa
            _erros_envio = []
            if _atualizacoes_retentar or _exclusoes_retentar:

                self._log("Enviando ajustes de dados que falharam na primeira tentativa.")

                for _entidade_retentativa, _dados_pagina in reversed(_exclusoes_retentar):
                    self._log(f"\r\nRemovendo dados em {_entidade_retentativa}.\r\n")
                    self._apagar_dados_bulk(_dados_pagina, _entidade_retentativa)

                for _entidade_retentativa, _dados_pagina in reversed(_atualizacoes_retentar):
                    self._log(f"\r\nCriando/Atualizando dados em {_entidade_retentativa}.\r\n")
                    _service = None
                    _erros_envio += self._enviar_dados(_dados_pagina, _entidade_retentativa)
                    self._processar_blobs(_entidade_retentativa, _service, _dados_pagina)


            if _diferencas:
                self._log(self._color("\r\nOcorreram diferenças na checagem da integridade, verifique a saída.\r\n", 93, _is_console))

            if not _diferencas:
                self._log(self._color("Verificação finalizada sem diferenças!\r\n", 92, _is_console))

            if _resumo:
                self._log(self._color("Resumo da integração:\r\n", 92, _is_console))
                for entidade, detalhes in _resumo.items():
                    self._log(f"{entidade}:  " + '\n'.join(detalhes) + "\n")

                if corrigir_auto:
                    self._log(self._color("Foram enviados dados de correção durante o processo, verifique a saída.\r\n",92, _is_console))

            if _erros_envio:
                self._log(self._color("Ocorreram erros que foram ignorados durante o processo, verifique:", 91, _is_console))
                self._log("")
                self._log("\n"+("-"*80)+"\n"+self._color("\n".join(str(e)+ "\n" + "-"*80 for e in _erros_envio), 91, _is_console))

        finally:
            self._em_execucao = False
            
    def _format_data(self, dt):                        
        if isinstance(dt, (datetime.datetime, datetime.date)):
            return dt.strftime("%Y-%m-%d %H:%M:%S.%f") if isinstance(dt, datetime.datetime) else dt.strftime("%Y-%m-%d 00:00:00.000000")
        return dt
