"""
Envia documentos e definicoes para a camada de contexto do Contextia.

    export CONTEXTIA_TENANT_KEY=ctx_...
    python3.8 enviar_contexto.py --dir=/caminho/do/dossie --scope=tenant --dry-run
    python3.8 enviar_contexto.py --dir=/caminho/do/dossie --scope=tenant

POST /api/v1/context. Antes deste endpoint, documento e semantica so entravam
pelo `bin/mcp` -- que roda no servidor do Contextia, e a extracao quase nunca
roda la.

ESCOPO
------
`--scope=tenant` exige a chave de TENANT, e o que entra ali e herdado por TODOS
os projetos: regra do ERP, manual do sistema, glossario de codigos. Contrato e
politica de UM cliente vao com `--scope=project` e a chave daquele projeto.

O servidor recusa uma chave de projeto pedindo escopo de tenant.

PROCEDENCIA
-----------
`source` aceita `client` (o sistema afirma) e `user` (uma pessoa afirma).
`inferred` e `system` sao RECUSADOS -- ficam reservados para o que a Contextia
deriva sozinha. Na duvida use `client`: dizer que uma pessoa afirmou o que foi
lido de um codigo apaga o rastro de que aquilo nunca foi revisado por ninguem.

Correcao feita por uma pessoa no painel NAO e desfeita: a definicao volta como
`skipped_corrected` e a versao dela continua valendo. Sobrescrever exige
`overwrite`, e assume a responsabilidade.
"""

import argparse
import io
import json
import os
import sys

import requests


# Fonte unica do endereco da instalacao: ver CONTEXTIA_URL em
# contextia_client.py. O Contextia e instalado por URL, e cada cliente tem a sua.
from contextia_client import L_API_BASE as L_BASE

# Limites do endpoint.
MAX_DOCS = 200
MAX_SEMANTICAS = 1000
MAX_BYTES_DOC = 1024 * 1024


def titulo_de(p_texto, p_padrao):
    """Primeiro cabecalho markdown do arquivo; o nome do arquivo se nao houver."""
    for linha in p_texto.splitlines():
        if linha.startswith('# '):
            return linha[2:].strip()
    return p_padrao


def ler_documentos(p_dir):
    docs = []
    for nome in sorted(os.listdir(p_dir)):
        if not nome.endswith(('.md', '.txt')):
            continue
        caminho = os.path.join(p_dir, nome)
        texto = io.open(caminho, encoding='utf-8').read()
        tamanho = len(texto.encode('utf-8'))
        if tamanho > MAX_BYTES_DOC:
            raise SystemExit(
                '%s tem %d bytes; o limite por documento e %d. Quebre em partes.'
                % (nome, tamanho, MAX_BYTES_DOC)
            )
        docs.append({
            # O identificador e a chave de idempotencia: reenviar o mesmo com o
            # mesmo conteudo volta como `unchanged`, sem criar versao nova.
            'identifier': os.path.splitext(nome)[0],
            'title': titulo_de(texto, nome),
            'content': texto,
        })
    return docs


def enviar(p_chave, p_corpo):
    r = requests.post(
        L_BASE + '/context', timeout=180,
        headers={'Authorization': 'Bearer ' + p_chave, 'Content-Type': 'application/json'},
        data=json.dumps(p_corpo, ensure_ascii=False).encode('utf-8'),
    )
    try:
        return r.status_code, r.json()
    except ValueError:
        return r.status_code, {'_texto': r.text[:400]}


def main():
    p = argparse.ArgumentParser(description='Envia contexto para o Contextia.')
    p.add_argument('--dir', required=True, help='diretorio com os documentos (.md/.txt)')
    p.add_argument('--scope', default='project', choices=('project', 'tenant'))
    p.add_argument('--semanticas', default=None,
                   help='JSON com uma lista de {subject, definition, source}')
    p.add_argument('--dry-run', action='store_true')
    args = p.parse_args()

    variavel = 'CONTEXTIA_TENANT_KEY' if args.scope == 'tenant' else 'CONTEXTIA_API_KEY'
    chave = os.environ.get(variavel)
    if not chave:
        raise SystemExit(
            'Falta %s. Escopo de tenant exige a chave de TENANT: o que entra ali e\n'
            'herdado por TODOS os projetos, e o servidor recusa a chave de projeto.'
            % variavel
        )

    docs = ler_documentos(args.dir)
    if not docs:
        raise SystemExit('Nenhum .md ou .txt em %s.' % args.dir)
    if len(docs) > MAX_DOCS:
        raise SystemExit('%d documentos; o limite por requisicao e %d.' % (len(docs), MAX_DOCS))

    semanticas = []
    if args.semanticas:
        semanticas = json.load(io.open(args.semanticas, encoding='utf-8'))
        if len(semanticas) > MAX_SEMANTICAS:
            raise SystemExit('%d definicoes; o limite e %d.' % (len(semanticas), MAX_SEMANTICAS))
        for d in semanticas:
            if d.get('source') in ('inferred', 'system'):
                raise SystemExit(
                    'source=%r e reservado para o que a Contextia deriva e sera recusado.'
                    ' Use "client" (o sistema afirma) ou "user" (uma pessoa afirma).'
                    % d['source']
                )

    print('Destino ..: %s' % (L_BASE + '/context'))
    print('Escopo ...: %s (chave %s...)' % (args.scope, chave[:12]))
    print('Documentos: %d | Definicoes: %d\n' % (len(docs), len(semanticas)))
    for d in docs:
        print('  %-34s %7d bytes  %s' % (d['identifier'], len(d['content'].encode('utf-8')),
                                         d['title'][:44]))

    if args.dry_run:
        print('\nNADA foi enviado (--dry-run).')
        return 0

    corpo = {'scope': args.scope, 'documents': docs}
    if semanticas:
        corpo['semantics'] = semanticas

    st, resp = enviar(chave, corpo)
    print('\nHTTP %s' % st)
    if st != 201:
        print(json.dumps(resp, ensure_ascii=False, indent=2)[:900])
        return 1

    for bloco in ('documents', 'semantics'):
        if bloco in resp:
            b = resp[bloco]
            print('  %-11s %s' % (bloco, ', '.join(
                '%s=%s' % (k, v) for k, v in b.items() if not isinstance(v, list))))
            for r in (b.get('rejected') or []):
                print('     RECUSADO: %s' % json.dumps(r, ensure_ascii=False)[:180])
    print('  alvo ...... %s (escopo %s)' % (resp.get('target'), resp.get('scope')))
    return 0


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