#!/usr/bin/env python3
"""
Cenario de demonstracao do Contextia -- dados estruturados de uma empresa
ficticia, enviados pela API de ingestao.

O QUE ESTE SCRIPT E
-------------------
Um gerador de DADOS DE TESTE. Ele nao toca na aplicacao: monta quatro datasets
coerentes entre si (clientes, categorias, contas a receber e contas a pagar) de
uma distribuidora ficticia -- a **Nortech Distribuidora** -- e os envia pelo
mesmo caminho que um cliente real usaria: POST /api/v1/intake/json com o
envelope canonico.

Nenhum dado aqui vem de cliente real: nomes, CNPJs, cidades e valores sao
inventados por um gerador deterministico.

IDEMPOTENCIA
------------
Todos os envelopes vao com `key: "ID"`, entao o servidor faz UPSERT: rodar o
script duas vezes ATUALIZA os mesmos registros em vez de duplicar. O gerador
tambem usa semente fixa (L_SEED), o que mantem os valores identicos entre
execucoes -- exceto as DATAS, que sao ancoradas em `date.today()` de proposito
(ver abaixo).

CICLO DE CARGA
--------------
Cada execucao e uma carga identificada: os lotes vao com `sync` (o mesmo em
todos, `L_SYNC` de `contextia_client`) e o ULTIMO lote de cada dataset leva
`complete: true`. So entao o servidor marca como removido o que nao apareceu na
carga. E o que `--limite=N` serve para demonstrar: reexecutar com
`--limite=10` faz os registros que sobraram de fora saírem em `removidos` (e
sumirem da view); rodar de novo sem limite traz os mesmos de volta em
`ressusc.`. Se algum lote falhar, o `complete` NAO e enviado e nada e removido
(ver `contextia_client.py`).

DATAS ANCORADAS NO HOJE
-----------------------
Vencimentos sao gerados como deslocamentos em dias a partir da data de hoje:
uma parte ja vencida ha mais de 30 dias, uma parte vencida ha menos de 30, uma
parte vencendo nos proximos 30 dias e o resto no futuro. E isso que permite
perguntar "o que vence este mes" e receber uma resposta viva em qualquer dia
que o cenario for reexecutado. Como o UPSERT e por ID, rodar de novo apenas
desloca as datas dos mesmos titulos.

STATUS_ID E OPACO DE PROPOSITO
------------------------------
Os titulos tem STATUS_ID 1, 2, 3 ou 9 e a descricao do campo enviada em
`fields` diz apenas "codigo de situacao do titulo" -- sem dizer o que cada
valor significa. O significado esta escrito em `storage/demo-docs/
glossario-financeiro.md`, que entra pela indexacao de documentos. E exatamente
o exercicio do cenario: o agente nao descobre isso pelos dados, tem que achar
no documento (ou perguntar) e registrar com `correct_context`.

USO
---
    python3 tools/cenario-demo.py
    python3 tools/cenario-demo.py --key=ctx_outra_chave
    python3 tools/cenario-demo.py --url=http://localhost/.../intake/json
    python3 tools/cenario-demo.py --limite=10   # so os 10 primeiros de cada
    python3 tools/cenario-demo.py --dry-run     # so imprime os envelopes

O envio propriamente dito e reaproveitado de `contextia_client.py` (mesma
pasta): este script so monta os registros.
"""

import argparse
import datetime
import json
import random
import sys

# 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,
    L_TIMEOUT,
    enviar_dataset,
    fatiar,
    montar_envelope,
)


# ---------------------------------------------------------------------------
# Semente do gerador. Fixa = mesmos nomes, valores e vinculos a cada execucao,
# o que faz o UPSERT por ID atualizar em vez de reescrever tudo diferente.
# ---------------------------------------------------------------------------
L_SEED = 20260828

L_QTD_CLIENTES = 40
L_QTD_RECEBER = 120
L_QTD_PAGAR = 80


# ---------------------------------------------------------------------------
# Vocabulario da empresa ficticia. Acentuacao real: o cenario tambem serve para
# provar que UTF-8 atravessa API, banco, view SQL e resposta do MCP intacto.
# ---------------------------------------------------------------------------
L_CIDADES = [
    ("Sao Paulo", "SP"), ("Santo Andre", "SP"), ("Campinas", "SP"),
    ("Ribeirao Preto", "SP"), ("Rio de Janeiro", "RJ"), ("Niteroi", "RJ"),
    ("Belo Horizonte", "MG"), ("Uberlandia", "MG"), ("Curitiba", "PR"),
    ("Londrina", "PR"), ("Florianopolis", "SC"), ("Joinville", "SC"),
    ("Porto Alegre", "RS"), ("Caxias do Sul", "RS"), ("Goiania", "GO"),
    ("Brasilia", "DF"), ("Salvador", "BA"), ("Recife", "PE"),
    ("Fortaleza", "CE"), ("Vitoria", "ES"), ("Cuiaba", "MT"),
    ("Belem", "PA"), ("Manaus", "AM"), ("Joao Pessoa", "PB"),
    ("Maceio", "AL"),
]

# Os nomes acentuados de verdade ficam aqui (a lista acima serve so de indice
# de UF; o nome exibido vem deste mapa).
L_CIDADE_ACENTO = {
    "Sao Paulo": "São Paulo",
    "Santo Andre": "Santo André",
    "Ribeirao Preto": "Ribeirão Preto",
    "Niteroi": "Niterói",
    "Uberlandia": "Uberlândia",
    "Florianopolis": "Florianópolis",
    "Goiania": "Goiânia",
    "Brasilia": "Brasília",
    "Vitoria": "Vitória",
    "Cuiaba": "Cuiabá",
    "Belem": "Belém",
    "Joao Pessoa": "João Pessoa",
    "Maceio": "Maceió",
}

L_PREFIXOS = [
    "Comércio de", "Distribuidora", "Indústria", "Metalúrgica", "Armazém",
    "Casa de", "Depósito", "Atacadista", "Mercantil", "Serviços de",
]

L_NUCLEOS = [
    "Ferragens", "Alimentos", "Embalagens", "Tintas", "Papelaria",
    "Autopeças", "Construção", "Eletrônicos", "Bebidas", "Cosméticos",
    "Vidros", "Móveis", "Uniformes", "Refrigeração", "Higiene",
]

L_SOBRENOMES = [
    "São Jorge", "Aurora", "Progresso", "Boa Vista", "Três Rios",
    "Santa Luzia", "Horizonte", "Guarani", "Ipê Amarelo", "Serra Azul",
    "Verde Vale", "Nova Época", "Rio Claro", "Alvorada", "Bandeirantes",
    "Pioneira", "União", "Cristal", "Atlântico", "Real",
]

L_SUFIXOS = ["Ltda", "S.A.", "ME", "EIRELI", "Comércio Ltda"]

L_GRUPOS = ["Varejo", "Industria", "Servicos"]

L_FORNECEDORES = [
    "Papelaria Céu Azul Ltda", "Transportadora Rápido Sul S.A.",
    "Energia Elétrica do Estado", "Imobiliária Três Marias Ltda",
    "Fornecedora Nacional de Embalagens", "Conservação e Limpeza Órion ME",
    "Telecom Integração S.A.", "Manutenção Predial Vértice Ltda",
    "Gráfica Impressão Fácil", "Distribuidora de Insumos Guairacá",
    "Consultoria Contábil Âncora", "Segurança Patrimonial Falcão Ltda",
    "Locadora de Veículos Onça Parda", "Suprimentos de Informática Byte a Byte",
    "Alimentação Coletiva Bom Prato ME",
]


# ---------------------------------------------------------------------------
# Datasets: descricao do dataset e descricao de CADA CAMPO.
#
# As descricoes falam do CAMPO (o que a coluna guarda, em que formato, o que
# referencia) -- nunca do significado dos VALORES. O servidor transforma cada
# uma numa definicao semantica `<dataset>.<campo>` com source `client`.
#
# Repare em STATUS_ID: descrito como "codigo de situacao", sem tabela de
# valores. E o buraco de contexto que o cenario quer exercitar.
# ---------------------------------------------------------------------------
L_DATASETS = {
    "clientes": {
        "description": "Cadastro de clientes da Nortech Distribuidora, extraído do ERP.",
        "key": "ID",
        "fields": {
            "ID": "Código interno do cliente no ERP; é o valor referenciado por CLIENTE_ID em contas_receber.",
            "RAZAOSOCIAL": "Razão social do cliente conforme o cadastro fiscal.",
            "CNPJ": "CNPJ do cliente, gravado com a pontuação (00.000.000/0001-00).",
            "CIDADE": "Município do endereço principal do cliente.",
            "ESTADO": "Sigla da unidade federativa do endereço principal (duas letras).",
            "GRUPO": "Grupo comercial em que o cliente foi classificado pelo setor de vendas.",
            "DATACADASTRO": "Data em que o cliente foi cadastrado, no formato AAAA-MM-DD.",
        },
    },
    "categorias": {
        "description": "Plano de categorias financeiras usado na classificação de contas a pagar e a receber.",
        "key": "ID",
        "fields": {
            "ID": "Código interno da categoria; é o valor referenciado por CATEGORIA_ID nos títulos.",
            "DESCRICAO": "Nome da categoria como aparece nos relatórios financeiros.",
            "TIPO": "Natureza da categoria: se classifica entrada ou saída de dinheiro.",
            "GRUPO": "Agrupamento gerencial usado para consolidar categorias no fechamento.",
        },
    },
    "contas_receber": {
        "description": "Títulos a receber (duplicatas emitidas contra clientes) da Nortech Distribuidora.",
        "key": "ID",
        "fields": {
            "ID": "Código interno do título a receber no ERP.",
            "CLIENTE_ID": "Código do cliente devedor; referencia o campo ID do dataset clientes.",
            "CATEGORIA_ID": "Código da categoria financeira do título; referencia o campo ID do dataset categorias.",
            "DOCUMENTO": "Número do documento fiscal ou da duplicata que originou o título.",
            "DATAEMISSAO": "Data de emissão do título, no formato AAAA-MM-DD.",
            "DATAVENCIMENTO": "Data de vencimento do título, no formato AAAA-MM-DD.",
            "VALOR": "Valor original do título, em reais.",
            "SALDO": "Parcela do valor que ainda está em aberto, em reais.",
            "STATUS_ID": "Código numérico da situação do título no ERP.",
        },
    },
    "contas_pagar": {
        "description": "Títulos a pagar (compromissos com fornecedores) da Nortech Distribuidora.",
        "key": "ID",
        "fields": {
            "ID": "Código interno do título a pagar no ERP.",
            "FORNECEDOR": "Razão social do fornecedor credor do título, gravada como texto livre.",
            "CATEGORIA_ID": "Código da categoria financeira do título; referencia o campo ID do dataset categorias.",
            "DOCUMENTO": "Número da nota fiscal ou do boleto recebido do fornecedor.",
            "DATAEMISSAO": "Data de emissão do título, no formato AAAA-MM-DD.",
            "DATAVENCIMENTO": "Data de vencimento do título, no formato AAAA-MM-DD.",
            "VALOR": "Valor original do título, em reais.",
            "SALDO": "Parcela do valor que ainda está em aberto, em reais.",
            "STATUS_ID": "Código numérico da situação do título no ERP.",
        },
    },
}


# ------------------------------------------  Utilitarios de geracao
def cidade_com_acento(p_nome):
    """A lista de cidades e ASCII para servir de indice; o nome exibido nao."""
    return L_CIDADE_ACENTO.get(p_nome, p_nome)


def gerar_cnpj(p_rng):
    """CNPJ ficticio, so com a forma visual certa (nao valida digito)."""
    return "%02d.%03d.%03d/0001-%02d" % (
        p_rng.randint(10, 99), p_rng.randint(100, 999),
        p_rng.randint(100, 999), p_rng.randint(10, 99),
    )


def dinheiro(p_rng, p_min, p_max):
    """Valor em reais com centavos variados (nunca redondo demais)."""
    return round(p_rng.uniform(p_min, p_max), 2)


def dia(p_hoje, p_offset):
    """Data ISO deslocada em dias a partir de hoje."""
    return (p_hoje + datetime.timedelta(days=p_offset)).isoformat()


# ------------------------------------------  clientes
def gerar_clientes(p_rng, p_hoje):
    loc_registros = []
    loc_usados = set()

    for loc_id in range(1, L_QTD_CLIENTES + 1):
        # Nome unico: sorteia ate nao repetir.
        while True:
            loc_nome = "%s %s %s %s" % (
                p_rng.choice(L_PREFIXOS), p_rng.choice(L_NUCLEOS),
                p_rng.choice(L_SOBRENOMES), p_rng.choice(L_SUFIXOS),
            )
            if loc_nome not in loc_usados:
                loc_usados.add(loc_nome)
                break

        loc_cidade, loc_uf = p_rng.choice(L_CIDADES)

        loc_registros.append({
            "ID": loc_id,
            "RAZAOSOCIAL": loc_nome,
            "CNPJ": gerar_cnpj(p_rng),
            "CIDADE": cidade_com_acento(loc_cidade),
            "ESTADO": loc_uf,
            "GRUPO": p_rng.choice(L_GRUPOS),
            # Cadastro entre ~5 anos atras e ~3 meses atras.
            "DATACADASTRO": dia(p_hoje, -p_rng.randint(90, 1800)),
        })

    return loc_registros


# ------------------------------------------  categorias
def gerar_categorias():
    """
    Fixas (nao sorteadas): sao o "plano de contas" a que os titulos se ligam, e
    os documentos internos citam os nomes. IDs 1-4 sao de receita e 5-8 de
    despesa -- e o que contas_receber e contas_pagar respeitam.
    """
    return [
        {"ID": 1, "DESCRICAO": "Venda de Mercadorias", "TIPO": "Receita", "GRUPO": "Operacional"},
        {"ID": 2, "DESCRICAO": "Prestação de Serviços", "TIPO": "Receita", "GRUPO": "Operacional"},
        {"ID": 3, "DESCRICAO": "Locação de Equipamentos", "TIPO": "Receita", "GRUPO": "Operacional"},
        {"ID": 4, "DESCRICAO": "Juros e Multas Recebidos", "TIPO": "Receita", "GRUPO": "Financeiro"},
        {"ID": 5, "DESCRICAO": "Compra de Mercadorias", "TIPO": "Despesa", "GRUPO": "Operacional"},
        {"ID": 6, "DESCRICAO": "Folha de Pagamento", "TIPO": "Despesa", "GRUPO": "Pessoal"},
        {"ID": 7, "DESCRICAO": "Aluguel e Condomínio", "TIPO": "Despesa", "GRUPO": "Administrativo"},
        {"ID": 8, "DESCRICAO": "Impostos e Taxas", "TIPO": "Despesa", "GRUPO": "Tributário"},
    ]


# ------------------------------------------  Janela de vencimentos
def sortear_vencimento(p_rng, p_faixa):
    """
    Deslocamento em dias, por faixa. As quatro faixas existem para que as
    perguntas do roteiro tenham resposta nao-vazia em qualquer dia:

      vencido_antigo  -> alimenta "em aberto ha mais de 30 dias" (a politica
                         de cobranca manda negativar)
      vencido_recente -> atraso dentro da tolerancia
      proximos_30     -> "o que vence este mes"
      futuro          -> carteira a vencer
    """
    if p_faixa == "vencido_antigo":
        return -p_rng.randint(31, 150)
    if p_faixa == "vencido_recente":
        return -p_rng.randint(1, 30)
    if p_faixa == "proximos_30":
        return p_rng.randint(0, 30)
    return p_rng.randint(31, 160)


def sortear_status(p_rng, p_faixa):
    """
    Situacao coerente com a faixa de vencimento. Os codigos sao os do ERP
    ficticio (1 em aberto, 2 liquidado, 3 em cobranca, 9 cancelado) -- o
    significado NAO viaja nos dados, so no glossario indexado.
    """
    if p_faixa == "vencido_antigo":
        # A maioria dos vencidos antigos ja foi para cobranca; alguns foram
        # pagos com atraso e um punhado foi cancelado.
        return p_rng.choices([3, 2, 9], weights=[60, 30, 10])[0]
    if p_faixa == "vencido_recente":
        return p_rng.choices([1, 3, 2], weights=[45, 30, 25])[0]
    if p_faixa == "proximos_30":
        return p_rng.choices([1, 2, 9], weights=[85, 12, 3])[0]
    return p_rng.choices([1, 9], weights=[96, 4])[0]


def calcular_saldo(p_rng, p_valor, p_status):
    """
    Liquidado (2) e cancelado (9) nao tem saldo. Em aberto (1) e em cobranca
    (3) mantem o saldo -- com uma minoria de pagamento parcial, para que
    SALDO < VALOR apareca e o agente tenha que escolher a coluna certa.
    """
    if p_status in (2, 9):
        return 0.0
    if p_rng.random() < 0.15:
        return round(p_valor * p_rng.uniform(0.2, 0.8), 2)
    return p_valor


def faixas(p_quantidade):
    """
    Distribuicao das faixas de vencimento: 20% vencido antigo, 17% vencido
    recente, 30% proximos 30 dias, 33% futuro.
    """
    loc_antigo = int(p_quantidade * 0.20)
    loc_recente = int(p_quantidade * 0.17)
    loc_proximo = int(p_quantidade * 0.30)
    loc_futuro = p_quantidade - loc_antigo - loc_recente - loc_proximo

    return (["vencido_antigo"] * loc_antigo + ["vencido_recente"] * loc_recente
            + ["proximos_30"] * loc_proximo + ["futuro"] * loc_futuro)


# ------------------------------------------  contas_receber
def gerar_contas_receber(p_rng, p_hoje, p_clientes):
    """
    O prazo entre emissao e vencimento segue a politica de credito escrita em
    `politica-cobranca.md`: 45 dias para o grupo Varejo, 30 para os demais.
    Coerencia dado <-> documento e o que torna o cruzamento verificavel.
    """
    loc_prazo_por_grupo = {"Varejo": 45, "Industria": 30, "Servicos": 30}
    loc_faixas = faixas(L_QTD_RECEBER)
    p_rng.shuffle(loc_faixas)

    loc_registros = []

    for loc_i in range(L_QTD_RECEBER):
        loc_id = 1000 + loc_i + 1
        loc_cliente = p_rng.choice(p_clientes)
        loc_faixa = loc_faixas[loc_i]

        loc_offset_venc = sortear_vencimento(p_rng, loc_faixa)
        loc_prazo = loc_prazo_por_grupo[loc_cliente["GRUPO"]]
        loc_status = sortear_status(p_rng, loc_faixa)
        loc_valor = dinheiro(p_rng, 180.0, 47500.0)

        loc_registros.append({
            "ID": loc_id,
            "CLIENTE_ID": loc_cliente["ID"],
            "CATEGORIA_ID": p_rng.choices([1, 2, 3, 4], weights=[65, 20, 10, 5])[0],
            "DOCUMENTO": "NF %06d/%d" % (p_rng.randint(1000, 99999), p_rng.randint(1, 3)),
            "DATAEMISSAO": dia(p_hoje, loc_offset_venc - loc_prazo),
            "DATAVENCIMENTO": dia(p_hoje, loc_offset_venc),
            "VALOR": loc_valor,
            "SALDO": calcular_saldo(p_rng, loc_valor, loc_status),
            "STATUS_ID": loc_status,
        })

    return loc_registros


# ------------------------------------------  contas_pagar
def gerar_contas_pagar(p_rng, p_hoje):
    loc_faixas = faixas(L_QTD_PAGAR)
    p_rng.shuffle(loc_faixas)

    loc_registros = []

    for loc_i in range(L_QTD_PAGAR):
        loc_id = 5000 + loc_i + 1
        loc_faixa = loc_faixas[loc_i]

        loc_offset_venc = sortear_vencimento(p_rng, loc_faixa)
        loc_status = sortear_status(p_rng, loc_faixa)
        loc_valor = dinheiro(p_rng, 120.0, 28900.0)

        loc_registros.append({
            "ID": loc_id,
            "FORNECEDOR": p_rng.choice(L_FORNECEDORES),
            "CATEGORIA_ID": p_rng.choice([5, 6, 7, 8]),
            "DOCUMENTO": "BOL %07d" % p_rng.randint(100000, 9999999),
            "DATAEMISSAO": dia(p_hoje, loc_offset_venc - p_rng.choice([15, 30, 45])),
            "DATAVENCIMENTO": dia(p_hoje, loc_offset_venc),
            "VALOR": loc_valor,
            "SALDO": calcular_saldo(p_rng, loc_valor, loc_status),
            "STATUS_ID": loc_status,
        })

    return loc_registros


# ------------------------------------------  Envio
def enviar(p_nome, p_registros, p_key, p_url, p_timeout, p_chunk):
    """
    Envia um dataset como UMA CARGA, delegando a `enviar_dataset` de
    `contextia_client.py` -- que quebra em lotes, repete o `sync` em todos,
    marca `complete: true` so no ultimo e, se algum lote falhar, NAO fecha a
    carga (nada e removido).

    Os numeros do resumo vem da RESPOSTA da API (bloco `dataset`), nao de
    contagem local: e isso que prova a idempotencia (na segunda execucao do
    mesmo dia, inserted=0 e tudo cai em `duplicates`) e o ciclo de carga
    (`removed`/`resurrected`).

    Devolve o resumo, ou None se algum lote falhou.
    """
    loc_meta = L_DATASETS[p_nome]

    loc_res = enviar_dataset(
        p_nome, p_registros,
        p_key=loc_meta["key"],
        p_description=loc_meta["description"],
        p_fields=loc_meta["fields"],
        p_chunk=p_chunk,
        p_api_key=p_key,
        p_url=p_url,
        p_timeout=p_timeout,
        p_sync=L_SYNC,
    )

    if loc_res["lotes_erro"]:
        print("  [FALHA] dataset=%s: %d de %d lote(s) falharam. A carga NAO foi"
              " fechada -- nenhum registro removido."
              % (p_nome, loc_res["lotes_erro"], loc_res["lotes"]), file=sys.stderr)
        return None

    return loc_res


# ------------------------------------------  Dry-run: so os envelopes
def imprimir_envelopes(p_nome, p_registros, p_chunk):
    """
    Imprime o ESQUELETO de cada lote da carga (sem os registros, que sao muitos)
    mais o primeiro registro do dataset.

    E a prova visual do contrato: `sync` igual em todos os lotes, `complete`
    presente APENAS no ultimo, e dataset vazio ainda emitindo um envelope de
    fechamento com `records: []`.
    """
    loc_meta = L_DATASETS[p_nome]
    loc_lotes = fatiar(p_registros, p_chunk)

    print("--- %s (%d registros -> %d lote(s)) ---" % (p_nome, len(p_registros), len(loc_lotes)))

    for loc_i, loc_lote in enumerate(loc_lotes):
        loc_envelope = montar_envelope(
            p_nome, loc_lote,
            p_key=loc_meta["key"],
            p_description=loc_meta["description"],
            p_fields=loc_meta["fields"],
            p_sync=L_SYNC,
            p_complete=(loc_i == len(loc_lotes) - 1),
        )

        loc_esqueleto = dict(loc_envelope)
        loc_esqueleto["description"] = "<...>"
        loc_esqueleto["fields"] = "<%d campos>" % len(loc_meta["fields"])
        loc_esqueleto["records"] = "<%d registros>" % len(loc_lote)

        print("  lote %d/%d: %s" % (
            loc_i + 1, len(loc_lotes),
            json.dumps(loc_esqueleto, ensure_ascii=False),
        ))

    if p_registros:
        print("  registro[0]: %s" % json.dumps(p_registros[0], ensure_ascii=False))


# ------------------------------------------  Execucao
def main():
    loc_parser = argparse.ArgumentParser(
        description="Popula um projeto do Contextia com o cenario de demonstracao."
    )
    loc_parser.add_argument("--key", default=L_API_KEY, help="API key ctx_... de destino")
    loc_parser.add_argument("--url", default=L_INTAKE_URL, help="URL do endpoint de intake JSON")
    loc_parser.add_argument("--timeout", type=int, default=L_TIMEOUT)
    loc_parser.add_argument("--chunk", type=int, default=L_CHUNK)
    loc_parser.add_argument("--limite", type=int, default=0,
                            help="envia so os N primeiros registros de cada dataset "
                                 "(demonstra a remocao: o resto sai da view)")
    loc_parser.add_argument("--dry-run", action="store_true",
                            help="imprime os envelopes de cada lote, sem enviar")
    loc_args = loc_parser.parse_args()

    loc_rng = random.Random(L_SEED)
    loc_hoje = datetime.date.today()

    loc_clientes = gerar_clientes(loc_rng, loc_hoje)
    loc_categorias = gerar_categorias()
    loc_receber = gerar_contas_receber(loc_rng, loc_hoje, loc_clientes)
    loc_pagar = gerar_contas_pagar(loc_rng, loc_hoje)

    loc_lote = [
        ("clientes", loc_clientes),
        ("categorias", loc_categorias),
        ("contas_receber", loc_receber),
        ("contas_pagar", loc_pagar),
    ]

    if loc_args.limite > 0:
        loc_lote = [(loc_nome, loc_registros[:loc_args.limite])
                    for loc_nome, loc_registros in loc_lote]

    if loc_args.dry_run:
        print("Carga (sync): %s" % L_SYNC)
        print("`complete` aparece SO no ultimo lote de cada dataset.")
        print("")
        for loc_nome, loc_registros in loc_lote:
            imprimir_envelopes(loc_nome, loc_registros, loc_args.chunk)
        return 0

    print("Cenario de demonstracao -- Nortech Distribuidora (dados ficticios)")
    print("Destino ..: %s" % loc_args.url)
    print("Chave ....: %s..." % loc_args.key[:12])
    print("Data base : %s (vencimentos ancorados nela)" % loc_hoje.isoformat())
    print("Carga ....: %s (mesmo `sync` em todos os lotes)" % L_SYNC)
    if loc_args.limite > 0:
        print("LIMITE ...: %d registros por dataset -- o restante sera REMOVIDO"
              % loc_args.limite)
    print("")

    loc_resumos = []
    loc_falhas = 0

    for loc_nome, loc_registros in loc_lote:
        print("Enviando %s (%d registros) ..." % (loc_nome, len(loc_registros)))
        loc_resumo = enviar(loc_nome, loc_registros, loc_args.key, loc_args.url,
                            loc_args.timeout, loc_args.chunk)

        if loc_resumo is None:
            loc_falhas += 1
            continue

        loc_resumos.append(loc_resumo)

    loc_removidos = sum(loc_r["removed"] for loc_r in loc_resumos)

    print("")
    print("| dataset         | view                        | novos | atualiz. | iguais | removidos | ressusc. | ativos |")
    print("|-----------------|-----------------------------|-------|----------|--------|-----------|----------|--------|")
    for loc_r in loc_resumos:
        print("| %-15s | %-27s | %5s | %8s | %6s | %9s | %8s | %6s |" % (
            loc_r["dataset"], loc_r["view"] or "-",
            loc_r["inserted"], loc_r["updated"], loc_r["duplicates"],
            loc_r["removed"], loc_r["resurrected"],
            loc_r["active_count"] if loc_r["active_count"] is not None else "-",
        ))

    print("")
    print("Numeros acima vem da RESPOSTA da API (bloco `dataset`), nao da contagem local.")
    print("Na PRIMEIRA execucao tudo cai em `novos`. Reexecutando no mesmo dia, tudo cai")
    print("em `iguais` (mesmo ID, mesmo hash de conteudo) e o total nao se mexe; num dia")
    print("diferente as datas mudam e os mesmos titulos aparecem em `atualiz.`. Em nenhum")
    print("dos casos o cenario duplica -- e o upsert por `key: ID` que garante isso.")
    print("")
    print("`removidos` sao registros que existiam no Contextia e NAO vieram nesta carga:")
    print("no fechamento (`complete`) eles saem da view. `ressusc.` sao os que tinham sido")
    print("removidos numa carga anterior e voltaram. Para ver os dois acontecendo: rode com")
    print("--limite=10 (os demais somem) e depois sem limite (os mesmos voltam).")
    if loc_removidos:
        print("")
        print("ATENCAO: %s registro(s) removidos nesta carga." % loc_removidos)
    print("")
    print("Falta so a parte textual do cenario -- os documentos internos em")
    print("storage/demo-docs/, que entram pela indexacao (ver docs/CENARIO-DEMO.md).")

    return 1 if loc_falhas else 0


if __name__ == "__main__":
    sys.exit(main())
