/*
 * sa-ctx - API de contexto SA para consumo por sistema/LLM
 *
 * Fontes normalizadas:
 *   <sistema>/arvores = árvores SA do sistema alvo
 *   <sistema>/relspec = relatórios SA derivados de specs
 *   sisc/arvores      = contexto da ferramenta SISC
 */
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <limits.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.h>
#include <stdint.h>
#include <stdarg.h>

#define CTX_MAX_ITENS_PADRAO 30
#define CTX_MAX_BYTES_PADRAO 60000
#define CTX_RA_SNIPPET_MAX 1400
#define CTX_RA_TOTAL_MAX 12000
#define CTX_RA_SCAN_MAX (256 * 1024)
#define CTX_RA_LINHA_MAX 260
#define CTX_RA_MAX_LINHAS_MATCH 10
#define CTX_HIERARQUIA_MAX 12000
#define CTX_CAMPO_TEXTO_MAX 1600
#define CTX_CAMPO_LISTA_MAX 800

typedef struct { char *buf; size_t len, cap; } Str;

static void str_init(Str *s)
{
    s->cap = 4096; s->len = 0; s->buf = malloc(s->cap);
    if (s->buf) s->buf[0] = '\0';
}

static int str_addn(Str *s, const char *txt, size_t n)
{
    if (!s->buf) return -1;
    if (s->len + n + 1 > s->cap) {
        while (s->len + n + 1 > s->cap) s->cap *= 2;
        char *nb = realloc(s->buf, s->cap);
        if (!nb) return -1;
        s->buf = nb;
    }
    memcpy(s->buf + s->len, txt, n);
    s->len += n;
    s->buf[s->len] = '\0';
    return 0;
}

static int str_add(Str *s, const char *txt) { return str_addn(s, txt ? txt : "", strlen(txt ? txt : "")); }

static int str_addf(Str *s, const char *fmt, ...)
{
    va_list ap, ap2;
    va_start(ap, fmt);
    va_copy(ap2, ap);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    if (n < 0) { va_end(ap2); return -1; }
    char *tmp = malloc((size_t)n + 1);
    if (!tmp) { va_end(ap2); return -1; }
    vsnprintf(tmp, (size_t)n + 1, fmt, ap2);
    va_end(ap2);
    int r = str_add(s, tmp);
    free(tmp);
    return r;
}

static char *xstrdup(const char *s) { return strdup(s ? s : ""); }

static void json_escape(Str *s, const char *v)
{
    str_add(s, "\"");
    for (const unsigned char *p = (const unsigned char *)(v ? v : ""); *p; p++) {
        char tmp[8];
        switch (*p) {
            case '"': str_add(s, "\\\""); break;
            case '\\': str_add(s, "\\\\"); break;
            case '\n': str_add(s, "\\n"); break;
            case '\r': str_add(s, "\\r"); break;
            case '\t': str_add(s, "\\t"); break;
            default:
                if (*p < 32) str_addf(s, "\\u%04x", *p);
                else { tmp[0] = (char)*p; tmp[1] = '\0'; str_add(s, tmp); }
        }
    }
    str_add(s, "\"");
}

typedef struct No {
    char *nome, *status, *rel, *valor, *vlt, *clt, *config_vin, *gc, *mm, *itens, *ra;
    struct No **filhos;
    int total, cap;
} No;

typedef struct { char *nome, *criadoEm, *atualizadoEm; No raiz; } Arvore;

static void no_init(No *n)
{
    memset(n, 0, sizeof(*n));
    n->nome = xstrdup("");
}

static No *no_criar(void)
{
    No *n = malloc(sizeof(No));
    if (!n) return NULL;
    no_init(n);
    return n;
}

static int no_add(No *pai, No *filho)
{
    if (pai->total >= pai->cap) {
        int nc = pai->cap ? pai->cap * 2 : 4;
        No **nv = realloc(pai->filhos, (size_t)nc * sizeof(No *));
        if (!nv) return -1;
        pai->filhos = nv; pai->cap = nc;
    }
    pai->filhos[pai->total++] = filho;
    return 0;
}

static void no_liberar(No *n)
{
    if (!n) return;
    for (int i = 0; i < n->total; i++) { no_liberar(n->filhos[i]); free(n->filhos[i]); }
    free(n->filhos);
    free(n->nome); free(n->status); free(n->rel); free(n->valor); free(n->vlt); free(n->clt);
    free(n->config_vin); free(n->gc); free(n->mm); free(n->itens); free(n->ra);
    memset(n, 0, sizeof(*n));
}

static void arvore_liberar(Arvore *a)
{
    free(a->nome);
    free(a->criadoEm);
    free(a->atualizadoEm);
    no_liberar(&a->raiz);
}

typedef struct { const char *p; } Parser;
static void ps_ws(Parser *ps) { while (*ps->p && isspace((unsigned char)*ps->p)) ps->p++; }
static int ps_chr(Parser *ps, char c) { ps_ws(ps); if (*ps->p == c) { ps->p++; return 1; } return 0; }

static char *ps_string(Parser *ps)
{
    ps_ws(ps);
    if (*ps->p != '"') return NULL;
    ps->p++;
    Str s; str_init(&s);
    while (*ps->p && *ps->p != '"') {
        char c = *ps->p++;
        if (c == '\\' && *ps->p) {
            c = *ps->p++;
            if (c == 'n') c = '\n'; else if (c == 'r') c = '\r'; else if (c == 't') c = '\t';
            else if (c == 'u') { /* ignora unicode escapado de forma segura */
                for (int i = 0; i < 4 && isxdigit((unsigned char)*ps->p); i++) ps->p++;
                c = '?';
            }
        }
        str_addn(&s, &c, 1);
    }
    if (*ps->p == '"') ps->p++;
    return s.buf;
}

static void ps_skip(Parser *ps)
{
    ps_ws(ps);
    if (*ps->p == '"') { char *x = ps_string(ps); free(x); return; }
    if (*ps->p == '{' || *ps->p == '[') {
        char abre = *ps->p, fecha = abre == '{' ? '}' : ']';
        int nivel = 0, str = 0, esc = 0;
        do {
            char c = *ps->p++;
            if (str) { if (esc) esc = 0; else if (c == '\\') esc = 1; else if (c == '"') str = 0; }
            else { if (c == '"') str = 1; else if (c == abre) nivel++; else if (c == fecha) nivel--; }
        } while (*ps->p && nivel > 0);
        return;
    }
    while (*ps->p && *ps->p != ',' && *ps->p != '}' && *ps->p != ']') ps->p++;
}

static No *ps_no(Parser *ps)
{
    if (!ps_chr(ps, '{')) return NULL;
    No *n = no_criar();
    if (!n) return NULL;
    while (1) {
        ps_ws(ps);
        if (ps_chr(ps, '}')) break;
        char *key = ps_string(ps);
        if (!key) { no_liberar(n); free(n); return NULL; }
        if (!ps_chr(ps, ':')) { free(key); no_liberar(n); free(n); return NULL; }
        char **dest = NULL;
        if (strcmp(key, "nome") == 0) dest = &n->nome;
        else if (strcmp(key, "status") == 0) dest = &n->status;
        else if (strcmp(key, "rel") == 0) dest = &n->rel;
        else if (strcmp(key, "valor") == 0) dest = &n->valor;
        else if (strcmp(key, "vlt") == 0 || strcmp(key, "valorTransferencia") == 0) dest = &n->vlt;
        else if (strcmp(key, "clt") == 0 || strcmp(key, "contasTransferencia") == 0) dest = &n->clt;
        else if (strcmp(key, "configVin") == 0 || strcmp(key, "config_vin") == 0) dest = &n->config_vin;
        else if (strcmp(key, "gc") == 0) dest = &n->gc;
        else if (strcmp(key, "mm") == 0) dest = &n->mm;
        else if (strcmp(key, "itens") == 0) dest = &n->itens;
        else if (strcmp(key, "ra") == 0) dest = &n->ra;

        if (dest) {
            char *v = ps_string(ps);
            if (v) { free(*dest); *dest = v; }
        } else if (strcmp(key, "filhos") == 0) {
            if (ps_chr(ps, '[')) {
                while (1) {
                    ps_ws(ps);
                    if (ps_chr(ps, ']')) break;
                    No *f = ps_no(ps);
                    if (f) no_add(n, f);
                    ps_ws(ps); ps_chr(ps, ',');
                }
            } else ps_skip(ps);
        } else ps_skip(ps);
        free(key);
        ps_ws(ps); ps_chr(ps, ',');
    }
    return n;
}

static int parse_arvore(const char *json, Arvore *a)
{
    Parser ps = { json };
    memset(a, 0, sizeof(*a));
    no_init(&a->raiz);
    int tipo_ok = 0;
    if (!ps_chr(&ps, '{')) return -1;
    while (1) {
        ps_ws(&ps);
        if (ps_chr(&ps, '}')) break;
        char *key = ps_string(&ps);
        if (!key) return -1;
        if (!ps_chr(&ps, ':')) { free(key); return -1; }
        if (strcmp(key, "tipo") == 0) {
            char *v = ps_string(&ps);
            if (v && strcmp(v, "arvore-sa") == 0) tipo_ok = 1;
            free(v);
        } else if (strcmp(key, "nome") == 0) {
            a->nome = ps_string(&ps);
            free(a->raiz.nome); a->raiz.nome = xstrdup(a->nome ? a->nome : "");
        } else if (strcmp(key, "criadoEm") == 0 || strcmp(key, "criado_em") == 0 || strcmp(key, "createdAt") == 0) {
            a->criadoEm = ps_string(&ps);
        } else if (strcmp(key, "atualizadoEm") == 0 || strcmp(key, "atualizado_em") == 0 || strcmp(key, "updatedAt") == 0 || strcmp(key, "modificadoEm") == 0) {
            a->atualizadoEm = ps_string(&ps);
        } else if (strcmp(key, "filhos") == 0) {
            if (ps_chr(&ps, '[')) {
                while (1) {
                    ps_ws(&ps);
                    if (ps_chr(&ps, ']')) break;
                    No *f = ps_no(&ps);
                    if (f) no_add(&a->raiz, f);
                    ps_ws(&ps); ps_chr(&ps, ',');
                }
            } else ps_skip(&ps);
        } else ps_skip(&ps);
        free(key);
        ps_ws(&ps); ps_chr(&ps, ',');
    }
    if (!tipo_ok) { arvore_liberar(a); return -1; }
    if (!a->nome) a->nome = xstrdup(a->raiz.nome ? a->raiz.nome : "");
    return 0;
}

static char *ler_arquivo(const char *path, size_t max)
{
    FILE *f = fopen(path, "rb");
    if (!f) return NULL;
    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
    long sz = ftell(f);
    if (sz < 0 || (size_t)sz > max) { fclose(f); return NULL; }
    rewind(f);
    char *buf = malloc((size_t)sz + 1);
    if (!buf) { fclose(f); return NULL; }
    size_t n = fread(buf, 1, (size_t)sz, f);
    fclose(f);
    buf[n] = '\0';
    return buf;
}

static char *ler_arquivo_prefixo(const char *path, size_t max, size_t *lidos, int *truncado)
{
    if (lidos) *lidos = 0;
    if (truncado) *truncado = 0;

    FILE *f = fopen(path, "rb");
    if (!f) return NULL;
    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
    long sz = ftell(f);
    if (sz < 0) { fclose(f); return NULL; }
    rewind(f);

    size_t nler = (size_t)sz;
    if (nler > max) {
        nler = max;
        if (truncado) *truncado = 1;
    }

    char *buf = malloc(nler + 1);
    if (!buf) { fclose(f); return NULL; }
    size_t n = fread(buf, 1, nler, f);
    int erro = ferror(f);
    fclose(f);
    if (erro) { free(buf); return NULL; }
    buf[n] = '\0';
    if (lidos) *lidos = n;
    if ((size_t)sz > n && truncado) *truncado = 1;
    return buf;
}

static int escrever_arquivo(const char *path, const char *txt)
{
    FILE *f = fopen(path, "wb");
    if (!f) return -1;
    size_t n = strlen(txt);
    int ok = fwrite(txt, 1, n, f) == n;
    if (n == 0 || txt[n-1] != '\n') fputc('\n', f);
    fclose(f);
    return ok ? 0 : -1;
}

static int mkdir_p(const char *path)
{
    char tmp[PATH_MAX * 2];
    snprintf(tmp, sizeof(tmp), "%s", path);
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') { *p = '\0'; mkdir(tmp, 0775); *p = '/'; }
    }
    return mkdir(tmp, 0775) == 0 || errno == EEXIST ? 0 : -1;
}

static int raiz_sisc(char *out, size_t tam)
{
    char exe[PATH_MAX];
    ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
    if (n <= 0) return -1;
    exe[n] = '\0';
    char *slash = strrchr(exe, '/'); if (!slash) return -1; *slash = '\0'; /* comandos */
    slash = strrchr(exe, '/'); if (!slash) return -1; *slash = '\0';       /* core */
    slash = strrchr(exe, '/'); if (!slash) return -1; *slash = '\0';       /* sisc */
    if (strlen(exe) + 1 > tam) return -1;
    snprintf(out, tam, "%s", exe);
    return 0;
}

static const char *base_nome(const char *p)
{
    const char *b = p ? strrchr(p, '/') : NULL;
    return b ? b + 1 : (p ? p : "");
}

static void formatar_iso_utc(time_t t, char *out, size_t tam)
{
    if (!out || tam == 0) return;
    struct tm tmv;
    if (!gmtime_r(&t, &tmv)) {
        out[0] = '\0';
        return;
    }
    strftime(out, tam, "%Y-%m-%dT%H:%M:%SZ", &tmv);
}

static char *arquivo_mtime_iso(const char *path)
{
    struct stat st;
    if (!path || stat(path, &st) != 0) return xstrdup("");
    char buf[64];
    formatar_iso_utc(st.st_mtime, buf, sizeof(buf));
    return xstrdup(buf);
}

static int checksum_arquivo_fnv1a64(const char *path, char *out, size_t tam)
{
    if (out && tam) out[0] = '\0';
    if (!path || !out || tam == 0) return -1;

    FILE *f = fopen(path, "rb");
    if (!f) return -1;

    uint64_t h = 1469598103934665603ULL;
    unsigned char buf[8192];
    size_t n;
    while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
        for (size_t i = 0; i < n; i++) {
            h ^= (uint64_t)buf[i];
            h *= 1099511628211ULL;
        }
    }
    int erro = ferror(f);
    fclose(f);
    if (erro) return -1;

    snprintf(out, tam, "fnv1a64:%016llx", (unsigned long long)h);
    return 0;
}

static int status_vazio_ou_null(const char *s)
{
    return !s || !*s || strcasecmp(s, "null") == 0 || strcasecmp(s, "nenhum") == 0;
}

static int contem_ci(const char *s, const char *agulha)
{
    if (!s || !agulha || !*agulha) return 0;
    size_t n = strlen(agulha);
    for (const char *p = s; *p; p++)
        if (strncasecmp(p, agulha, n) == 0) return 1;
    return 0;
}

static int status_aprovado_contexto(const char *s)
{
    if (status_vazio_ou_null(s)) return 0;
    if (contem_ci(s, "reprov") || contem_ci(s, "erro") || contem_ci(s, "bloque")) return 0;
    return contem_ci(s, "aprov") || contem_ci(s, "valid") ||
           contem_ci(s, "conclu") || strcasecmp(s, "ok") == 0;
}

static int status_sa_valido(const char *s)
{
    if (status_vazio_ou_null(s)) return 1;
    if (contem_ci(s, "aprov") || contem_ci(s, "reprov") || contem_ci(s, "valid") ||
        contem_ci(s, "conclu") || contem_ci(s, "andamento") || contem_ci(s, "ativo") ||
        contem_ci(s, "pendente") || contem_ci(s, "bloque") || contem_ci(s, "erro") ||
        strcasecmp(s, "ok") == 0)
        return 1;
    return 0;
}

typedef struct { char origem[16], dir[PATH_MAX*2], rotulo[PATH_MAX]; } Fonte;

typedef enum { ORIGEM_AUTO, ORIGEM_SISTEMA, ORIGEM_FERRAMENTA } OrigemFiltro;

static int parse_origem(const char *s, OrigemFiltro *o)
{
    if (!s || !*s || strcmp(s, "auto") == 0) { *o = ORIGEM_AUTO; return 0; }
    if (strcmp(s, "sistema") == 0 || strcmp(s, "produto") == 0 || strcmp(s, "alvo") == 0) { *o = ORIGEM_SISTEMA; return 0; }
    if (strcmp(s, "ferramenta") == 0 || strcmp(s, "sisc") == 0) { *o = ORIGEM_FERRAMENTA; return 0; }
    return -1;
}

static int montar_fontes(OrigemFiltro of, Fonte fontes[4], int *nf)
{
    *nf = 0;
    char cwd[PATH_MAX], root[PATH_MAX];
    if (!getcwd(cwd, sizeof(cwd))) return -1;
    if (of == ORIGEM_AUTO || of == ORIGEM_SISTEMA) {
        Fonte *f = &fontes[(*nf)++];
        snprintf(f->origem, sizeof(f->origem), "sistema");
        snprintf(f->dir, sizeof(f->dir), "%s/arvores", cwd);
        snprintf(f->rotulo, sizeof(f->rotulo), "%s/arvores", base_nome(cwd));

        f = &fontes[(*nf)++];
        snprintf(f->origem, sizeof(f->origem), "sistema");
        snprintf(f->dir, sizeof(f->dir), "%s/relspec", cwd);
        snprintf(f->rotulo, sizeof(f->rotulo), "%s/relspec", base_nome(cwd));
    }
    if ((of == ORIGEM_AUTO || of == ORIGEM_FERRAMENTA) && raiz_sisc(root, sizeof(root)) == 0) {
        char d[PATH_MAX*2]; snprintf(d, sizeof(d), "%s/arvores", root);
        int dup = 0;
        for (int i = 0; i < *nf; i++) if (strcmp(fontes[i].dir, d) == 0) dup = 1;
        if (!dup) {
            Fonte *f = &fontes[(*nf)++];
            snprintf(f->origem, sizeof(f->origem), "ferramenta");
            snprintf(f->dir, sizeof(f->dir), "%s", d);
            snprintf(f->rotulo, sizeof(f->rotulo), "sisc/arvores");
        }
    }
    return 0;
}

typedef struct { char **v; int total, cap; } ListaStr;
static void lista_add(ListaStr *l, const char *s)
{
    if (l->total >= l->cap) { int nc = l->cap ? l->cap * 2 : 16; char **nv = realloc(l->v, (size_t)nc*sizeof(char*)); if (!nv) return; l->v = nv; l->cap = nc; }
    l->v[l->total++] = xstrdup(s);
}
static void lista_free(ListaStr *l) { for (int i=0;i<l->total;i++) free(l->v[i]); free(l->v); memset(l,0,sizeof(*l)); }
static int lista_contem_str(ListaStr *l, const char *s) { if(!l||!s)return 0; for(int i=0;i<l->total;i++) if(l->v[i]&&strcmp(l->v[i],s)==0) return 1; return 0; }
static int cmp_strp(const void *a, const void *b) { return strcasecmp(*(char* const*)a, *(char* const*)b); }

static int termina_json(const char *s) { size_t n = strlen(s); return n > 5 && strcmp(s+n-5, ".json") == 0; }
static int arquivo_contexto(const char *n) { return n && n[0] != '.' && termina_json(n) && strcmp(n, "reversao-gc.json") != 0; }

static void listar_json_dir(const char *dir, ListaStr *out)
{
    DIR *d = opendir(dir);
    if (!d) return;
    struct dirent *e;
    while ((e = readdir(d))) {
        if (!arquivo_contexto(e->d_name)) continue;
        char p[PATH_MAX*2]; snprintf(p, sizeof(p), "%s/%s", dir, e->d_name);
        struct stat st; if (stat(p, &st) == 0 && S_ISREG(st.st_mode)) lista_add(out, p);
    }
    closedir(d);
    if (out->total > 1) qsort(out->v, (size_t)out->total, sizeof(char*), cmp_strp);
}

static char *sem_ext(const char *p)
{
    const char *b = base_nome(p);
    size_t n = strlen(b); if (n > 5 && strcmp(b+n-5, ".json") == 0) n -= 5;
    char *o = malloc(n+1); if (!o) return NULL; memcpy(o,b,n); o[n]='\0'; return o;
}

static int filtro_arvore_casa(const char *path, const char *nome_arv, const char *filtro)
{
    if (!filtro || !*filtro) return 1;
    char *f = sem_ext(filtro), *p = sem_ext(path);
    int ok = (f && p && strcmp(f,p)==0) || (f && nome_arv && strcmp(f,nome_arv)==0);
    free(f); free(p); return ok;
}

typedef struct {
    char *origem, *arquivo, *arvore, *id, *nome, *caminho, *status, *gc, *mm, *itens, *ra, *rel, *configVin, *clt, *valor, *vlt, *filhosResumo;
    char *arvoreCriadoEm, *arvoreAtualizadoEm, *arvoreAtualizadoFonte;
    int filhos, contextoValidado, contextoAprovado, contextoErros, contextoAvisos;
    double score;
} Item;

typedef struct { Item *v; int total, cap; } Itens;

static void item_free(Item *it)
{
    free(it->origem); free(it->arquivo); free(it->arvore); free(it->id); free(it->nome); free(it->caminho); free(it->status);
    free(it->gc); free(it->mm); free(it->itens); free(it->ra); free(it->rel); free(it->configVin); free(it->clt); free(it->valor); free(it->vlt); free(it->filhosResumo);
    free(it->arvoreCriadoEm); free(it->arvoreAtualizadoEm); free(it->arvoreAtualizadoFonte);
}
static void itens_free(Itens *its) { for(int i=0;i<its->total;i++) item_free(&its->v[i]); free(its->v); memset(its,0,sizeof(*its)); }
static int itens_add(Itens *its, Item *it)
{
    if (its->total >= its->cap) { int nc = its->cap ? its->cap*2 : 128; Item *nv = realloc(its->v, (size_t)nc*sizeof(Item)); if(!nv) return -1; its->v=nv; its->cap=nc; }
    its->v[its->total++] = *it; return 0;
}

static int id_valido(const char *s);
static void validar_itens_contexto(Str *s, int *diag, int *erros, int *avisos,
                                   Item *v, int total, const char *origem, const char *arquivo);

static void construir_id(char *buf, size_t tam, int *idx, int prof)
{
    buf[0]='\0';
    for (int i=0;i<prof;i++) { char t[32]; snprintf(t,sizeof(t),"%s%d", i?".":"", idx[i]+1); strncat(buf,t,tam-strlen(buf)-1); }
}

static char *resumo_filhos(No *n)
{
    Str s; str_init(&s);
    int lim = n->total < 8 ? n->total : 8;
    for (int i=0;i<lim;i++) { if(i) str_add(&s," | "); str_addf(&s,"%d: %s", i+1, n->filhos[i]->nome?n->filhos[i]->nome:""); }
    if (n->total > lim) str_addf(&s," | ... +%d", n->total-lim);
    return s.buf;
}

static void coletar_rec(Itens *its, No *n, int *idx, int prof, const char *origem, const char *arquivo, const char *arvore,
                        const char *arvore_criado, const char *arvore_atualizado,
                        const char *arvore_atualizado_fonte, const char *ancestrais)
{
    for (int i=0;i<n->total;i++) {
        No *f = n->filhos[i]; idx[prof]=i; char id[256]; construir_id(id,sizeof(id),idx,prof+1);
        Str cam; str_init(&cam);
        if (ancestrais && *ancestrais) str_addf(&cam, "%s > %s", ancestrais, f->nome?f->nome:""); else str_add(&cam, f->nome?f->nome:"");
        Item it; memset(&it,0,sizeof(it));
        it.origem=xstrdup(origem); it.arquivo=xstrdup(arquivo); it.arvore=xstrdup(arvore); it.id=xstrdup(id); it.nome=xstrdup(f->nome);
        it.caminho=cam.buf; it.status=xstrdup(f->status); it.gc=xstrdup(f->gc); it.mm=xstrdup(f->mm); it.itens=xstrdup(f->itens); it.ra=xstrdup(f->ra);
        it.rel=xstrdup(f->rel); it.configVin=xstrdup(f->config_vin); it.clt=xstrdup(f->clt); it.valor=xstrdup(f->valor); it.vlt=xstrdup(f->vlt); it.filhosResumo=resumo_filhos(f); it.filhos=f->total;
        it.arvoreCriadoEm=xstrdup(arvore_criado); it.arvoreAtualizadoEm=xstrdup(arvore_atualizado); it.arvoreAtualizadoFonte=xstrdup(arvore_atualizado_fonte);
        it.contextoAprovado=status_aprovado_contexto(it.status);
        if (itens_add(its,&it)!=0) item_free(&it);
        coletar_rec(its, f, idx, prof+1, origem, arquivo, arvore, arvore_criado, arvore_atualizado, arvore_atualizado_fonte, cam.buf);
    }
}

static int carregar_itens(OrigemFiltro of, const char *filtro, Itens *its)
{
    memset(its,0,sizeof(*its));
    Fonte fontes[4]; int nf=0; if (montar_fontes(of,fontes,&nf)!=0) return -1;
    for (int fi=0; fi<nf; fi++) {
        ListaStr files={0}; listar_json_dir(fontes[fi].dir, &files);
        for (int ai=0; ai<files.total; ai++) {
            char *txt = ler_arquivo(files.v[ai], 50*1024*1024); if(!txt) continue;
            Arvore a; int ok = parse_arvore(txt,&a)==0; free(txt); if(!ok) continue;
            if (!filtro_arvore_casa(files.v[ai], a.nome, filtro)) { arvore_liberar(&a); continue; }
            char rot[PATH_MAX*2]; snprintf(rot,sizeof(rot),"%s/%s", fontes[fi].rotulo, base_nome(files.v[ai]));
            char *mtime = arquivo_mtime_iso(files.v[ai]);
            const char *atualizado = (a.atualizadoEm && *a.atualizadoEm) ? a.atualizadoEm : mtime;
            const char *fonte_atualizado = (a.atualizadoEm && *a.atualizadoEm) ? "arvore.atualizadoEm" : "arquivo.mtime";
            int inicio = its->total;
            int idx[256]; coletar_rec(its, &a.raiz, idx, 0, fontes[fi].origem, rot, a.nome?a.nome:"",
                                      a.criadoEm?a.criadoEm:"", atualizado?atualizado:"", fonte_atualizado, "");
            int erros_ctx = 0, avisos_ctx = 0;
            validar_itens_contexto(NULL, NULL, &erros_ctx, &avisos_ctx,
                                   inicio < its->total ? its->v + inicio : NULL,
                                   its->total - inicio, fontes[fi].origem, rot);
            for (int mi = inicio; mi < its->total; mi++) {
                its->v[mi].contextoValidado = erros_ctx == 0;
                its->v[mi].contextoErros = erros_ctx;
                its->v[mi].contextoAvisos = avisos_ctx;
            }
            free(mtime);
            arvore_liberar(&a);
        }
        lista_free(&files);
    }
    return 0;
}

static const char *stopwords[] = { "a","o","as","os","um","uma","de","do","da","dos","das","e","ou","que","qual","quais","em","no","na","nos","nas","para","por","com","sem","sobre","me","te","se","eu","voce","voces","nos","ele","ela","eles","elas","este","esta","esse","essa","isso","isto","aquilo","como","quando","onde","crie","criar","gere","gerar","faca","fazer","mostre","liste","explique","resuma","resumo","sistema","projeto","contexto","geral", NULL };
static int eh_stop(const char *t) { for(int i=0;stopwords[i];i++) if(strcmp(t,stopwords[i])==0) return 1; return 0; }

typedef struct { char **v; int total, cap; } Tokens;
static void tokens_free(Tokens *t) { for(int i=0;i<t->total;i++) free(t->v[i]); free(t->v); memset(t,0,sizeof(*t)); }
static int token_existe(Tokens *t, const char *s) { for(int i=0;i<t->total;i++) if(strcmp(t->v[i],s)==0) return 1; return 0; }
static void token_add(Tokens *t, const char *ini, size_t n)
{
    if (n < 2) return;

    char tmp[128];
    if (n >= sizeof(tmp)) n = sizeof(tmp) - 1;
    memcpy(tmp, ini, n);
    tmp[n] = '\0';
    if (eh_stop(tmp) || token_existe(t, tmp)) return;

    if (t->total >= t->cap) {
        int nc = t->cap ? t->cap * 2 : 32;
        char **nv = realloc(t->v, (size_t)nc * sizeof(char*));
        if (!nv) return;
        t->v = nv;
        t->cap = nc;
    }
    t->v[t->total++] = xstrdup(tmp);
}
static int map_utf8(const unsigned char **pp, char *out)
{
    const unsigned char *p=*pp; *out=0;
    if(*p==0xC3 && p[1]) { unsigned char d=p[1];
        if(d==0xA1||d==0xA0||d==0xA2||d==0xA3||d==0xA4||d==0x81||d==0x80||d==0x82||d==0x83||d==0x84)*out='a';
        else if(d==0xA9||d==0xA8||d==0xAA||d==0xAB||d==0x89||d==0x88||d==0x8A||d==0x8B)*out='e';
        else if(d==0xAD||d==0xAC||d==0xAE||d==0xAF||d==0x8D||d==0x8C||d==0x8E||d==0x8F)*out='i';
        else if(d==0xB3||d==0xB2||d==0xB4||d==0xB5||d==0xB6||d==0x93||d==0x92||d==0x94||d==0x95||d==0x96)*out='o';
        else if(d==0xBA||d==0xB9||d==0xBB||d==0xBC||d==0x9A||d==0x99||d==0x9B||d==0x9C)*out='u';
        else if(d==0xA7||d==0x87)*out='c'; else if(d==0xB1||d==0x91)*out='n';
        *pp=p+1; return *out!=0;
    }
    if(isalnum(*p) || *p=='_') { *out=(char)tolower(*p); return 1; }
    return 0;
}
static void tokenizar(const char *txt, Tokens *t)
{
    memset(t,0,sizeof(*t)); char buf[128]; size_t bl=0;
    for(const unsigned char *p=(const unsigned char*)(txt?txt:""); *p; p++) { char c=0; const unsigned char *old=p; int ok=map_utf8(&p,&c); (void)old;
        if(ok && c){ if(bl+1<sizeof(buf)) buf[bl++]=c; }
        else { if(bl){ token_add(t,buf,bl); bl=0; } }
    }
    if(bl) token_add(t,buf,bl);
}
static int token_compativel(const char *a, const char *b)
{
    if (!a || !b) return 0;
    if (strcmp(a, b) == 0) return 1;

    size_t la = strlen(a), lb = strlen(b);

    /* plural simples: fluxo <-> fluxos, divergencia <-> divergencias */
    if (la >= 4 && lb == la + 1 && b[lb - 1] == 's' && strncmp(a, b, la) == 0) return 1;
    if (lb >= 4 && la == lb + 1 && a[la - 1] == 's' && strncmp(a, b, lb) == 0) return 1;

    /* radicais próximos: validar <-> validacao, construir <-> construtor */
    if (la >= 6 && lb >= 6) {
        size_t comum = 0;
        while (comum < la && comum < lb && a[comum] == b[comum]) comum++;
        size_t menor = la < lb ? la : lb;
        if (comum >= 6 && (comum * 100 / menor) >= 80) return 1;
    }

    return 0;
}

static int token_em(Tokens *t, const char *s)
{
    for (int i = 0; i < t->total; i++)
        if (token_compativel(t->v[i], s)) return 1;
    return 0;
}

static double score_texto(Tokens *q, const char *txt)
{
    if (!q || q->total == 0) return 0.0;

    Tokens tt;
    tokenizar(txt, &tt);
    if (tt.total == 0) {
        tokens_free(&tt);
        return 0.0;
    }

    int inter = 0;
    for (int i = 0; i < q->total; i++)
        if (token_em(&tt, q->v[i])) inter++;

    if (inter == 0) {
        tokens_free(&tt);
        return 0.0;
    }

    /*
     * O ranking antigo usava Jaccard puro. Isso derrubava campos longos
     * como mm/ra mesmo quando eles cobriam toda a consulta. Agora a maior
     * parte do score vem da cobertura da consulta e uma fração mede o foco
     * do campo para preservar precisão em títulos/caminhos curtos.
     */
    double cobertura = (double)inter / (double)q->total;
    double foco = (double)inter / (double)tt.total;
    double s = 0.80 * cobertura + 0.20 * foco;

    if (inter == q->total) s += 0.10;
    if (tt.total <= q->total + 2 && cobertura >= 0.50) s += 0.05;
    if (s > 1.0) s = 1.0;

    tokens_free(&tt);
    return s;
}

static int ra_seguro(const char *ra);
static int resolver_ra(const char *ra, char *out, size_t tam);

static double score_ra_conteudo(Tokens *q, const char *lista)
{
    if (!q || q->total == 0 || !lista || !*lista) return 0.0;

    char *dup = xstrdup(lista), *save = NULL, *tok = strtok_r(dup, ",", &save);
    double melhor = 0.0;

    while (tok) {
        while (isspace((unsigned char)*tok)) tok++;
        char *e = tok + strlen(tok);
        while (e > tok && isspace((unsigned char)e[-1])) e--;
        *e = '\0';

        char path[PATH_MAX * 2];
        if (*tok && resolver_ra(tok, path, sizeof(path)) == 0) {
            struct stat st;
            if (stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size <= 256 * 1024) {
                char *txt = ler_arquivo(path, 256 * 1024);
                if (txt) {
                    if (strlen(txt) > 8192) txt[8192] = '\0';
                    double sc = score_texto(q, txt);
                    if (sc > melhor) melhor = sc;
                    free(txt);
                }
            }
        }

        tok = strtok_r(NULL, ",", &save);
    }

    free(dup);
    return melhor;
}

static double score_campo(double *soma, double *peso_total, double peso, Tokens *q, const char *txt)
{
    *peso_total += peso;
    if (!txt || !*txt) return 0.0;

    double sc = score_texto(q, txt);
    *soma += peso * sc;
    return sc;
}

static double score_status_bonus(const char *status)
{
    if (!status || !*status) return 0.0;

    if (strcasecmp(status, "aprovado") == 0 || strcasecmp(status, "validado") == 0 ||
        strcasecmp(status, "concluido") == 0 || strcasecmp(status, "ok") == 0)
        return 0.030;
    if (strcasecmp(status, "andamento") == 0 || strcasecmp(status, "ativo") == 0)
        return 0.020;
    if (strcasecmp(status, "pendente") == 0)
        return 0.005;
    if (strcasecmp(status, "reprovado") == 0 || strcasecmp(status, "erro") == 0 ||
        strcasecmp(status, "bloqueado") == 0)
        return -0.050;

    return 0.0;
}

static double score_item(Item *it, Tokens *q)
{
    if (!q || q->total == 0)
        return strcmp(it->origem, "sistema") == 0 ? 0.015 : 0.0;

    double soma = 0.0, peso = 0.0;

    double sc_nome = score_campo(&soma, &peso, 6.0, q, it->nome);
    score_campo(&soma, &peso, 4.0, q, it->caminho);
    double sc_mm = score_campo(&soma, &peso, 5.0, q, it->mm);
    score_campo(&soma, &peso, 2.5, q, it->ra);
    score_campo(&soma, &peso, 2.0, q, it->filhosResumo);
    score_campo(&soma, &peso, 1.5, q, it->gc);
    score_campo(&soma, &peso, 1.0, q, it->status);
    score_campo(&soma, &peso, 1.2, q, it->itens);
    score_campo(&soma, &peso, 0.8, q, it->rel);
    score_campo(&soma, &peso, 0.8, q, it->configVin);
    score_campo(&soma, &peso, 0.8, q, it->clt);
    score_campo(&soma, &peso, 1.0, q, it->valor);
    score_campo(&soma, &peso, 1.0, q, it->vlt);
    score_campo(&soma, &peso, 1.0, q, it->arquivo);
    score_campo(&soma, &peso, 1.0, q, it->arvore);

    double sc_ra_conteudo = score_ra_conteudo(q, it->ra);
    if (sc_ra_conteudo > 0.0) {
        soma += 2.0 * sc_ra_conteudo;
        peso += 2.0;
    }

    double sc = peso > 0.0 ? soma / peso : 0.0;

    if (strcmp(it->origem, "sistema") == 0) sc += 0.015;
    sc += score_status_bonus(it->status);

    if (it->filhos > 0) sc += it->filhos >= 4 ? 0.010 : 0.005;
    if (it->gc && *it->gc) sc += 0.004;
    if (sc_nome >= 0.95) sc += 0.035;
    else if (sc_nome >= 0.65) sc += 0.015;
    if (sc_mm >= 0.95) sc += 0.015;

    if (sc < 0.0) sc = 0.0;
    return sc;
}
static int cmp_item_score(const void *a,const void *b)
{
    const Item *ia = a, *ib = b;
    if (ib->score > ia->score) return 1;
    if (ib->score < ia->score) return -1;
    int as = strcmp(ia->origem, "sistema") == 0;
    int bs = strcmp(ib->origem, "sistema") == 0;
    if (as != bs) return bs - as;
    int c = strcmp(ia->arquivo, ib->arquivo);
    if (c != 0) return c;
    return strcmp(ia->id, ib->id);
}

static void json_prop_sep(Str *s, int *first){ if(!*first) str_add(s,","); *first=0; }
static void json_prop_str(Str *s,int *first,const char*k,const char*v){ if(!v||!*v)return; json_prop_sep(s,first); str_addf(s,"\n      \"%s\": ",k); json_escape(s,v); }
static void json_prop_int(Str *s,int *first,const char*k,int v){ json_prop_sep(s,first); str_addf(s,"\n      \"%s\": %d",k,v); }
static void json_prop_long(Str *s,int *first,const char*k,long long v){ json_prop_sep(s,first); str_addf(s,"\n      \"%s\": %lld",k,v); }
static void json_prop_bool(Str *s,int *first,const char*k,int v){ json_prop_sep(s,first); str_addf(s,"\n      \"%s\": %s",k,v?"true":"false"); }
static void json_prop_double(Str *s,int *first,const char*k,double v){ json_prop_sep(s,first); str_addf(s,"\n      \"%s\": %.6f",k,v); }

static void json_prop_ra_metadados(Str *s, int *first, const char *lista)
{
    if (!lista || !*lista) return;

    char *dup = xstrdup(lista), *save = NULL, *tok = strtok_r(dup, ",", &save);
    int out = 0;
    json_prop_sep(s, first);
    str_add(s, "\n      \"raMetadados\": [");

    while (tok) {
        while (isspace((unsigned char)*tok)) tok++;
        char *e = tok + strlen(tok);
        while (e > tok && isspace((unsigned char)e[-1])) e--;
        *e = '\0';

        if (*tok) {
            if (out) str_add(s, ",");
            str_add(s, "\n        {");
            int f = 1;
            json_prop_str(s, &f, "ra", tok);
            int seguro = ra_seguro(tok);
            json_prop_bool(s, &f, "seguro", seguro);

            char path[PATH_MAX * 2];
            if (seguro && resolver_ra(tok, path, sizeof(path)) == 0) {
                struct stat st;
                int existe = stat(path, &st) == 0;
                json_prop_bool(s, &f, "existe", existe);
                if (existe) {
                    if (S_ISREG(st.st_mode)) json_prop_str(s, &f, "tipo", "arquivo");
                    else if (S_ISDIR(st.st_mode)) json_prop_str(s, &f, "tipo", "diretorio");
                    else json_prop_str(s, &f, "tipo", "outro");
                    json_prop_str(s, &f, "path", path);
                    json_prop_long(s, &f, "bytes", (long long)st.st_size);
                    char iso[64]; formatar_iso_utc(st.st_mtime, iso, sizeof(iso));
                    json_prop_str(s, &f, "modificadoEm", iso);
                    if (S_ISREG(st.st_mode)) {
                        char chk[64];
                        if (checksum_arquivo_fnv1a64(path, chk, sizeof(chk)) == 0)
                            json_prop_str(s, &f, "checksum", chk);
                        else
                            json_prop_str(s, &f, "checksumErro", "arquivo sem leitura");
                    } else {
                        json_prop_str(s, &f, "checksum", "n/a");
                    }
                }
            } else {
                json_prop_bool(s, &f, "existe", 0);
                json_prop_str(s, &f, "erro", "referência ra insegura ou inválida");
            }
            str_add(s, "\n        }");
            out++;
        }
        tok = strtok_r(NULL, ",", &save);
    }

    if (out) str_add(s, "\n      ");
    str_add(s, "]");
    free(dup);
}

static void item_json(Str *s, Item *it, int score)
{
    int f=1;
    str_add(s,"{");
    json_prop_str(s,&f,"origem",it->origem);
    json_prop_str(s,&f,"arquivo",it->arquivo);
    json_prop_str(s,&f,"arvore",it->arvore);
    json_prop_str(s,&f,"arvoreCriadoEm",it->arvoreCriadoEm);
    json_prop_str(s,&f,"arvoreAtualizadoEm",it->arvoreAtualizadoEm);
    json_prop_str(s,&f,"arvoreAtualizadoFonte",it->arvoreAtualizadoFonte);
    json_prop_bool(s,&f,"contextoValidado",it->contextoValidado);
    json_prop_bool(s,&f,"contextoAprovado",it->contextoAprovado);
    json_prop_int(s,&f,"contextoErros",it->contextoErros);
    json_prop_int(s,&f,"contextoAvisos",it->contextoAvisos);
    json_prop_str(s,&f,"id",it->id);
    json_prop_str(s,&f,"nome",it->nome);
    json_prop_str(s,&f,"caminho",it->caminho);
    json_prop_int(s,&f,"filhos",it->filhos);
    if(score) json_prop_double(s,&f,"score",it->score);
    json_prop_str(s,&f,"status",it->status);
    json_prop_str(s,&f,"gc",it->gc);
    json_prop_str(s,&f,"mm",it->mm);
    json_prop_str(s,&f,"itens",it->itens);
    json_prop_str(s,&f,"ra",it->ra);
    json_prop_ra_metadados(s,&f,it->ra);
    json_prop_str(s,&f,"rel",it->rel);
    json_prop_str(s,&f,"configVin",it->configVin);
    json_prop_str(s,&f,"clt",it->clt);
    json_prop_str(s,&f,"valor",it->valor);
    json_prop_str(s,&f,"vlt",it->vlt);
    json_prop_str(s,&f,"filhosResumo",it->filhosResumo);
    if(!f) str_add(s,"\n    ");
    str_add(s,"}");
}

static int cmd_fontes(OrigemFiltro of)
{
    Fonte fontes[4];
    int nf = 0;
    montar_fontes(of, fontes, &nf);

    Str s;
    str_init(&s);
    str_add(&s, "{\n  \"tipo\": \"sa-ctx-fontes\",\n  \"fontes\": [");

    for (int i = 0; i < nf; i++) {
        ListaStr files = {0};
        listar_json_dir(fontes[i].dir, &files);

        int validas = 0;
        for (int j = 0; j < files.total; j++) {
            char *txt = ler_arquivo(files.v[j], 50 * 1024 * 1024);
            if (txt) {
                Arvore a;
                if (parse_arvore(txt, &a) == 0) {
                    validas++;
                    arvore_liberar(&a);
                }
                free(txt);
            }
        }

        if (i) str_add(&s, ",");
        str_add(&s, "\n    {");
        int f = 1;
        json_prop_str(&s, &f, "origem", fontes[i].origem);
        json_prop_str(&s, &f, "diretorio", fontes[i].rotulo);
        json_prop_str(&s, &f, "path", fontes[i].dir);
        struct stat st;
        json_prop_sep(&s, &f);
        str_addf(&s, "\n      \"existe\": %s", (stat(fontes[i].dir, &st) == 0 && S_ISDIR(st.st_mode)) ? "true" : "false");
        json_prop_int(&s, &f, "arquivosJson", files.total);
        json_prop_int(&s, &f, "arvoresValidas", validas);
        json_prop_sep(&s, &f);
        str_add(&s, "\n      \"arquivos\": [");
        for (int j = 0; j < files.total; j++) {
            if (j) str_add(&s, ", ");
            char rot[PATH_MAX * 2];
            snprintf(rot, sizeof(rot), "%s/%s", fontes[i].rotulo, base_nome(files.v[j]));
            json_escape(&s, rot);
        }
        str_add(&s, "]\n    }");
        lista_free(&files);
    }

    str_add(&s, "\n  ]\n}\n");
    fputs(s.buf, stdout);
    free(s.buf);
    return 0;
}

static void rankear(Itens *its, const char *consulta, Tokens *qout)
{
    tokenizar(consulta?consulta:"", qout);
    for(int i=0;i<its->total;i++) its->v[i].score=score_item(&its->v[i],qout);
    qsort(its->v,(size_t)its->total,sizeof(Item),cmp_item_score);
}

static int cmd_indice(OrigemFiltro of,const char*filtro)
{ Itens its; carregar_itens(of,filtro,&its); Str s; str_init(&s); str_addf(&s,"{\n  \"tipo\": \"sa-ctx-indice\",\n  \"total\": %d,\n  \"itens\": [",its.total); for(int i=0;i<its.total;i++){if(i)str_add(&s,","); str_add(&s,"\n    "); item_json(&s,&its.v[i],0);} str_add(&s,"\n  ]\n}\n"); fputs(s.buf,stdout); free(s.buf); itens_free(&its); return 0; }

static int cmd_buscar(OrigemFiltro of,const char*filtro,const char*q,int maxit)
{ Itens its; carregar_itens(of,filtro,&its); Tokens tq; rankear(&its,q,&tq); Str s; str_init(&s); str_add(&s,"{\n  \"tipo\": \"sa-ctx-buscar\",\n  \"consulta\": "); json_escape(&s,q?q:""); str_addf(&s,",\n  \"totalDisponivel\": %d,\n  \"resultados\": [",its.total); int out=0; if(maxit<=0)maxit=CTX_MAX_ITENS_PADRAO; for(int i=0;i<its.total&&out<maxit;i++){ if(tq.total>0&&its.v[i].score<=0) continue; if(out)str_add(&s,","); str_add(&s,"\n    "); item_json(&s,&its.v[i],1); out++; } str_addf(&s,"\n  ],\n  \"totalRetornado\": %d\n}\n",out); fputs(s.buf,stdout); free(s.buf); tokens_free(&tq); itens_free(&its); return 0; }

static int selecionar_itens(Itens *its, Tokens *tq, int maxit, int *selecionado)
{
    int out = 0;
    if (!its || !selecionado) return 0;
    if (maxit <= 0) maxit = CTX_MAX_ITENS_PADRAO;
    for (int i = 0; i < its->total && out < maxit; i++) {
        if (tq && tq->total > 0 && its->v[i].score <= 0.0) continue;
        selecionado[i] = 1;
        out++;
    }
    return out;
}

static int id_profundidade(const char *id)
{
    if (!id || !*id) return 0;
    int d = 1;
    for (const char *p = id; *p; p++) if (*p == '.') d++;
    return d;
}

static int id_ancestral_de(const char *anc, const char *id)
{
    if (!anc || !id) return 0;
    size_t la = strlen(anc), li = strlen(id);
    return la < li && strncmp(anc, id, la) == 0 && id[la] == '.';
}

static int item_mesma_arvore(Item *a, Item *b)
{
    return a && b && strcmp(a->origem, b->origem) == 0 && strcmp(a->arquivo, b->arquivo) == 0;
}

static int cmp_id_natural(const char *a, const char *b)
{
    const char *pa = a ? a : "", *pb = b ? b : "";
    while (*pa || *pb) {
        char *ea = NULL, *eb = NULL;
        long va = strtol(pa, &ea, 10);
        long vb = strtol(pb, &eb, 10);
        if (ea == pa || eb == pb) return strcmp(pa, pb);
        if (va < vb) return -1;
        if (va > vb) return 1;
        if (*ea == '.' && *eb == '.') { pa = ea + 1; pb = eb + 1; continue; }
        if (*ea == '.') return 1;
        if (*eb == '.') return -1;
        if (*ea == '\0' && *eb == '\0') return 0;
        return strcmp(ea, eb);
    }
    return 0;
}

typedef struct { Item *it; int selecionado; } HierRef;

static int cmp_hier_ref(const void *a, const void *b)
{
    const HierRef *ha = a, *hb = b;
    int as = strcmp(ha->it->origem, "sistema") == 0;
    int bs = strcmp(hb->it->origem, "sistema") == 0;
    if (as != bs) return as ? -1 : 1;
    int c = strcasecmp(ha->it->arquivo, hb->it->arquivo);
    if (c != 0) return c;
    return cmp_id_natural(ha->it->id, hb->it->id);
}

static void str_add_inline_limitado(Str *s, const char *txt, size_t limite)
{
    if (!txt || !*txt) return;
    size_t n = strlen(txt);
    if (n <= limite) str_add(s, txt);
    else {
        str_addn(s, txt, limite);
        str_addf(s, " ... [truncado: %zu bytes]", n);
    }
}

static void add_campo_compacto(Str *s, const char *rotulo, const char *valor, size_t limite)
{
    if (!valor || !*valor) return;
    str_addf(s, "%s: ", rotulo);
    str_add_inline_limitado(s, valor, limite);
    str_add(s, "\n");
}

static void add_hierarquia_compacta(Str *s, Itens *its, int *selecionado, int total_sel, size_t maxb)
{
    if (!s || !its || !selecionado || total_sel <= 0 || s->len >= maxb) return;

    HierRef *refs = calloc((size_t)its->total, sizeof(HierRef));
    if (!refs) return;
    int nr = 0;

    for (int i = 0; i < its->total; i++) {
        int incluir = selecionado[i];
        if (!incluir) {
            for (int j = 0; j < its->total; j++) {
                if (!selecionado[j]) continue;
                if (item_mesma_arvore(&its->v[i], &its->v[j]) && id_ancestral_de(its->v[i].id, its->v[j].id)) {
                    incluir = 1;
                    break;
                }
            }
        }
        if (incluir) {
            refs[nr].it = &its->v[i];
            refs[nr].selecionado = selecionado[i];
            nr++;
        }
    }

    if (nr == 0) { free(refs); return; }
    qsort(refs, (size_t)nr, sizeof(HierRef), cmp_hier_ref);

    size_t budget = maxb / 4;
    if (budget < 4096 && maxb >= 8192) budget = 4096;
    if (budget > CTX_HIERARQUIA_MAX) budget = CTX_HIERARQUIA_MAX;
    size_t limite = s->len + budget;
    if (maxb > 4096 && limite > maxb - 1024) limite = maxb - 1024;
    if (limite <= s->len + 512) limite = maxb;

    str_add(s, "=== Mapa hierárquico compacto dos itens selecionados ===\n");
    str_add(s, "Legenda: * item selecionado; · ancestral preservado para manter a hierarquia.\n");

    const char *arq_atual = NULL;
    int truncado = 0;
    for (int i = 0; i < nr; i++) {
        Item *it = refs[i].it;
        size_t nome_len = strlen(it->nome ? it->nome : "");
        if (nome_len > 180) nome_len = 180;
        size_t filhos_len = refs[i].selecionado ? strlen(it->filhosResumo ? it->filhosResumo : "") : 0;
        if (filhos_len > 260) filhos_len = 260;
        size_t estimado = nome_len + filhos_len + 220;
        if (s->len + estimado > limite) { truncado = 1; break; }

        if (!arq_atual || strcmp(arq_atual, it->arquivo) != 0) {
            arq_atual = it->arquivo;
            str_addf(s, "\n[%s] %s", it->origem, it->arquivo);
            if (it->arvore && *it->arvore) str_addf(s, " (árvore: %s)", it->arvore);
            if (it->arvoreAtualizadoEm && *it->arvoreAtualizadoEm) str_addf(s, " | atualizado=%s", it->arvoreAtualizadoEm);
            str_addf(s, " | validado=%s", it->contextoValidado ? "true" : "false");
            str_add(s, "\n");
        }

        int prof = id_profundidade(it->id);
        for (int d = 1; d < prof && d < 8; d++) str_add(s, "  ");
        if (prof >= 8) str_add(s, "  ");
        str_addf(s, "%s[%s] ", refs[i].selecionado ? "* " : "· ", it->id);
        str_add_inline_limitado(s, it->nome, 180);
        if (refs[i].selecionado) str_addf(s, " | score=%.3f", it->score);
        else str_add(s, " | ancestral");
        if (it->status && *it->status) str_addf(s, " | status=%s", it->status);
        if (it->filhos > 0) str_addf(s, " | filhos=%d", it->filhos);
        if (refs[i].selecionado && it->filhosResumo && *it->filhosResumo) {
            str_add(s, " | ");
            str_add_inline_limitado(s, it->filhosResumo, 260);
        }
        str_add(s, "\n");
    }
    if (truncado) str_add(s, "[...mapa hierárquico compactado pelo limite de bytes...]\n");
    str_add(s, "\n");
    free(refs);
}

static int linha_relevante_para_tokens(Tokens *q, const char *ini, size_t len)
{
    if (!q || q->total == 0 || !ini || len == 0) return 0;
    while (len > 0 && isspace((unsigned char)*ini)) { ini++; len--; }
    while (len > 0 && isspace((unsigned char)ini[len - 1])) len--;
    if (len == 0) return 0;
    size_t usar = len > 1024 ? 1024 : len;
    char *tmp = malloc(usar + 1);
    if (!tmp) return 0;
    memcpy(tmp, ini, usar);
    tmp[usar] = '\0';
    double sc = score_texto(q, tmp);
    free(tmp);
    return sc > 0.0;
}

static void add_linha_ra(Str *s, int num, const char *ini, size_t len, size_t limite)
{
    while (len > 0 && (ini[len - 1] == '\n' || ini[len - 1] == '\r')) len--;
    if (s->len + 40 >= limite) return;
    size_t usar = len > CTX_RA_LINHA_MAX ? CTX_RA_LINHA_MAX : len;
    if (s->len + usar + 48 > limite) {
        if (limite <= s->len + 48) return;
        usar = limite - s->len - 48;
    }
    str_addf(s, "%5d | ", num);
    str_addn(s, ini, usar);
    if (len > usar) str_add(s, " ...");
    str_add(s, "\n");
}

static void construir_snippet_ra(Str *sn, const char *txt, size_t lidos, int trunc_arq, Tokens *q, size_t limite)
{
    if (!sn || !txt || limite < 128) return;
    const char *fim = txt + lidos;

    if (q && q->total > 0) {
        int matches = 0, linhas = 1, interrompeu = 0;
        for (const char *p = txt; p < fim && *p; linhas++) {
            const char *e = memchr(p, '\n', (size_t)(fim - p));
            if (!e) e = fim;
            size_t len = (size_t)(e - p);
            if (linha_relevante_para_tokens(q, p, len)) {
                if (sn->len + CTX_RA_LINHA_MAX + 80 > limite) { interrompeu = 1; break; }
                add_linha_ra(sn, linhas, p, len, limite);
                matches++;
                if (matches >= CTX_RA_MAX_LINHAS_MATCH) { interrompeu = 1; break; }
            }
            p = (e < fim) ? e + 1 : fim;
        }
        if (matches > 0) {
            if ((interrompeu || trunc_arq) && sn->len + 80 < limite)
                str_add(sn, "[...ra compactado: exibidas apenas linhas relevantes...]\n");
            return;
        }
    }

    int linhas = 1, adicionadas = 0, interrompeu = 0;
    for (const char *p = txt; p < fim && *p; linhas++) {
        const char *e = memchr(p, '\n', (size_t)(fim - p));
        if (!e) e = fim;
        size_t len = (size_t)(e - p);
        if (sn->len + CTX_RA_LINHA_MAX + 80 > limite || adicionadas >= 18) { interrompeu = 1; break; }
        add_linha_ra(sn, linhas, p, len, limite);
        adicionadas++;
        p = (e < fim) ? e + 1 : fim;
    }
    if ((interrompeu || trunc_arq) && sn->len + 80 < limite)
        str_add(sn, "[...ra compactado: início do arquivo apenas...]\n");
}

static int ra_seguro(const char *ra)
{ if(!ra||strncmp(ra,"sisc/",5)!=0||ra[0]=='/'||strstr(ra,"..")) return 0; const char*bloq[]={"/.git/","/secretos/","/contas-llm/","/biblioteca-contas-llm/","/espaco/","/sessoes/","/rascunho/","/tarefas/","/tmp/","/cache/",NULL}; char tmp[PATH_MAX]; snprintf(tmp,sizeof(tmp),"/%s/",ra); for(int i=0;bloq[i];i++) if(strstr(tmp,bloq[i])) return 0; return 1; }
static int resolver_ra(const char *ra,char*out,size_t tam){char root[PATH_MAX]; if(!ra_seguro(ra)||raiz_sisc(root,sizeof(root))!=0)return -1; snprintf(out,tam,"%s/%s",root,ra+5); return 0;}

static void add_ra_metadados_texto(Str *s, const char *lista, size_t limite)
{
    if (!s || !lista || !*lista || s->len + 64 >= limite) return;

    Str linha;
    str_init(&linha);
    str_add(&linha, "RA metadados: ");

    char *dup = xstrdup(lista), *save = NULL, *tok = strtok_r(dup, ",", &save);
    int out = 0, truncado = 0;
    while (tok) {
        while (isspace((unsigned char)*tok)) tok++;
        char *e = tok + strlen(tok);
        while (e > tok && isspace((unsigned char)e[-1])) e--;
        *e = '\0';

        if (*tok) {
            if (linha.len > 1000) { truncado = 1; break; }
            if (out) str_add(&linha, " | ");
            str_add(&linha, tok);

            char path[PATH_MAX * 2];
            if (!ra_seguro(tok) || resolver_ra(tok, path, sizeof(path)) != 0) {
                str_add(&linha, " [inseguro/inválido]");
            } else {
                struct stat st;
                if (stat(path, &st) != 0) {
                    str_add(&linha, " [não encontrado]");
                } else {
                    char iso[64];
                    formatar_iso_utc(st.st_mtime, iso, sizeof(iso));
                    if (S_ISREG(st.st_mode)) {
                        char chk[64];
                        if (checksum_arquivo_fnv1a64(path, chk, sizeof(chk)) == 0)
                            str_addf(&linha, " [arquivo, %lld bytes, modificado=%s, checksum=%s]", (long long)st.st_size, iso, chk);
                        else
                            str_addf(&linha, " [arquivo, %lld bytes, modificado=%s, checksum=erro-leitura]", (long long)st.st_size, iso);
                    } else if (S_ISDIR(st.st_mode)) {
                        str_addf(&linha, " [diretório, modificado=%s, checksum=n/a]", iso);
                    } else {
                        str_addf(&linha, " [outro, modificado=%s, checksum=n/a]", iso);
                    }
                }
            }
            out++;
        }
        tok = strtok_r(NULL, ",", &save);
    }
    if (truncado) str_add(&linha, " | ...");
    str_add(&linha, "\n");

    if (out && s->len + linha.len < limite) str_addn(s, linha.buf, linha.len);
    free(linha.buf);
    free(dup);
}

static void append_ra(Str *s, const char *lista, size_t maxb, Tokens *q,
                      size_t *ra_usado, size_t ra_limite, ListaStr *ra_incluidos)
{
    if (!s || !lista || !*lista || s->len >= maxb) return;
    if (!ra_usado) return;
    if (ra_limite == 0) ra_limite = CTX_RA_TOTAL_MAX;

    char *dup = xstrdup(lista), *save = NULL, *tok = strtok_r(dup, ",", &save);
    while (tok && s->len < maxb && *ra_usado < ra_limite) {
        while (isspace((unsigned char)*tok)) tok++;
        char *e = tok + strlen(tok);
        while (e > tok && isspace((unsigned char)e[-1])) e--;
        *e = '\0';

        if (*tok && (!ra_incluidos || !lista_contem_str(ra_incluidos, tok))) {
            char path[PATH_MAX * 2];
            if (resolver_ra(tok, path, sizeof(path)) == 0) {
                struct stat st;
                if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) {
                    size_t restante_ctx = maxb > s->len ? maxb - s->len : 0;
                    size_t restante_ra = ra_limite > *ra_usado ? ra_limite - *ra_usado : 0;
                    if (restante_ctx < 256 || restante_ra < 256) break;

                    size_t por_arquivo = CTX_RA_SNIPPET_MAX;
                    if (por_arquivo + 160 > restante_ra) por_arquivo = restante_ra > 160 ? restante_ra - 160 : 0;
                    if (por_arquivo + 160 > restante_ctx) por_arquivo = restante_ctx > 160 ? restante_ctx - 160 : 0;
                    if (por_arquivo < 128) break;

                    size_t lidos = 0;
                    int trunc_arq = 0;
                    char *txt = ler_arquivo_prefixo(path, CTX_RA_SCAN_MAX, &lidos, &trunc_arq);
                    if (txt) {
                        if (!memchr(txt, '\0', lidos)) {
                            Str sn; str_init(&sn);
                            construir_snippet_ra(&sn, txt, lidos, trunc_arq, q, por_arquivo);
                            if (sn.len > 0) {
                                size_t antes = s->len;
                                char chk[64];
                                int tem_chk = checksum_arquivo_fnv1a64(path, chk, sizeof(chk)) == 0;
                                str_addf(s, "\n[ra:%s | %ld bytes | checksum=%s%s]\n", tok, (long)st.st_size,
                                         tem_chk ? chk : "erro-leitura",
                                         trunc_arq ? ", arquivo lido parcialmente" : "");
                                str_addn(s, sn.buf, sn.len);
                                if (s->len > maxb) { s->buf[maxb] = '\0'; s->len = maxb; }
                                *ra_usado += s->len - antes;
                                if (ra_incluidos) lista_add(ra_incluidos, tok);
                            }
                            free(sn.buf);
                        }
                        free(txt);
                    }
                }
            }
        }

        tok = strtok_r(NULL, ",", &save);
    }
    free(dup);
}

static int cmd_extrair(OrigemFiltro of, const char *filtro, const char *q,
                       int maxit, size_t maxb, int incra, int json)
{
    Itens its;
    carregar_itens(of, filtro, &its);

    Tokens tq;
    rankear(&its, q, &tq);
    if (maxit <= 0) maxit = CTX_MAX_ITENS_PADRAO;
    if (maxb < 1024) maxb = CTX_MAX_BYTES_PADRAO;

    int *selecionado = calloc((size_t)(its.total > 0 ? its.total : 1), sizeof(int));
    if (!selecionado) {
        tokens_free(&tq);
        itens_free(&its);
        return 1;
    }
    int total_sel = selecionar_itens(&its, &tq, maxit, selecionado);

    if (json) {
        Str s;
        str_init(&s);
        str_add(&s, "{\n  \"tipo\": \"sa-ctx-extrair\",\n  \"consulta\": ");
        json_escape(&s, q ? q : "");
        str_add(&s, ",\n  \"formato\": \"json\",");
        str_add(&s, "\n  \"compactacao\": \"itens ranqueados; use formato texto para mapa hierarquico compacto e ra resumido\",");
        str_add(&s, "\n  \"itens\": [");
        int out = 0;
        for (int i = 0; i < its.total && out < maxit && s.len < maxb; i++) {
            if (!selecionado[i]) continue;
            if (out) str_add(&s, ",");
            str_add(&s, "\n    ");
            item_json(&s, &its.v[i], 1);
            out++;
        }
        str_addf(&s, "\n  ],\n  \"totalRetornado\": %d,\n  \"bytes\": %zu\n}\n", out, s.len);
        fputs(s.buf, stdout);
        free(s.buf);
    } else {
        Str s;
        str_init(&s);
        str_add(&s, "=== Contexto SA extraido para LLM ===\n");
        str_addf(&s, "Consulta: %s\n", q ? q : "");
        str_add(&s, "Fontes normalizadas: <sistema>/arvores e <sistema>/relspec = sistema alvo; sisc/arvores = ferramenta SISC\n");
        str_add(&s, "Metadados de confiança: origem do item, atualização da árvore, validação/aprovação e checksum dos RA são exibidos quando disponíveis.\n");
        str_addf(&s, "Compactação: %d item(ns) ranqueado(s), ancestrais preservados no mapa hierárquico, campos longos truncados", total_sel);
        if (incra) str_add(&s, "; ra incluído por linhas relevantes e orçamento total limitado");
        str_add(&s, ".\n\n");

        add_hierarquia_compacta(&s, &its, selecionado, total_sel, maxb);

        size_t ra_limite = maxb / 5;
        if (ra_limite < 2048 && maxb >= 8192) ra_limite = 2048;
        if (ra_limite > CTX_RA_TOTAL_MAX) ra_limite = CTX_RA_TOTAL_MAX;
        if (ra_limite > maxb / 2) ra_limite = maxb / 2;
        size_t ra_usado = 0;
        ListaStr ra_incluidos = {0};

        int out = 0;
        for (int i = 0; i < its.total && out < maxit && s.len < maxb; i++) {
            Item *it = &its.v[i];
            if (!selecionado[i]) continue;
            str_addf(&s, "--- %s %s id=%s score=%.3f ---\n", it->origem, it->arquivo, it->id, it->score);
            str_addf(&s, "Confiança: origem=%s; arvoreAtualizadoEm=%s (%s); contextoValidado=%s; contextoAprovado=%s; erros=%d; avisos=%d\n",
                     it->origem ? it->origem : "",
                     it->arvoreAtualizadoEm && *it->arvoreAtualizadoEm ? it->arvoreAtualizadoEm : "indisponível",
                     it->arvoreAtualizadoFonte && *it->arvoreAtualizadoFonte ? it->arvoreAtualizadoFonte : "indisponível",
                     it->contextoValidado ? "true" : "false",
                     it->contextoAprovado ? "true" : "false",
                     it->contextoErros, it->contextoAvisos);
            add_campo_compacto(&s, "Caminho", it->caminho, 900);
            add_campo_compacto(&s, "Item", it->nome, 500);
            add_campo_compacto(&s, "Status", it->status, 120);
            add_campo_compacto(&s, "GC", it->gc, 120);
            add_campo_compacto(&s, "MM", it->mm, CTX_CAMPO_TEXTO_MAX);
            add_campo_compacto(&s, "Itens relacionados", it->itens, CTX_CAMPO_LISTA_MAX);
            add_campo_compacto(&s, "Rel", it->rel, 120);
            add_campo_compacto(&s, "ConfigVin", it->configVin, CTX_CAMPO_LISTA_MAX);
            add_campo_compacto(&s, "CLT", it->clt, CTX_CAMPO_LISTA_MAX);
            add_campo_compacto(&s, "Valor", it->valor, CTX_CAMPO_TEXTO_MAX);
            add_campo_compacto(&s, "VLT", it->vlt, CTX_CAMPO_TEXTO_MAX);
            add_campo_compacto(&s, "RA", it->ra, CTX_CAMPO_LISTA_MAX);
            add_ra_metadados_texto(&s, it->ra, maxb);
            add_campo_compacto(&s, "Filhos", it->filhosResumo, CTX_CAMPO_LISTA_MAX);
            if (incra) append_ra(&s, it->ra, maxb, &tq, &ra_usado, ra_limite, &ra_incluidos);
            str_add(&s, "\n");
            out++;
        }

        if (incra && ra_usado >= ra_limite && s.len + 80 < maxb)
            str_add(&s, "[...limite total de trechos ra atingido...]\n");
        if (s.len > maxb) { s.buf[maxb] = '\0'; s.len = maxb; }
        fputs(s.buf, stdout);
        lista_free(&ra_incluidos);
        free(s.buf);
    }

    free(selecionado);
    tokens_free(&tq);
    itens_free(&its);
    return 0;
}

static int id_valido(const char*s){ if(!s||!*s)return 0; int dig=1; for(const char*p=s;*p;p++){ if(dig){ if(!isdigit((unsigned char)*p)||*p=='0')return 0; dig=0;} else if(*p=='.')dig=1; else if(!isdigit((unsigned char)*p))return 0;} return !dig; }
static int cmd_item(OrigemFiltro of,const char*filtro,const char*id){ if(!id_valido(id)){fprintf(stderr,"sa-ctx: --ctx-item exige ID válido\n");return 1;} Itens its; carregar_itens(of,filtro,&its); Str s; str_init(&s); str_add(&s,"{\n  \"tipo\": \"sa-ctx-item\",\n  \"id\": "); json_escape(&s,id); str_add(&s,",\n  \"resultados\": ["); int out=0; for(int i=0;i<its.total;i++){ if(strcmp(its.v[i].id,id)!=0)continue; if(out)str_add(&s,","); str_add(&s,"\n    "); item_json(&s,&its.v[i],0); out++; } str_addf(&s,"\n  ],\n  \"totalRetornado\": %d\n}\n",out); fputs(s.buf,stdout); free(s.buf); itens_free(&its); return out?0:2; }

static int id_em_lista(ListaStr *ids,const char*id){for(int i=0;i<ids->total;i++)if(strcmp(ids->v[i],id)==0)return 1;return 0;}

static void diag_add(Str*s,int*cnt,const char*nivel,const char*orig,const char*arq,const char*id,const char*campo,const char*msg)
{
    if (!s || !cnt) return;
    if(*cnt)str_add(s,",");
    str_add(s,"\n    {");
    int f=1;
    json_prop_str(s,&f,"nivel",nivel);
    json_prop_str(s,&f,"origem",orig);
    json_prop_str(s,&f,"arquivo",arq);
    json_prop_str(s,&f,"id",id);
    json_prop_str(s,&f,"campo",campo);
    json_prop_str(s,&f,"mensagem",msg);
    str_add(s,"\n    }");
    (*cnt)++;
}

static void validar_ids(Str*s,int*cnt,int*erros,ListaStr*ids,const char*orig,const char*arq,const char*id,const char*campo,const char*lista,int zero)
{
    if(!lista||!*lista)return;
    char*dup=xstrdup(lista),*save=NULL,*tok=strtok_r(dup,",",&save);
    while(tok){
        while(isspace((unsigned char)*tok))tok++;
        char*e=tok+strlen(tok);
        while(e>tok&&isspace((unsigned char)e[-1]))e--;
        *e='\0';
        if(*tok&&!(zero&&strcmp(tok,"0")==0)&&(!id_valido(tok)||!id_em_lista(ids,tok))){
            char msg[512];
            snprintf(msg,sizeof(msg),"referência de item inválida ou inexistente: %s",tok);
            if (s && cnt) diag_add(s,cnt,"erro",orig,arq,id,campo,msg);
            if (erros) (*erros)++;
        }
        tok=strtok_r(NULL,",",&save);
    }
    free(dup);
}

static void validar_itens_contexto(Str *s, int *diag, int *erros, int *avisos,
                                   Item *v, int total, const char *origem, const char *arquivo)
{
    if (!v || total <= 0) return;

    ListaStr ids={0},gcs={0};
    for(int i=0;i<total;i++) lista_add(&ids,v[i].id);

    for(int i=0;i<total;i++){
        Item*it=&v[i];
        if(!it->nome || !*it->nome){
            diag_add(s,diag,"erro",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"nome","item sem nome");
            if (erros) (*erros)++;
        }
        if(!status_sa_valido(it->status)){
            diag_add(s,diag,"erro",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"status","status inválido");
            if (erros) (*erros)++;
        }
        if(it->gc&&*it->gc&&!id_em_lista(&gcs,it->gc)) lista_add(&gcs,it->gc);
        if(it->rel&&*it->rel&&(!id_valido(it->rel)||!id_em_lista(&ids,it->rel))){
            diag_add(s,diag,"erro",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"rel","relação aponta para item inexistente");
            if (erros) (*erros)++;
        }
        validar_ids(s,diag,erros,&ids,it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"itens",it->itens,0);
        validar_ids(s,diag,erros,&ids,it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"configVin",it->configVin,0);
        validar_ids(s,diag,erros,&ids,it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"clt",it->clt,1);

        if(it->ra&&*it->ra){
            char*dup=xstrdup(it->ra),*save=NULL,*tok=strtok_r(dup,",",&save);
            while(tok){
                while(isspace((unsigned char)*tok))tok++;
                char*e=tok+strlen(tok);
                while(e>tok&&isspace((unsigned char)e[-1]))e--;
                *e='\0';
                if(*tok){
                    if(!ra_seguro(tok)){
                        diag_add(s,diag,"erro",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"ra","referência ra insegura ou inválida");
                        if (erros) (*erros)++;
                    } else {
                        char path[PATH_MAX*2];
                        if(resolver_ra(tok,path,sizeof(path))!=0){
                            diag_add(s,diag,"erro",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"ra","não foi possível resolver referência ra");
                            if (erros) (*erros)++;
                        } else if(access(path,R_OK)!=0){
                            char msg[PATH_MAX*2];
                            snprintf(msg,sizeof(msg),"arquivo referenciado não encontrado: %s",tok);
                            diag_add(s,diag,"aviso",it->origem?it->origem:origem,it->arquivo?it->arquivo:arquivo,it->id,"ra",msg);
                            if (avisos) (*avisos)++;
                        }
                    }
                }
                tok=strtok_r(NULL,",",&save);
            }
            free(dup);
        }
    }

    if(gcs.total>1){
        diag_add(s,diag,"erro",origem,arquivo,"","gc","mais de um GC ativo na mesma árvore");
        if (erros) (*erros)++;
    }
    lista_free(&gcs);
    lista_free(&ids);
}

static int itens_contexto_aprovado(Item *v, int total)
{
    int com_status = 0;
    for (int i = 0; i < total; i++) {
        if (status_vazio_ou_null(v[i].status)) continue;
        com_status = 1;
        if (!status_aprovado_contexto(v[i].status)) return 0;
    }
    return com_status;
}

static void validar_add_arvore(Str *arvs, int *count, const char *origem, const char *arquivo,
                               const char *arvore, const char *criado, const char *atualizado,
                               const char *fonte_atualizado, int itens, int validado,
                               int aprovado, int erros, int avisos)
{
    if (*count) str_add(arvs, ",");
    str_add(arvs, "\n    {");
    int f = 1;
    json_prop_str(arvs, &f, "origem", origem);
    json_prop_str(arvs, &f, "arquivo", arquivo);
    json_prop_str(arvs, &f, "arvore", arvore);
    json_prop_str(arvs, &f, "arvoreCriadoEm", criado);
    json_prop_str(arvs, &f, "arvoreAtualizadoEm", atualizado);
    json_prop_str(arvs, &f, "arvoreAtualizadoFonte", fonte_atualizado);
    json_prop_int(arvs, &f, "itens", itens);
    json_prop_bool(arvs, &f, "contextoValidado", validado);
    json_prop_bool(arvs, &f, "contextoAprovado", aprovado);
    json_prop_int(arvs, &f, "erros", erros);
    json_prop_int(arvs, &f, "avisos", avisos);
    str_add(arvs, "\n    }");
    (*count)++;
}

static int cmd_validar(OrigemFiltro of,const char*filtro)
{
    Fonte fontes[4];
    int nf=0;
    montar_fontes(of,fontes,&nf);

    Str s;
    str_init(&s);
    Str arvs;
    str_init(&arvs);

    int diag=0,erros=0,avisos=0,arqs=0,validas=0,arv_count=0,aprovadas=0;
    char validado_em[64];
    formatar_iso_utc(time(NULL), validado_em, sizeof(validado_em));

    str_add(&s,"{\n  \"tipo\": \"sa-ctx-validar\",");
    str_add(&s,"\n  \"validadoEm\": ");
    json_escape(&s, validado_em);
    str_add(&s,",\n  \"diagnosticos\": [");

    for(int fi=0;fi<nf;fi++){
        ListaStr files={0};
        listar_json_dir(fontes[fi].dir,&files);
        for(int ai=0;ai<files.total;ai++){
            arqs++;
            char rot[PATH_MAX*2];
            snprintf(rot,sizeof(rot),"%s/%s",fontes[fi].rotulo,base_nome(files.v[ai]));

            char*txt=ler_arquivo(files.v[ai],50*1024*1024);
            Arvore a;
            if(!txt||parse_arvore(txt,&a)!=0){
                diag_add(&s,&diag,"erro",fontes[fi].origem,rot,"","json","arquivo não é uma árvore SA válida");
                erros++;
                validar_add_arvore(&arvs,&arv_count,fontes[fi].origem,rot,"","","","",0,0,0,1,0);
                free(txt);
                continue;
            }
            free(txt);

            if(!filtro_arvore_casa(files.v[ai],a.nome,filtro)){
                arvore_liberar(&a);
                continue;
            }
            validas++;

            char *mtime = arquivo_mtime_iso(files.v[ai]);
            const char *atualizado = (a.atualizadoEm && *a.atualizadoEm) ? a.atualizadoEm : mtime;
            const char *fonte_atualizado = (a.atualizadoEm && *a.atualizadoEm) ? "arvore.atualizadoEm" : "arquivo.mtime";

            Itens its={0};
            int idx[256];
            coletar_rec(&its,&a.raiz,idx,0,fontes[fi].origem,rot,a.nome?a.nome:"",
                        a.criadoEm?a.criadoEm:"", atualizado?atualizado:"", fonte_atualizado, "");

            int erros_arv=0, avisos_arv=0;
            validar_itens_contexto(&s,&diag,&erros_arv,&avisos_arv,its.v,its.total,fontes[fi].origem,rot);
            int aprovado = itens_contexto_aprovado(its.v, its.total);
            if (aprovado) aprovadas++;
            erros += erros_arv;
            avisos += avisos_arv;

            validar_add_arvore(&arvs,&arv_count,fontes[fi].origem,rot,a.nome?a.nome:"",
                               a.criadoEm?a.criadoEm:"", atualizado?atualizado:"", fonte_atualizado,
                               its.total, erros_arv==0, aprovado, erros_arv, avisos_arv);

            itens_free(&its);
            free(mtime);
            arvore_liberar(&a);
        }
        lista_free(&files);
    }

    str_add(&s,"\n  ],\n  \"arvores\": [");
    str_addn(&s, arvs.buf, arvs.len);
    str_addf(&s,"\n  ],\n  \"arquivosJson\": %d,\n  \"arvoresValidas\": %d,\n  \"diagnosticosTotal\": %d,\n  \"erros\": %d,\n  \"avisos\": %d,\n  \"contextoValidado\": %s,\n  \"contextoAprovado\": %s,\n  \"ok\": %s\n}\n",
             arqs,validas,diag,erros,avisos,erros?"false":"true",(arv_count>0&&aprovadas==arv_count)?"true":"false",erros?"false":"true");
    fputs(s.buf,stdout);
    free(arvs.buf);
    free(s.buf);
    return erros?2:0;
}

static int cmd_atualizar_indice(OrigemFiltro of,const char*filtro)
{ Itens its; carregar_itens(of,filtro,&its); int total_itens=its.total; Str s; str_init(&s); str_addf(&s,"{\n  \"tipo\": \"sa-ctx-indice-cache\",\n  \"total\": %d,\n  \"itens\": [",its.total); for(int i=0;i<its.total;i++){if(i)str_add(&s,",");str_add(&s,"\n    ");item_json(&s,&its.v[i],0);} str_add(&s,"\n  ]\n}\n"); char cwd[PATH_MAX],root[PATH_MAX],dest[PATH_MAX*2]; getcwd(cwd,sizeof(cwd)); if(of==ORIGEM_FERRAMENTA&&raiz_sisc(root,sizeof(root))==0)snprintf(dest,sizeof(dest),"%s/arvores/.indice-contexto.json",root); else snprintf(dest,sizeof(dest),"%s/arvores/.indice-contexto.json",cwd); char dir[PATH_MAX*2];snprintf(dir,sizeof(dir),"%s",dest);char*slash=strrchr(dir,'/');if(slash){*slash='\0';mkdir_p(dir);} int ok=escrever_arquivo(dest,s.buf)==0; free(s.buf); itens_free(&its); Str o;str_init(&o);str_add(&o,"{\n  \"tipo\": \"sa-ctx-atualizar-indice\",\n  \"arquivo\": ");json_escape(&o,dest);str_addf(&o,",\n  \"total\": %d,\n  \"ok\": %s\n}\n",total_itens,ok?"true":"false");fputs(o.buf,stdout);free(o.buf);return ok?0:1; }

static void ajuda(void)
{
    fputs(
        "Uso:\n"
        "  pp --sa-ctx --ctx-fontes [opcoes]\n"
        "  pp --sa-ctx --ctx-indice [--origem=auto|sistema|ferramenta] [--sa=<arvore>]\n"
        "  pp --sa-ctx --ctx-buscar=\"texto\" [--max-itens=N]\n"
        "  pp --sa-ctx --ctx-extrair=\"texto\" [--max-itens=N] [--max-bytes=N] [--include-ra] [--json]\n"
        "  pp --sa-ctx --ctx-item=<id> [--sa=<arvore>]\n"
        "  pp --sa-ctx --ctx-validar [--origem=auto|sistema|ferramenta]\n"
        "  pp --sa-ctx --ctx-atualizar-indice [--origem=sistema|ferramenta]\n"
        "\n"
        "Descrição:\n"
        "  API de contexto SA para consumo pelo próprio sistema e por LLMs.\n"
        "  É somente-leitura, exceto --ctx-atualizar-indice, que grava .indice-contexto.json.\n"
        "  Normaliza as fontes de contexto:\n"
        "    <sistema>/arvores      = árvores SA do sistema alvo\n"
        "    <sistema>/relspec      = relatórios SA derivados de specs\n"
        "    sisc/arvores           = contexto da ferramenta SISC\n"
        "\n"
        "Grupos de ajuda: uso, fontes, indice, busca, extrair, item, validar, cache, origem, exemplos.\n"
        "\n"
        "1) Fontes [fontes origem diretorios]\n"
        "  --ctx-fontes                  Lista fontes normalizadas em JSON.\n"
        "  --origem=auto                 Lista sistema alvo + ferramenta SISC (padrão).\n"
        "  --origem=sistema              Lista <sistema>/arvores e <sistema>/relspec.\n"
        "  --origem=ferramenta           Lista apenas sisc/arvores.\n"
        "\n"
        "2) Índice [indice itens json]\n"
        "  --ctx-indice                  Exporta todos os itens SA em JSON.\n"
        "  --sa=<arvore>                 Quando usado pelo pp, restringe a árvore informada.\n"
        "  Campos principais: origem, arquivo, arvore, id, nome, caminho, filhos, gc, mm, itens, ra, rel.\n"
        "  Metadados de confiança: arvoreCriadoEm, arvoreAtualizadoEm, contextoValidado, contextoAprovado e raMetadados/checksum.\n"
        "\n"
        "3) Busca [buscar search ranking]\n"
        "  --ctx-buscar=<texto>          Busca/rankeia itens por pontuação ponderada: nome, caminho, mm, ra, filhos, GC e status.\n"
        "  --ctx-search=<texto>          Alias de --ctx-buscar.\n"
        "  --max-itens=N                 Limita resultados retornados; padrão 30.\n"
        "\n"
        "4) Extração para LLM [extrair contexto llm]\n"
        "  --ctx-extrair=<texto>         Retorna contexto textual pronto para entrar no prompt da LLM.\n"
        "  --ctx-extract=<texto>         Alias de --ctx-extrair.\n"
        "  No formato texto, gera mapa hierárquico compacto com ancestrais dos itens ranqueados.\n"
        "  --json ou --formato=json      Retorna a extração em JSON estruturado.\n"
        "  --formato=texto               Força saída textual.\n"
        "  --max-bytes=N                 Limite de bytes da extração textual; padrão 60000.\n"
        "  --include-ra                  Inclui trechos seguros e resumidos dos arquivos referenciados por ra.\n"
        "  --sem-ra                      Não inclui conteúdo de ra (padrão).\n"
        "\n"
        "5) Item específico [item id]\n"
        "  --ctx-item=<id>               Retorna item com ID posicional, ex: 1, 2.1, 3.4.2.\n"
        "  --sa=<arvore>                 Recomendada para evitar colisão de IDs entre árvores.\n"
        "\n"
        "6) Validação [validar integridade ra relacoes]\n"
        "  --ctx-validar                 Valida JSON SA, relações, itens, status, GC, ra e integridade básica.\n"
        "  --ctx-validate                Alias de --ctx-validar.\n"
        "  Retorna JSON com ok=true/false, erros, avisos, diagnósticos e resumo por árvore com atualização/validação/aprovação.\n"
        "\n"
        "7) Cache de índice [cache atualizar indice]\n"
        "  --ctx-atualizar-indice        Gera arvores/.indice-contexto.json.\n"
        "  --ctx-update-index            Alias de --ctx-atualizar-indice.\n"
        "  Com --origem=sistema grava em <sistema>/arvores/.indice-contexto.json.\n"
        "  Com --origem=ferramenta grava em sisc/arvores/.indice-contexto.json.\n"
        "\n"
        "8) Exemplos [exemplos]\n"
        "  pp --sa-ctx --ctx-fontes\n"
        "  pp --sa-ctx --ctx-indice --origem=sistema\n"
        "  pp --sa-ctx --ctx-buscar=\"checkout pix cliente\" --max-itens=20\n"
        "  pp --sa-ctx --ctx-extrair=\"checkout pix cliente\" --max-bytes=60000 --include-ra\n"
        "  pp --sa-ctx operador-manual-conector --ctx-item=2.1\n"
        "  pp --sa-ctx --ctx-validar --origem=auto\n"
        "  pp --sa-ctx --ctx-atualizar-indice --origem=sistema\n",
        stdout);
}

static int eh_ajuda_arg(const char *a)
{
    return a && (strcmp(a, "--ajuda") == 0 || strcmp(a, "--help") == 0 || strcmp(a, "-h") == 0 ||
                 strncmp(a, "--ajuda:", 8) == 0 || strncmp(a, "--ajuda=", 8) == 0 ||
                 strncmp(a, "--help:", 7) == 0 || strncmp(a, "--help=", 7) == 0);
}

typedef struct { const char *nome; int fontes, indice, validar, atualizar; const char *buscar, *extrair, *item, *origem, *formato; int json, include_ra, max_itens; size_t max_bytes; } Cfg;
static void parse_args(int argc,char**argv,Cfg*c){ memset(c,0,sizeof(*c)); c->origem="auto"; c->max_itens=CTX_MAX_ITENS_PADRAO; c->max_bytes=CTX_MAX_BYTES_PADRAO; for(int i=1;i<argc;i++){char*a=argv[i]; if(strcmp(a,"--json")==0)c->json=1; else if(strcmp(a,"--ctx-fontes")==0)c->fontes=1; else if(strcmp(a,"--ctx-indice")==0)c->indice=1; else if(strncmp(a,"--ctx-buscar=",13)==0)c->buscar=a+13; else if(strncmp(a,"--ctx-search=",13)==0)c->buscar=a+13; else if(strncmp(a,"--ctx-extrair=",14)==0)c->extrair=a+14; else if(strncmp(a,"--ctx-extract=",14)==0)c->extrair=a+14; else if(strncmp(a,"--ctx-item=",11)==0)c->item=a+11; else if(strcmp(a,"--ctx-validar")==0||strcmp(a,"--ctx-validate")==0)c->validar=1; else if(strcmp(a,"--ctx-atualizar-indice")==0||strcmp(a,"--ctx-update-index")==0)c->atualizar=1; else if(strncmp(a,"--origem=",9)==0)c->origem=a+9; else if(strncmp(a,"--max-itens=",12)==0)c->max_itens=atoi(a+12); else if(strncmp(a,"--max-bytes=",12)==0)c->max_bytes=(size_t)atol(a+12); else if(strcmp(a,"--include-ra")==0)c->include_ra=1; else if(strcmp(a,"--sem-ra")==0)c->include_ra=0; else if(strncmp(a,"--formato=",10)==0)c->formato=a+10; else if(a[0]!='-'&&!c->nome)c->nome=a; }}

int main(int argc,char**argv)
{
    for (int i = 1; i < argc; i++) {
        if (eh_ajuda_arg(argv[i])) {
            ajuda();
            return 0;
        }
    }

    Cfg c;
    parse_args(argc, argv, &c);
    int ops = c.fontes + c.indice + c.validar + c.atualizar +
              (c.buscar ? 1 : 0) + (c.extrair ? 1 : 0) + (c.item ? 1 : 0);
    if (ops != 1) {
        fprintf(stderr, "sa-ctx: use exatamente uma flag --ctx-*; veja --ajuda\n");
        return 1;
    }

    OrigemFiltro of;
    if (parse_origem(c.origem, &of) != 0) {
        fprintf(stderr, "sa-ctx: --origem aceita auto, sistema ou ferramenta\n");
        return 1;
    }
    if (c.formato && strcmp(c.formato, "json") != 0 && strcmp(c.formato, "texto") != 0) {
        fprintf(stderr, "sa-ctx: --formato aceita json ou texto\n");
        return 1;
    }

    if (c.fontes) return cmd_fontes(of);
    if (c.indice) return cmd_indice(of, c.nome);
    if (c.buscar) return cmd_buscar(of, c.nome, c.buscar, c.max_itens);
    if (c.extrair) {
        int js = c.json || (c.formato && strcmp(c.formato, "json") == 0);
        return cmd_extrair(of, c.nome, c.extrair, c.max_itens, c.max_bytes, c.include_ra, js);
    }
    if (c.item) return cmd_item(of, c.nome, c.item);
    if (c.validar) return cmd_validar(of, c.nome);
    if (c.atualizar) return cmd_atualizar_indice(of, c.nome);
    return 1;
}
