#!/usr/local/bin/python3.8
"""
Conector de dados -> Contextia Data MCP.

    SUPERADO POR connector.py. NAO EDITE ESTE ARQUIVO.

    Este e o conector escrito a mao para o HubCount. Ele continua funcionando e
    e o que rodou a primeira carga real, mas o que ele tem de especifico do
    cliente foi extraido para dois arquivos declarativos:

        mappings/hubcount.json   as 26 views, os grupos e os 290 campos
                                 descritos (equivalencia provada campo a campo)
        sources/hubcount.py      a conexao por tunel SSH

    e o motor virou connector.py, que e o mesmo para todo cliente:

        python3 connector.py --mapping=mappings/hubcount.json --source=hubcount

    Mexer aqui em vez de mexer no mapeamento faz o trabalho parar de acumular:
    o mapeamento e o artefato que a skill de onboarding gera para cada cliente
    novo, e o mesmo que o agente instalado vai consumir quando existir.

    Mantido apenas ate a carga rodar pelo motor generico contra a origem real.

Adaptado do conector HubCount. O que mudou:

  * Destino: a API de ingestao do Contextia (POST /api/v1/intake/json), que
    autentica por uma unica chave `Authorization: Bearer ctx_...`. A chave ja
    identifica tenant + projeto no lado do Contextia, entao TODA a resolucao de
    tenant/empresa que o script original fazia (tabela MARKETPLACE, os globais
    l_tenant_id / l_hubcount_*, o base64 nos parametros) deixou de existir.

  * Sem filtro por empresa: as views sao lidas inteiras. O antigo
    `has_empresa_id` virou apenas documentacao de qual view TEM a coluna
    EMPRESA_ID -- util para saber se da para reativar o recorte depois, mas
    nenhuma consulta filtra mais nada.

  * ENVELOPE POR DATASET (formato atual). Cada lote vai como

        {"dataset": "contas_receber", "description": "...", "key": "ID",
         "fields": {"VALOR": "Valor do titulo", ...}, "records": [ ... ]}

    e cada registro vai CRU, exatamente como saiu da view.

    Saiu daqui o antigo bloco `_origem`, que era acrescentado a cada registro:
      - repetia o mesmo metadado em dezenas de milhares de registros;
      - sujava o payload original do cliente, que deve ser preservado intacto;
      - nao tinha traducao para importacao por ARQUIVO (um CSV nao embute
        origem linha a linha).
    A informacao que o `_origem` carregava agora esta no envelope, uma vez por
    lote: `dataset` diz de que view veio a linha, e o lote de intake registra
    quando foi recebido.

  * O envelope tambem leva a SEMANTICA dos campos (L_DESCRICOES): o Contextia
    transforma `description`/`fields` em definicoes semanticas (source `client`)
    e no item de catalogo do dataset, sem sobrescrever o que ja existir. E o que
    permite ao agente entender as colunas sem nenhum servidor de contexto.

  * `key: "ID"` liga o UPSERT no servidor: reenviar o mesmo ID ATUALIZA o
    registro em vez de acumular historico. O dataset e o ESTADO ATUAL, entao
    rodar o conector toda noite mantem a base em dia sem inflar nada.

  * Envio em lotes (L_CHUNK): o payload vai inteiro em memoria e no corpo do
    POST; uma view grande estouraria limites de upload/execucao do PHP. O
    envelope repete `dataset`/`description`/`key`/`fields` em todos os lotes --
    o servidor faz upsert do metadado, entao repetir e idempotente.

CICLO DE CARGA (`sync` + `complete`)
------------------------------------
O upsert cobre inclusao e alteracao, mas nao EXCLUSAO: um titulo cancelado e
apagado no ERP continuaria vivo no Contextia, e o agente somaria dinheiro que
nao existe mais. Por isso cada execucao do conector e uma CARGA identificada:

  * todos os lotes de um dataset vao com o mesmo `sync` (`L_SYNC`, gerado uma
    vez por execucao do conector e compartilhado por todas as views -- "a carga
    da noite de 28/08" e uma coisa so, e aparece igual no log de todos os
    datasets; a remocao continua sendo sempre POR DATASET);
  * o ULTIMO lote de cada dataset leva `complete: true`, e so entao o servidor
    marca como removido o que nao apareceu nesta carga. Registro removido que
    volta numa carga futura e ressuscitado.

REMOCAO SO COM A CARGA COMPLETA. Se qualquer lote de uma view falhou (rede,
timeout, 500), `enviar_dataset` NAO manda o `complete`: a carga vira um upsert
comum, nada e apagado e a proxima execucao completa o servico. Se o `complete`
fosse enviado assim mesmo, o servidor apagaria os registros que existem no ERP e
apenas nao chegaram -- uma falha de rede viraria perda de dado.

Pela mesma razao, uma view que ERRA na consulta e PULADA por inteiro (nem o
fechamento vai), enquanto uma view que responde ZERO LINHAS ainda envia o
envelope de fechamento com `records: []` -- ai o esvaziamento e real e o
dataset deve mesmo ficar vazio no Contextia. A diferenca entre "nao consegui
ler" e "li e nao tem nada" e a linha entre preservar e remover.

O resumo final imprime o total REMOVIDO justamente para o operador perceber uma
carga que apagou muita coisa -- quase sempre sinal de problema na origem (view
quebrada, filtro errado, banco em manutencao), nao de dado que sumiu de verdade.

O envio propriamente dito mora em `contextia_client.py` (mesma pasta), separado
para poder ser testado/reaproveitado sem o banco de origem.
"""

from models import clientes, tenants, contasreceber
from ssh_server import conn
from sqlalchemy.exc import SQLAlchemyError
import json
from logzero import logger  # type: ignore
from tqdm import tqdm  # type: ignore

# contextia_client mudou de lugar: mora em sdk/coleta/, junto do conector.
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'sdk', 'coleta'))

from contextia_client import (
    L_API_KEY,
    L_CHUNK,
    L_INTAKE_URL,
    L_SYNC,
    alchemyencoder,
    enviar_dataset,
    normalizar_dataset,
)


# ---------------------------------------------------------------------------
# Views a extrair.
#
# `tem_empresa_id` NAO filtra nada -- e so o registro de quais views trazem a
# coluna EMPRESA_ID, para quando/se o recorte por empresa voltar a ser desejado.
# ---------------------------------------------------------------------------
l_actions = [
    {
        "action": "EMPRESAS",
        "columns": ["ID", "RAZAOSOCIAL", "CNPJ"],
        "tem_empresa_id": True,
    },
    {
        "action": "TIPO_CONTABIL",
        "columns": ["ID", "DESCRICAO"],
        "tem_empresa_id": False,
    },
    {
        "action": "GRUPOSRESULTADOS",
        "columns": ["ID", "DESCRICAO"],
        "tem_empresa_id": False,
    },
    {
        "action": "GRUPOSCATEGORIAS",
        "columns": [
            "ID", "DESCRICAO", "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "GRUPOSRESULTADOS_ID", "GRUPOSRESULTADOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "CATEGORIASRESULTADOS",
        "columns": [
            "ID", "DESCRICAO", "TIPO", "NATUREZA", "EMITE_NOTA",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "GRUPOSCATEGORIAS_ID", "GRUPOSCATEGORIAS_DESCRICAO",
            "TIPO_CONTABIL_ID", "TIPO_CONTABIL_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "GRUPOSCLIENTES",
        "columns": ["ID", "DESCRICAO", "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL"],
        "tem_empresa_id": True,
    },
    {
        "action": "CLIENTES",
        "columns": [
            "ID", "RAZAOSOCIAL", "EMAIL", "EMAIL_ADICIONAL",
            "CNPJ", "CPF", "FONE", "ESTADO", "CIDADE",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "GRUPOSCLIENTES_ID", "GRUPOSCLIENTES_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "FORNECEDORES",
        "columns": [
            "ID", "RAZAOSOCIAL", "EMAIL", "CNPJ", "CPF",
            "FONE", "ESTADO", "CIDADE",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "VENDEDORES",
        "columns": [
            "ID", "RAZAOSOCIAL", "EMAIL", "FONE", "COMISSAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "GRUPOSCENTROSCUSTOS",
        "columns": ["ID", "DESCRICAO", "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL"],
        "tem_empresa_id": True,
    },
    {
        "action": "CENTROSRESULTADOS",
        "columns": [
            "ID", "DESCRICAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "GRUPOSCENTROSCUSTOS_ID", "GRUPOSCENTROSCUSTOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "STATUSTITULOS",
        "columns": ["ID", "DESCRICAO"],
        "tem_empresa_id": False,
    },
    {
        "action": "COMISSOES_EMISSAO",
        "columns": [
            "ID", "DOCUMENTO", "DATAEMISSAO", "DATAVENCIMENTO",
            "VALOR", "SALDO", "COMISSAOBASE", "COMISSAOPERCENTUAL",
            "COMISSAOVALOR",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "VENDEDORES_ID", "VENDEDORES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "STATUSTITULOS_ID", "STATUSTITULOS_DESCRICAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "COMISSOES_RECEBIMENTO",
        "columns": [
            "ID", "DOCUMENTO", "DATAEMISSAO", "DATAVENCIMENTO",
            "VALOR", "SALDO", "COMISSAOBASE", "COMISSAOPERCENTUAL",
            "COMISSAOVALOR",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "VENDEDORES_ID", "VENDEDORES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "TIPOSCONTASFINANCEIRAS",
        "columns": ["ID", "DESCRICAO", "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL"],
        "tem_empresa_id": False,
    },
    {
        "action": "CONTAS_FINANCEIRAS",
        "columns": [
            "ID", "NOME", "DATASALDOSISTEMA", "SALDOINICIALSISTEMA",
            "DATASALDOBANCO", "SALDOINICIALBANCO", "INTEGRARCONTABILIDADE",
            "AGENCIA", "NUMERO", "DIGITO", "BANCO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "TIPOSCONTASFINANCEIRAS_ID", "TIPOSCONTASFINANCEIRAS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "FORMAS_PAGAMENTOS",
        "columns": ["ID", "DESCRICAO", "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL"],
        "tem_empresa_id": True,
    },
    {
        "action": "CONTAS_PAGAR",
        "columns": [
            "ID", "DOCUMENTO", "DATAEMISSAO", "DATACOMPETENCIA",
            "DATAVENCIMENTO", "DATAAGENDAMENTO", "VALOR", "SALDO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "FORNECEDORES_ID", "FORNECEDORES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "STATUSTITULOS_ID", "STATUSTITULOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "CONTAS_PAGAR_PAGAS",
        "columns": [
            "ID", "VALORJUROS", "VALORMULTA", "VALORDESCONTO", "VALOROUTROS",
            "OBSERVACOES", "DOCUMENTO", "DATAVENCIMENTO", "DATAPAGAMENTO",
            "VALOR", "SALDO", "VALORPAGO",
            "CONTAS_PAGAR_ID", "CONTAS_PAGAR_DATAEMISSAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "FORNECEDORES_ID", "FORNECEDORES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "CONTAS_FINANCEIRA_ID", "CONTAS_FINANCEIRA_NOME",
            "FORMAS_PAGAMENTOS_ID", "FORMAS_PAGAMENTOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "CONTAS_RECEBER",
        "columns": [
            "ID", "DOCUMENTO", "DATAEMISSAO", "DATACOMPETENCIA",
            "DATAVENCIMENTO", "DATAAGENDAMENTO", "VALOR", "SALDO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "STATUSTITULOS_ID", "STATUSTITULOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "CONTAS_RECEBER_RECEBIDAS",
        "columns": [
            "ID", "VALORJUROS", "VALORMULTA", "VALORDESCONTO", "VALOROUTROS",
            "OBSERVACOES", "DOCUMENTO", "DATAVENCIMENTO", "DATAPAGAMENTO",
            "VALOR", "SALDO", "VALORPAGO",
            "CONTAS_RECEBER_ID", "CONTAS_RECEBER_DATAEMISSAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "CONTAS_FINANCEIRA_ID", "CONTAS_FINANCEIRA_NOME",
            "FORMAS_PAGAMENTOS_ID", "FORMAS_PAGAMENTOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "MOVIMENTO_CAIXA",
        "columns": [
            "ID", "TIPO", "NUMERO", "DATA", "DOCUMENTO", "HISTORICO",
            "VALOR", "STATUS", "SITUACAO", "OBSERVACOES",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "FORMAS_PAGAMENTOS_ID", "FORMAS_PAGAMENTOS_DESCRICAO",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CONTAS_FINANCEIRA_ID", "CONTAS_FINANCEIRA_NOME",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "TIPOSLANCAMENTOSFINANCEIROS",
        "columns": ["ID", "DESCRICAO"],
        "tem_empresa_id": False,
    },
    {
        "action": "MOVIMENTO_GERAL_CONTAS",
        "columns": [
            "ID", "DATA", "HISTORICO", "DOCUMENTO", "MOVIMENTACAO",
            "VALOR", "CONCILIADO", "SITUACAO", "OBSERVACOES",
            "TRANSFER_CODE", "EMISSAO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "FORMAS_PAGAMENTOS_ID", "FORMAS_PAGAMENTOS_DESCRICAO",
            "CLIENTES_FORNECEDOR_ID", "CLIENTES_FORNECEDOR",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "CONTAS_FINANCEIRA_ID", "CONTAS_FINANCEIRA_NOME",
            "TIPOSLANCAMENTOSFINANCEIROS_ID",
            "TIPOSLANCAMENTOSFINANCEIROS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "NFS",
        "columns": [
            "ID", "NUMERO", "DATAEMISSAO", "STATUS",
            "VALOR_SERVICO", "VALORICMS", "VALORISS", "VALORLIQUIDO",
            "TITULOSGERADOS",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "CLIENTES_ID", "CLIENTES_RAZAOSOCIAL",
            "CENTROSRESULTADOS_ID", "CENTROSRESULTADOS_DESCRICAO",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
    {
        "action": "ORCAMENTO_CATEGORIAS",
        "columns": [
            "ID", "MES", "ANO", "REALIZADO", "META", "SALDO",
            "EMPRESA_ID", "EMPRESA_RAZAOSOCIAL",
            "CATEGORIASRESULTADOS_ID", "CATEGORIASRESULTADOS_DESCRICAO",
            "GRUPOCATEGORIAS_ID", "GRUPOCATEGORIAS_DESCRICAO",
        ],
        "tem_empresa_id": True,
    },
]


# ---------------------------------------------------------------------------
# Semantica dos campos.
#
# Vira definicao semantica automatica no Contextia (subject `<dataset>.<campo>`,
# source `client`) -- e o que faz o agente entender as colunas sem servidor de
# contexto. Definicao ja existente feita por pessoa ou pelo agente NAO e
# sobrescrita pelo servidor.
#
# REGRA: descreve-se o CAMPO, nunca o significado de um VALOR. Nao ha como saber
# daqui o que significa STATUS=3 ou NATUREZA='D'; colunas assim ficam de fora
# (`fields` e opcional e parcial). Quem souber a regra de negocio documenta
# depois pelo painel, e a definicao dele prevalece.
#
# L_CAMPOS_COMUNS: colunas que se repetem em varias views (chaves estrangeiras
# e identificacao). Sao mescladas automaticamente, e o dicionario especifico da
# action tem prioridade sobre elas.
# ---------------------------------------------------------------------------
L_CAMPOS_COMUNS = {
    "EMPRESA_ID":                        "Codigo da empresa a que o registro pertence",
    "EMPRESA_RAZAOSOCIAL":               "Nome ou razao social da empresa",
    "CLIENTES_ID":                       "Codigo do cliente",
    "CLIENTES_RAZAOSOCIAL":              "Nome ou razao social do cliente",
    "FORNECEDORES_ID":                   "Codigo do fornecedor",
    "FORNECEDORES_RAZAOSOCIAL":          "Nome ou razao social do fornecedor",
    "VENDEDORES_ID":                     "Codigo do vendedor",
    "VENDEDORES_RAZAOSOCIAL":            "Nome ou razao social do vendedor",
    "CATEGORIASRESULTADOS_ID":           "Codigo da categoria de resultado",
    "CATEGORIASRESULTADOS_DESCRICAO":    "Nome da categoria de resultado",
    "CENTROSRESULTADOS_ID":              "Codigo do centro de resultado",
    "CENTROSRESULTADOS_DESCRICAO":       "Nome do centro de resultado",
    "GRUPOSCATEGORIAS_ID":               "Codigo do grupo de categorias",
    "GRUPOSCATEGORIAS_DESCRICAO":        "Nome do grupo de categorias",
    "GRUPOCATEGORIAS_ID":                "Codigo do grupo de categorias",
    "GRUPOCATEGORIAS_DESCRICAO":         "Nome do grupo de categorias",
    "GRUPOSRESULTADOS_ID":               "Codigo do grupo de resultados",
    "GRUPOSRESULTADOS_DESCRICAO":        "Nome do grupo de resultados",
    "GRUPOSCLIENTES_ID":                 "Codigo do grupo de clientes",
    "GRUPOSCLIENTES_DESCRICAO":          "Nome do grupo de clientes",
    "GRUPOSCENTROSCUSTOS_ID":            "Codigo do grupo de centros de custo",
    "GRUPOSCENTROSCUSTOS_DESCRICAO":     "Nome do grupo de centros de custo",
    "STATUSTITULOS_ID":                  "Codigo do status do titulo",
    "STATUSTITULOS_DESCRICAO":           "Nome do status do titulo",
    "TIPO_CONTABIL_ID":                  "Codigo do tipo contabil",
    "TIPO_CONTABIL_DESCRICAO":           "Nome do tipo contabil",
    "CONTAS_FINANCEIRA_ID":              "Codigo da conta financeira",
    "CONTAS_FINANCEIRA_NOME":            "Nome da conta financeira",
    "TIPOSCONTASFINANCEIRAS_ID":         "Codigo do tipo de conta financeira",
    "TIPOSCONTASFINANCEIRAS_DESCRICAO":  "Nome do tipo de conta financeira",
    "FORMAS_PAGAMENTOS_ID":              "Codigo da forma de pagamento",
    "FORMAS_PAGAMENTOS_DESCRICAO":       "Nome da forma de pagamento",
    "TIPOSLANCAMENTOSFINANCEIROS_ID":    "Codigo do tipo de lancamento financeiro",
    "TIPOSLANCAMENTOSFINANCEIROS_DESCRICAO": "Nome do tipo de lancamento financeiro",
    "EMAIL":                             "Endereco de e-mail",
    "FONE":                              "Telefone de contato",
    "CNPJ":                              "CNPJ (pessoa juridica)",
    "CPF":                               "CPF (pessoa fisica)",
    "ESTADO":                            "UF do endereco",
    "CIDADE":                            "Cidade do endereco",
    "DOCUMENTO":                         "Numero do documento",
    "OBSERVACOES":                       "Observacoes registradas no lancamento",
    "HISTORICO":                         "Historico (texto livre) do lancamento",
    "VALOR":                             "Valor do lancamento",
    "SALDO":                             "Saldo em aberto",
    "DATAEMISSAO":                       "Data de emissao",
    "DATAVENCIMENTO":                    "Data de vencimento",
    "DATACOMPETENCIA":                   "Data de competencia",
    "DATAAGENDAMENTO":                   "Data agendada para o pagamento",
    "DATAPAGAMENTO":                     "Data da baixa (pagamento/recebimento)",
    "VALORJUROS":                        "Valor de juros",
    "VALORMULTA":                        "Valor de multa",
    "VALORDESCONTO":                     "Valor de desconto",
    "VALOROUTROS":                       "Valor de outros acrescimos ou deducoes",
    "COMISSAOBASE":                      "Valor usado como base de calculo da comissao",
    "COMISSAOPERCENTUAL":                "Percentual de comissao aplicado",
    "COMISSAOVALOR":                     "Valor da comissao",
}

L_DESCRICOES = {
    "EMPRESAS": {
        "description": "Cadastro das empresas (matriz e filiais) do ERP",
        "fields": {
            "ID":          "Codigo interno da empresa",
            "RAZAOSOCIAL": "Nome ou razao social da empresa",
            "CNPJ":        "CNPJ da empresa",
        },
    },
    "TIPO_CONTABIL": {
        "description": "Tabela de tipos contabeis usados na classificacao das categorias",
        "fields": {
            "ID":        "Codigo interno do tipo contabil",
            "DESCRICAO": "Nome do tipo contabil",
        },
    },
    "GRUPOSRESULTADOS": {
        "description": "Grupos de resultado, nivel mais alto da classificacao de categorias",
        "fields": {
            "ID":        "Codigo interno do grupo de resultados",
            "DESCRICAO": "Nome do grupo de resultados",
        },
    },
    "GRUPOSCATEGORIAS": {
        "description": "Grupos de categorias de resultado, subordinados aos grupos de resultado",
        "fields": {
            "ID":        "Codigo interno do grupo de categorias",
            "DESCRICAO": "Nome do grupo de categorias",
        },
    },
    "CATEGORIASRESULTADOS": {
        "description": "Categorias de resultado usadas para classificar lancamentos e titulos",
        "fields": {
            "ID":        "Codigo interno da categoria de resultado",
            "DESCRICAO": "Nome da categoria de resultado",
        },
    },
    "GRUPOSCLIENTES": {
        "description": "Grupos usados para segmentar o cadastro de clientes",
        "fields": {
            "ID":        "Codigo interno do grupo de clientes",
            "DESCRICAO": "Nome do grupo de clientes",
        },
    },
    "CLIENTES": {
        "description": "Cadastro de clientes do ERP",
        "fields": {
            "ID":              "Codigo interno do cliente",
            "RAZAOSOCIAL":     "Nome ou razao social do cliente",
            "EMAIL":           "Endereco de e-mail principal do cliente",
            "EMAIL_ADICIONAL": "Endereco de e-mail adicional do cliente",
            "CNPJ":            "CNPJ do cliente (pessoa juridica)",
            "CPF":             "CPF do cliente (pessoa fisica)",
            "ESTADO":          "UF do endereco do cliente",
            "CIDADE":          "Cidade do endereco do cliente",
        },
    },
    "FORNECEDORES": {
        "description": "Cadastro de fornecedores do ERP",
        "fields": {
            "ID":          "Codigo interno do fornecedor",
            "RAZAOSOCIAL": "Nome ou razao social do fornecedor",
            "EMAIL":       "Endereco de e-mail principal do fornecedor",
            "CNPJ":        "CNPJ do fornecedor (pessoa juridica)",
            "CPF":         "CPF do fornecedor (pessoa fisica)",
            "ESTADO":      "UF do endereco do fornecedor",
            "CIDADE":      "Cidade do endereco do fornecedor",
        },
    },
    "VENDEDORES": {
        "description": "Cadastro de vendedores/representantes do ERP",
        "fields": {
            "ID":          "Codigo interno do vendedor",
            "RAZAOSOCIAL": "Nome ou razao social do vendedor",
            "EMAIL":       "Endereco de e-mail do vendedor",
            "COMISSAO":    "Comissao cadastrada para o vendedor",
        },
    },
    "GRUPOSCENTROSCUSTOS": {
        "description": "Grupos de centros de custo/resultado",
        "fields": {
            "ID":        "Codigo interno do grupo de centros de custo",
            "DESCRICAO": "Nome do grupo de centros de custo",
        },
    },
    "CENTROSRESULTADOS": {
        "description": "Centros de resultado (centros de custo) usados para ratear lancamentos",
        "fields": {
            "ID":        "Codigo interno do centro de resultado",
            "DESCRICAO": "Nome do centro de resultado",
        },
    },
    "STATUSTITULOS": {
        "description": "Tabela de status dos titulos financeiros",
        "fields": {
            "ID":        "Codigo interno do status do titulo",
            "DESCRICAO": "Nome do status do titulo",
        },
    },
    "COMISSOES_EMISSAO": {
        "description": "Comissoes apuradas na emissao dos titulos",
        "fields": {
            "ID":     "Codigo interno do titulo que gerou a comissao",
            "VALOR":  "Valor do titulo",
            "SALDO":  "Saldo em aberto do titulo",
        },
    },
    "COMISSOES_RECEBIMENTO": {
        "description": "Comissoes apuradas no recebimento dos titulos",
        "fields": {
            "ID":     "Codigo interno do titulo que gerou a comissao",
            "VALOR":  "Valor do titulo",
            "SALDO":  "Saldo em aberto do titulo",
        },
    },
    "TIPOSCONTASFINANCEIRAS": {
        "description": "Tipos de conta financeira (classificacao das contas)",
        "fields": {
            "ID":        "Codigo interno do tipo de conta financeira",
            "DESCRICAO": "Nome do tipo de conta financeira",
        },
    },
    "CONTAS_FINANCEIRAS": {
        "description": "Cadastro das contas financeiras (contas bancarias e caixas)",
        "fields": {
            "ID":                    "Codigo interno da conta financeira",
            "NOME":                  "Nome da conta financeira",
            "DATASALDOSISTEMA":      "Data do saldo inicial registrado no sistema",
            "SALDOINICIALSISTEMA":   "Valor do saldo inicial registrado no sistema",
            "DATASALDOBANCO":        "Data do saldo inicial informado pelo banco",
            "SALDOINICIALBANCO":     "Valor do saldo inicial informado pelo banco",
            "INTEGRARCONTABILIDADE": "Indicador de integracao da conta com a contabilidade",
            "AGENCIA":               "Agencia bancaria da conta",
            "NUMERO":                "Numero da conta bancaria",
            "DIGITO":                "Digito verificador da conta bancaria",
            "BANCO":                 "Banco da conta",
        },
    },
    "FORMAS_PAGAMENTOS": {
        "description": "Cadastro das formas de pagamento/recebimento",
        "fields": {
            "ID":        "Codigo interno da forma de pagamento",
            "DESCRICAO": "Nome da forma de pagamento",
        },
    },
    "CONTAS_PAGAR": {
        "description": "Titulos a pagar (contas a pagar), com valor, vencimento e saldo em aberto",
        "fields": {
            "ID":              "Codigo interno do titulo a pagar",
            "DOCUMENTO":       "Numero do documento do titulo",
            "DATAEMISSAO":     "Data de emissao do titulo",
            "DATACOMPETENCIA": "Data de competencia do titulo",
            "DATAVENCIMENTO":  "Data de vencimento do titulo",
            "VALOR":           "Valor do titulo",
            "SALDO":           "Saldo em aberto do titulo",
        },
    },
    "CONTAS_PAGAR_PAGAS": {
        "description": "Baixas (pagamentos) dos titulos a pagar",
        "fields": {
            "ID":                       "Codigo interno da baixa de pagamento",
            "DOCUMENTO":                "Numero do documento do titulo pago",
            "DATAVENCIMENTO":           "Data de vencimento do titulo pago",
            "DATAPAGAMENTO":            "Data em que o pagamento foi realizado",
            "VALOR":                    "Valor do titulo",
            "SALDO":                    "Saldo em aberto do titulo apos a baixa",
            "VALORPAGO":                "Valor efetivamente pago",
            "CONTAS_PAGAR_ID":          "Codigo do titulo a pagar que originou a baixa",
            "CONTAS_PAGAR_DATAEMISSAO": "Data de emissao do titulo a pagar de origem",
        },
    },
    "CONTAS_RECEBER": {
        "description": "Titulos a receber (contas a receber), com valor, vencimento e saldo em aberto",
        "fields": {
            "ID":              "Codigo interno do titulo a receber",
            "DOCUMENTO":       "Numero do documento do titulo",
            "DATAEMISSAO":     "Data de emissao do titulo",
            "DATACOMPETENCIA": "Data de competencia do titulo",
            "DATAVENCIMENTO":  "Data de vencimento do titulo",
            "VALOR":           "Valor do titulo",
            "SALDO":           "Saldo em aberto do titulo",
        },
    },
    "CONTAS_RECEBER_RECEBIDAS": {
        "description": "Baixas (recebimentos) dos titulos a receber",
        "fields": {
            "ID":                         "Codigo interno da baixa de recebimento",
            "DOCUMENTO":                  "Numero do documento do titulo recebido",
            "DATAVENCIMENTO":             "Data de vencimento do titulo recebido",
            "DATAPAGAMENTO":              "Data em que o recebimento foi realizado",
            "VALOR":                      "Valor do titulo",
            "SALDO":                      "Saldo em aberto do titulo apos a baixa",
            "VALORPAGO":                  "Valor efetivamente recebido",
            "CONTAS_RECEBER_ID":          "Codigo do titulo a receber que originou a baixa",
            "CONTAS_RECEBER_DATAEMISSAO": "Data de emissao do titulo a receber de origem",
        },
    },
    "MOVIMENTO_CAIXA": {
        "description": "Movimentos de caixa lancados no ERP",
        "fields": {
            "ID":        "Codigo interno do movimento de caixa",
            "NUMERO":    "Numero do movimento",
            "DATA":      "Data do movimento",
            "DOCUMENTO": "Numero do documento do movimento",
            "VALOR":     "Valor do movimento",
        },
    },
    "TIPOSLANCAMENTOSFINANCEIROS": {
        "description": "Tipos de lancamento financeiro usados no movimento das contas",
        "fields": {
            "ID":        "Codigo interno do tipo de lancamento financeiro",
            "DESCRICAO": "Nome do tipo de lancamento financeiro",
        },
    },
    "MOVIMENTO_GERAL_CONTAS": {
        "description": "Movimento (extrato) das contas financeiras",
        "fields": {
            "ID":                     "Codigo interno do movimento",
            "DATA":                   "Data do movimento",
            "DOCUMENTO":              "Numero do documento do movimento",
            "VALOR":                  "Valor do movimento",
            "CONCILIADO":             "Indicador de conciliacao do movimento",
            "TRANSFER_CODE":          "Codigo que amarra as duas pontas de uma transferencia entre contas",
            "EMISSAO":                "Data de emissao do documento do movimento",
            "CLIENTES_FORNECEDOR_ID": "Codigo do cliente ou fornecedor do movimento",
            "CLIENTES_FORNECEDOR":    "Nome do cliente ou fornecedor do movimento",
        },
    },
    "NFS": {
        "description": "Notas fiscais de servico emitidas",
        "fields": {
            "ID":             "Codigo interno da nota fiscal de servico",
            "NUMERO":         "Numero da nota fiscal",
            "DATAEMISSAO":    "Data de emissao da nota fiscal",
            "VALOR_SERVICO":  "Valor dos servicos da nota",
            "VALORICMS":      "Valor de ICMS da nota",
            "VALORISS":       "Valor de ISS da nota",
            "VALORLIQUIDO":   "Valor liquido da nota",
            "TITULOSGERADOS": "Indicador de titulos financeiros gerados a partir da nota",
        },
    },
    "ORCAMENTO_CATEGORIAS": {
        "description": "Orcamento por categoria de resultado, com meta e realizado por mes/ano",
        "fields": {
            "ID":        "Codigo interno da linha de orcamento",
            "MES":       "Mes de referencia do orcamento",
            "ANO":       "Ano de referencia do orcamento",
            "REALIZADO": "Valor realizado no periodo",
            "META":      "Valor orcado (meta) para o periodo",
            "SALDO":     "Saldo do orcamento no periodo",
        },
    },
}


# ---------------------------------------------------------------------------
# Assunto (grupo) de cada view.
#
# O Contextia usa isso para responder o inventario RESUMIDO ao agente: em vez
# de despejar as 26 views a cada conversa (~3.500 tokens so para listar), ele
# mostra os assuntos com a contagem e o agente pede o detalhe do que interessa.
#
# O criterio do agrupamento e a PERGUNTA que o dado responde, nao a estrutura da
# tabela: "quanto tenho a receber" e financeiro, "quem e esse cliente" e
# cadastro, "quanto sobrou em banco" e tesouraria. Tabela de apoio (categoria,
# centro de resultado, status) vai para classificacoes -- ninguem pergunta
# sobre elas, elas explicam as outras.
#
# A taxonomia vive no TENANT do lado do Contextia e a associacao e por NOME de
# dataset, entao mandar o grupo em todo lote e idempotente, e um cliente novo do
# ERP ja nasce classificado. Action sem grupo aqui cai em "outros" e funciona
# igual -- classificar e opcional de proposito.
# ---------------------------------------------------------------------------
L_GRUPOS = {
    "EMPRESAS":                    "Cadastros",
    "CLIENTES":                    "Cadastros",
    "FORNECEDORES":                "Cadastros",
    "VENDEDORES":                  "Cadastros",
    "GRUPOSCLIENTES":              "Cadastros",

    "CONTAS_PAGAR":                "Financeiro",
    "CONTAS_PAGAR_PAGAS":          "Financeiro",
    "CONTAS_RECEBER":              "Financeiro",
    "CONTAS_RECEBER_RECEBIDAS":    "Financeiro",

    "CONTAS_FINANCEIRAS":          "Tesouraria",
    "MOVIMENTO_CAIXA":             "Tesouraria",
    "MOVIMENTO_GERAL_CONTAS":      "Tesouraria",
    "FORMAS_PAGAMENTOS":           "Tesouraria",
    "TIPOSCONTASFINANCEIRAS":      "Tesouraria",
    "TIPOSLANCAMENTOSFINANCEIROS": "Tesouraria",

    "COMISSOES_EMISSAO":           "Comissoes",
    "COMISSOES_RECEBIMENTO":       "Comissoes",

    "NFS":                         "Fiscal",

    "ORCAMENTO_CATEGORIAS":        "Planejamento",

    "TIPO_CONTABIL":               "Classificacoes",
    "GRUPOSRESULTADOS":            "Classificacoes",
    "GRUPOSCATEGORIAS":            "Classificacoes",
    "CATEGORIASRESULTADOS":        "Classificacoes",
    "GRUPOSCENTROSCUSTOS":         "Classificacoes",
    "CENTROSRESULTADOS":           "Classificacoes",
    "STATUSTITULOS":               "Classificacoes",
}


# ------------------------------------------  Semantica de uma action
def semantica_action(p_action, p_columns):
    """
    Devolve (description, fields) para a action.

    `fields` = comuns + especificos, FILTRADO pelas colunas realmente enviadas:
    descrever campo que nao vai no payload criaria definicao semantica para uma
    coluna inexistente. O especifico da action vence o comum.
    """
    loc_info        = L_DESCRICOES.get(p_action, {})
    loc_description = loc_info.get("description")

    loc_todos = dict(L_CAMPOS_COMUNS)
    loc_todos.update(loc_info.get("fields", {}))

    loc_fields = {}
    for col in p_columns:
        if col in loc_todos:
            loc_fields[col] = loc_todos[col]

    return loc_description, loc_fields


# ------------------------------------------  Consultar a view HC_<ACTION>
def buscar_dados_view(p_action, p_columns):
    """Le a view inteira -- sem recorte por empresa/tenant."""
    loc_view    = "HC_" + p_action
    loc_colunas = ", ".join(p_columns)

    sql     = "select " + loc_colunas + " from " + loc_view
    result  = conn.execute(sql)
    records = result.fetchall()

    if not records:
        return []

    return json.loads(
        json.dumps(
            [{k.upper(): v for k, v in dict(r).items()} for r in records],
            default=alchemyencoder,
        )
    )


# ------------------------------------------  Processar uma view de ponta a ponta
def processar_action(p_action, p_columns):
    """
    Le a view e envia o dataset como UMA carga (mesmo `sync`, `complete` no
    ultimo lote).

    Devolve (registros_enviados, lotes_com_erro, removidos).

    ERRO DE LEITURA != VIEW VAZIA. Se a consulta falhou, o conector nao sabe o
    que a origem tem e sai SEM ENVIAR NADA -- nem o fechamento --, para que o
    servidor nao remova registros por causa de um banco fora do ar. Se a
    consulta respondeu zero linhas, a carga acontece do mesmo jeito com
    `records: []`: a origem esvaziou de verdade e o dataset deve esvaziar junto.
    """
    logger.info("Consultando view HC_%s ...", p_action)

    try:
        loc_registros = buscar_dados_view(p_action, p_columns)
    except SQLAlchemyError as e:
        logger.error("  [ERRO SQLAlchemy] view HC_%s: %s", p_action, str(e.__dict__.get("orig")))
        logger.error("  Carga de %s ABORTADA (nada enviado, nada removido).", p_action)
        return 0, 1, 0
    except Exception as e:
        logger.error("  [ERRO GERAL] view HC_%s: %s", p_action, str(e))
        logger.error("  Carga de %s ABORTADA (nada enviado, nada removido).", p_action)
        return 0, 1, 0

    loc_dataset = normalizar_dataset(p_action)
    loc_description, loc_fields = semantica_action(p_action, p_columns)

    if not loc_registros:
        logger.warning(
            "  HC_%s respondeu ZERO linhas. Enviando so o fechamento da carga:"
            " o dataset %s sera esvaziado no Contextia.", p_action, loc_dataset,
        )

    # Todas as views HC_* expoem ID. Se alguma nao expuser, o envelope vai sem
    # `key` e o servidor cai na deduplicacao por hash do conteudo.
    loc_key = "ID" if "ID" in p_columns else None

    loc_res = enviar_dataset(
        loc_dataset, loc_registros,
        p_key=loc_key,
        p_description=loc_description,
        p_fields=loc_fields,
        p_group=L_GRUPOS.get(p_action),
        p_chunk=L_CHUNK,
        p_logger=logger,
        p_sync=L_SYNC,
    )

    logger.info(
        "  [OK] dataset=%s | lidos=%s | enviados=%s | lotes=%s (erro=%s)"
        " | novos=%s atualizados=%s iguais=%s removidos=%s ressuscitados=%s"
        " | ativos=%s | carga fechada=%s | campos descritos=%s",
        loc_dataset, len(loc_registros), loc_res["registros"],
        loc_res["lotes"], loc_res["lotes_erro"],
        loc_res["inserted"], loc_res["updated"], loc_res["duplicates"],
        loc_res["removed"], loc_res["resurrected"],
        loc_res["active_count"], "sim" if loc_res["complete_enviado"] else "NAO",
        len(loc_fields),
    )

    return loc_res["registros"], loc_res["lotes_erro"], loc_res["removed"]


# ------------------------------------------  Execucao
logger.info("Enviando para %s", L_INTAKE_URL)
logger.info("Chave: %s... (identifica tenant e projeto no Contextia)", L_API_KEY[:12])
logger.info("Carga (sync): %s -- o mesmo em todos os lotes desta execucao", L_SYNC)

l_total_registros = 0
l_total_erros     = 0
l_total_removidos = 0

for row_action in tqdm(l_actions):
    registros, erros, removidos = processar_action(
        row_action["action"],
        row_action["columns"],
    )
    l_total_registros += registros
    l_total_erros     += erros
    l_total_removidos += removidos

print("|----------------------------------------|")
print("Processo finalizado!")
print("  Carga (sync) ........: %s" % L_SYNC)
print("  Registros enviados ..: %s" % l_total_registros)
print("  Registros removidos .: %s" % l_total_removidos)
print("  Lotes com erro ......: %s" % l_total_erros)
print("|----------------------------------------|")
if l_total_removidos:
    print("ATENCAO: %s registro(s) foram marcados como REMOVIDOS nesta carga --" % l_total_removidos)
    print("estavam no Contextia e nao vieram mais da origem. Numero alto sem")
    print("motivo de negocio quase sempre e problema na ORIGEM (view quebrada,")
    print("filtro errado, banco em manutencao). Eles somem das views, mas nao sao")
    print("apagados: voltam ressuscitados assim que reaparecerem numa carga.")
if l_total_erros:
    print("ATENCAO: %s lote(s) falharam. Os datasets afetados NAO tiveram a carga" % l_total_erros)
    print("fechada (`complete` nao enviado), entao nada foi removido neles.")
    print("Rode o conector de novo para completar.")
print("Cada dataset virou uma tabela consultavel por SQL no Contextia:")
print("o agente usa a connection `datasets` (query_data) e enxerga uma view")
print("por dataset -- com os campos ja descritos por `fields`, sem nenhuma")
print("configuracao de schema. Reenviar o mesmo ID ATUALIZA o registro (upsert),")
print("e o que sumiu da origem sai da view no fechamento da carga (`complete`).")
print("")
print("Documentos (contratos, politicas, PDFs) NAO passam por aqui: seguem")
print("pelo caminho de indexacao (index_sources / intake de arquivo), que")
print("gera chunks e busca textual.")
