#!/usr/bin/env python3
# start - preflight dinamico do SISC para o sistema atual
# Uso: pp --start [--json] [--somente-pendencias]

import json
import os
import re
import stat
import subprocess
import sys
import time
from pathlib import Path

RESERVADOS_RAIZ = {
    "core", "ajuda", "arvores", "rascunho", "secretos", "testes", "tmp", "cache",
    "logs", "log", ".git", ".svn", ".hg", "node_modules", "vendor"
}

ESPACO_DIRS = [
    "entrada", "caixas", "processando", "concluidas", "erro", "quarentena",
    "duplicadas", "locks", "tmp", "eventos", "idempotencia", "indice",
    "processos", "tentativas", "vinculos", "api-publicadas", "estado"
]

ESCUTA_EXES = ["escuta", "runtime-agente", "runtime-conector"]

class Relatorio:
    def __init__(self):
        self.itens = []

    def add(self, nivel, escopo, item, detalhe="", solucao=""):
        self.itens.append({
            "nivel": nivel,
            "escopo": escopo,
            "item": item,
            "detalhe": detalhe,
            "solucao": solucao,
        })

    def ok(self, escopo, item, detalhe=""):
        self.add("OK", escopo, item, detalhe, "")

    def aviso(self, escopo, item, detalhe="", solucao=""):
        self.add("AVISO", escopo, item, detalhe, solucao)

    def pend(self, escopo, item, detalhe="", solucao=""):
        self.add("PENDENTE", escopo, item, detalhe, solucao)

    def erro(self, escopo, item, detalhe="", solucao=""):
        self.add("ERRO", escopo, item, detalhe, solucao)

    def pendencias(self):
        return [i for i in self.itens if i["nivel"] in ("PENDENTE", "ERRO")]


def uso():
    print("""Uso:
  pp --start [--json] [--somente-pendencias]

Descrição:
  Executa um preflight dinâmico antes de iniciar trabalho no SISC.
  Deve ser chamado diretamente dentro da raiz de um sistema, isto é,
  em um subdiretório direto de sisc, por exemplo:
    cd /var/www/html/sisc/siscore && ../pp --start

O comando lista pendências da instalação SISC e do sistema atual.
""")


def real(p):
    try:
        return Path(p).resolve()
    except Exception:
        return Path(p).absolute()


def raiz_sisc_por_executavel():
    exe = real(sys.argv[0])
    # <raiz>/core/comandos/start
    try:
        return exe.parents[2]
    except Exception:
        return None


def resolver_sistema_atual(raiz, rel):
    cwd = real(Path.cwd())
    try:
        cwd.relative_to(raiz)
    except Exception:
        rel.erro("ambiente", "diretório atual fora da instalação SISC", f"cwd={cwd}; raiz={raiz}", "Entre na raiz de um sistema e execute pp --start.")
        return None

    if cwd == raiz:
        rel.erro("ambiente", "pp --start não pode ser executado na raiz sisc", f"cwd={cwd}", "Entre em um sistema: cd <raiz-sisc>/<sistema>.")
        return None

    pai = cwd.parent
    if pai != raiz:
        rel.erro("ambiente", "pp --start deve ser executado diretamente na raiz do sistema", f"cwd={cwd}", "Suba até o diretório do sistema antes de iniciar.")
        return None

    if cwd.name in RESERVADOS_RAIZ:
        rel.erro("ambiente", "diretório atual não parece ser um sistema SISC", f"diretório reservado: {cwd.name}", "Entre em um sistema real, por exemplo siscore/ ou outro sistema criado.")
        return None

    return cwd


def ler_json(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f), None
    except Exception as e:
        return None, str(e)


def modo(path):
    try:
        return stat.S_IMODE(os.stat(path).st_mode)
    except Exception:
        return None


def fmt_modo(path):
    m = modo(path)
    return "???" if m is None else oct(m)[2:].zfill(3)


def is_exec(path):
    return path.is_file() and os.access(path, os.X_OK)


def normalizar_conta(v):
    if not isinstance(v, str):
        return ""
    v = v.strip()
    if not v:
        return ""
    if not re.match(r"^[A-Za-z0-9._-]+$", v):
        return ""
    return v if v.endswith(".json") else v + ".json"


def secret_file_ok(path):
    return path.is_file() and os.access(path, os.R_OK)


def env_key_name(provedor):
    if not provedor:
        return ""
    return re.sub(r"[^A-Za-z0-9]", "_", str(provedor)).upper() + "_API_KEY"


def resolver_segredo_conta(conta_json, dir_secretos):
    """Retorna (ok, detalhes, avisos, pendencias) sem vazar segredo."""
    avisos, pend = [], []
    if not isinstance(conta_json, dict):
        return False, "JSON de conta não é objeto", avisos, ["JSON de conta inválido"]

    modelo = conta_json.get("modelo")
    url = conta_json.get("url")
    if not modelo:
        pend.append("campo modelo ausente")
    if not url:
        pend.append("campo url ausente")

    auth = conta_json.get("autenticacao") if isinstance(conta_json.get("autenticacao"), dict) else {}
    tipo = conta_json.get("tipo") or auth.get("tipo") or "key"
    provedor = conta_json.get("provedor") or auth.get("provedor") or ""
    conexao = conta_json.get("conexao") or ""
    api_key_nome = auth.get("apiKey") or conta_json.get("apiKey") or ""
    arquivo = auth.get("arquivo") or conta_json.get("arquivo") or ""

    if tipo == "oauth":
        base = arquivo or conexao
        if not base:
            pend.append("conta oauth sem autenticacao.arquivo/conexao")
        else:
            sec = dir_secretos / (base if str(base).endswith(".json") else str(base) + ".json")
            if not sec.is_file():
                pend.append(f"segredo oauth ausente: secretos/{sec.name}")
            else:
                tok, err = ler_json(sec)
                if err:
                    pend.append(f"segredo oauth inválido: secretos/{sec.name}")
                else:
                    access = bool(tok.get("access") or tok.get("access_token")) if isinstance(tok, dict) else False
                    refresh = bool(tok.get("refresh") or tok.get("refresh_token")) if isinstance(tok, dict) else False
                    exp = tok.get("expires", 0) if isinstance(tok, dict) else 0
                    account = bool(tok.get("accountId") or tok.get("account_id")) if isinstance(tok, dict) else False
                    if provedor in ("codex", "openai-codex") and not account:
                        pend.append(f"oauth codex sem accountId em secretos/{sec.name}")
                    if not access and not refresh:
                        pend.append(f"oauth sem access/refresh em secretos/{sec.name}")
                    elif isinstance(exp, (int, float)) and exp and exp <= time.time() + 60:
                        if refresh:
                            avisos.append(f"oauth expirado em secretos/{sec.name}; depende de refresh válido")
                        else:
                            pend.append(f"oauth expirado sem refresh em secretos/{sec.name}")
                if modo(sec) is not None and (modo(sec) & 0o077):
                    avisos.append(f"permissão ampla em secretos/{sec.name}: {fmt_modo(sec)}; recomendado 600")
    else:
        candidatos = []
        if conexao:
            candidatos.append(dir_secretos / f"{conexao}.txt")
        if api_key_nome:
            nome = str(api_key_nome)
            candidatos.append(dir_secretos / (nome if nome.endswith(".txt") else nome + ".txt"))
        envn = env_key_name(provedor)
        if envn and os.environ.get(envn):
            detalhe = f"resolvido por variável {envn}"
        else:
            achou = next((p for p in candidatos if secret_file_ok(p)), None)
            if achou:
                detalhe = f"resolvido por secretos/{achou.name}"
                if modo(achou) is not None and (modo(achou) & 0o077):
                    avisos.append(f"permissão ampla em secretos/{achou.name}: {fmt_modo(achou)}; recomendado 600")
            else:
                nomes = ", ".join("secretos/" + p.name for p in candidatos) or "secretos/<conexao>.txt"
                pend.append(f"chave não resolvida; esperado {envn or '<PROVEDOR>_API_KEY'} ou {nomes}")
                detalhe = "sem segredo resolvido"

    ok = not pend
    return ok, detalhe if 'detalhe' in locals() else "segredo avaliado", avisos, pend


def listar_comandos(core_comandos):
    if not core_comandos.is_dir():
        return []
    out = []
    for p in core_comandos.iterdir():
        if not p.is_file() or not os.access(p, os.X_OK):
            continue
        n = p.name
        if n in ("Makefile", "heb.txt") or n.endswith(".c") or n.endswith(".bak"):
            continue
        out.append(n)
    return sorted(out)


def auditar_permissionamento_secretos(rel, escopo, dir_secretos):
    if not dir_secretos.exists():
        rel.pend(escopo, "diretório secretos ausente", str(dir_secretos), "Crie/importe secretos antes de usar contas LLM/conectores.")
        return
    if not dir_secretos.is_dir():
        rel.pend(escopo, "secretos não é diretório", str(dir_secretos))
        return
    m = modo(dir_secretos)
    if m is not None and (m & 0o077):
        rel.aviso(escopo, "permissão ampla no diretório secretos", f"{dir_secretos}: {fmt_modo(dir_secretos)}", f"chmod 700 {dir_secretos}")
    elif os.access(dir_secretos, os.R_OK):
        rel.ok(escopo, "diretório secretos acessível e restrito", f"{dir_secretos} ({fmt_modo(dir_secretos)})")
    else:
        rel.pend(escopo, "diretório secretos sem leitura", str(dir_secretos), "Ajuste dono/permissões.")

    for p in sorted(dir_secretos.iterdir()):
        if not p.is_file():
            continue
        if p.name == ".htaccess" or p.name.endswith(".pub"):
            continue
        m = modo(p)
        if m is not None and (m & 0o077):
            rel.aviso(escopo, "arquivo secreto com permissão ampla", f"{p}: {fmt_modo(p)}", f"chmod 600 {p}")


def auditar_contas(rel, escopo, dir_contas, dir_secretos, exigir_llm=True):
    if not dir_contas.exists():
        rel.pend(escopo, "diretório contas-llm ausente", str(dir_contas), "Importe/crie contas LLM.")
        return set()
    if not dir_contas.is_dir():
        rel.pend(escopo, "contas-llm não é diretório", str(dir_contas))
        return set()

    arquivos = sorted(p for p in dir_contas.iterdir() if p.name.endswith(".json"))
    if not arquivos:
        rel.pend(escopo, "nenhuma conta LLM encontrada", str(dir_contas), "Crie/importe pelo menos uma conta e llm.json.")
        return set()

    nomes = {p.name for p in arquivos}
    if exigir_llm and "llm.json" not in nomes:
        rel.pend(escopo, "conta ativa ausente", f"{dir_contas}/llm.json", "Execute pp --setar-conta-llm ou importe contas.")

    ok_count = 0
    for p in arquivos:
        j, err = ler_json(p)
        if err:
            rel.pend(escopo, "JSON de conta inválido", f"{p}: {err}")
            continue
        ok, detalhe, avisos, pend = resolver_segredo_conta(j, dir_secretos)
        for a in avisos:
            rel.aviso(escopo, f"conta {p.name}: aviso de segredo", a)
        for pe in pend:
            rel.pend(escopo, f"conta {p.name}: pendência", pe, "Ajuste o preset ou o arquivo correspondente em secretos/.")
        if ok:
            ok_count += 1
    if ok_count:
        rel.ok(escopo, "contas LLM com segredo resolvido", f"{ok_count}/{len(arquivos)}")
    return nomes


def normalizar_mapa_comandos(mapa):
    if not isinstance(mapa, dict):
        return None
    if mapa.get("tipo") == "arvore-sa":
        out = {}
        for item in mapa.get("filhos", []):
            if not isinstance(item, dict):
                continue
            nome = item.get("nome")
            conta = item.get("mm") or item.get("valor") or item.get("conta")
            if not conta:
                filhos = item.get("filhos")
                if isinstance(filhos, list) and filhos and isinstance(filhos[0], dict):
                    conta = filhos[0].get("nome")
            if isinstance(nome, str) and isinstance(conta, str) and nome:
                out[nome[2:] if nome.startswith("--") else nome] = conta
        return out
    return {k: v for k, v in mapa.items() if isinstance(k, str) and isinstance(v, str)}


def auditar_mapa(rel, raiz, sistema, contas_sistema):
    mapa_path = raiz / "core" / "config" / "mapa-comandos.json"
    comandos = listar_comandos(raiz / "core" / "comandos")
    if not comandos:
        rel.pend("sisc", "nenhum comando executável encontrado", str(raiz / "core" / "comandos"))
        return

    if not mapa_path.is_file():
        rel.pend("sisc", "mapa de comandos ausente", str(mapa_path), "Crie core/config/mapa-comandos.json.")
        return
    mapa_raw, err = ler_json(mapa_path)
    mapa = normalizar_mapa_comandos(mapa_raw) if not err else None
    if err or mapa is None:
        rel.pend("sisc", "mapa de comandos inválido", f"{mapa_path}: {err or 'formato não reconhecido'}")
        return

    faltam = [c for c in comandos if c not in mapa]
    extras = [c for c in mapa.keys() if c not in comandos]
    if faltam:
        rel.pend("sisc", "comandos sem conta no mapa", ", ".join(faltam), "Atualize core/config/mapa-comandos.json.")
    else:
        rel.ok("sisc", "mapa cobre todos os comandos executáveis", f"{len(comandos)} comandos")
    if extras:
        rel.aviso("sisc", "mapa contém comandos sem executável", ", ".join(extras), "Remova ou recrie os executáveis correspondentes.")

    usos = {}
    for cmd, conta in mapa.items():
        nc = normalizar_conta(conta)
        if nc:
            usos.setdefault(nc, []).append(cmd)
    ausentes = {conta: cmds for conta, cmds in usos.items() if contas_sistema and conta not in contas_sistema}
    # Se contas_sistema está vazio porque contas-llm ausente, a pendência principal já foi registrada.
    if contas_sistema and ausentes:
        for conta, cmds in sorted(ausentes.items()):
            rel.pend("sistema", "conta mapeada ausente no sistema atual", f"{conta} usada por {len(cmds)} comando(s): {', '.join(cmds[:8])}{'...' if len(cmds) > 8 else ''}", f"Crie/importe {sistema}/contas-llm/{conta} ou altere o mapa.")
    elif contas_sistema:
        rel.ok("sistema", "contas referenciadas pelo mapa existem no sistema atual", f"{len(usos)} conta(s) distintas")

    mapa_local = sistema / "config" / "mapa-comandos.json"
    if mapa_local.exists():
        ml_raw, er = ler_json(mapa_local)
        ml = normalizar_mapa_comandos(ml_raw) if not er else None
        if er or ml is None:
            rel.pend("sistema", "mapa local inválido", f"{mapa_local}: {er or 'formato não reconhecido'}")
        else:
            rel.ok("sistema", "mapa local encontrado", str(mapa_local))
    else:
        rel.aviso("sistema", "mapa local ausente", "usando fallback core/config/mapa-comandos.json", "Crie config/mapa-comandos.json se quiser roteamento LLM específico deste sistema.")


def auditar_bibliotecas(rel, raiz):
    spec_dir = raiz / "core" / "biblioteca-specs"
    ag_dir = raiz / "core" / "biblioteca-agentes-fixos"
    contas = raiz / "core" / "biblioteca-contas-llm"
    secretos = raiz / "core" / "biblioteca-secretos"

    if spec_dir.is_dir():
        specs = sorted(p for p in spec_dir.iterdir() if p.name.endswith(".json"))
        if specs:
            rel.ok("sisc", "biblioteca de specs presente", f"{len(specs)} JSON(s)")
        else:
            rel.pend("sisc", "biblioteca de specs vazia", str(spec_dir))
        if not (spec_dir / "specs-ativacao.json").is_file():
            rel.aviso("sisc", "specs-ativacao.json ausente", str(spec_dir / "specs-ativacao.json"))
    else:
        rel.pend("sisc", "biblioteca de specs ausente", str(spec_dir))

    if ag_dir.is_dir():
        agentes = sorted(p for p in ag_dir.iterdir() if p.name.endswith(".json"))
        if agentes:
            rel.ok("sisc", "biblioteca de agentes fixos presente", f"{len(agentes)} agente(s)")
        else:
            rel.pend("sisc", "biblioteca de agentes fixos vazia", str(ag_dir))
    else:
        rel.pend("sisc", "biblioteca de agentes fixos ausente", str(ag_dir))

    auditar_permissionamento_secretos(rel, "sisc", secretos)
    auditar_contas(rel, "sisc", contas, secretos, exigir_llm=True)


def auditar_arvores(rel, sistema):
    raiz_sisc = sistema.parent
    arvores = raiz_sisc / "arvores"
    nome = "operador-manual-funcional.json"
    p = arvores / nome
    if not p.is_file():
        rel.pend("sistema", f"árvore obrigatória ausente: {nome}", str(p), "Execute pp --atualizar-manual-funcional.")
    else:
        _, err = ler_json(p)
        if err:
            rel.pend("sistema", f"árvore inválida: {nome}", err, "Regenere com pp --atualizar-manual-funcional.")
        else:
            rel.ok("sistema", f"árvore presente: {nome}", str(p))


def auditar_escuta(rel, sistema):
    esc = sistema / "escuta"
    if not esc.is_dir():
        rel.pend("sistema", "diretório escuta ausente", str(esc))
        return
    for exe in ESCUTA_EXES:
        p = esc / exe
        if not p.exists():
            rel.pend("sistema", f"executável de escuta ausente: {exe}", str(p), "Compile/copiar escuta do template siscore.")
        elif not os.access(p, os.X_OK):
            rel.pend("sistema", f"executável de escuta sem permissão: {exe}", f"{p} ({fmt_modo(p)})", f"chmod +x {p}")
        else:
            rel.ok("sistema", f"executável de escuta OK: {exe}", str(p))

    pares = [("escuta", "escuta.c"), ("runtime-agente", "runtime-agente.c"), ("runtime-conector", "runtime-conector.c")]
    comum = esc / "runtime_comum.c"
    for exe, src in pares:
        ep, sp = esc / exe, esc / src
        if ep.is_file() and sp.is_file() and sp.stat().st_mtime > ep.stat().st_mtime + 1:
            rel.pend("sistema", f"{exe} desatualizado em relação ao fonte", f"{src} é mais novo que {exe}", f"make -C {sistema}")
        if exe.startswith("runtime") and ep.is_file() and comum.is_file() and comum.stat().st_mtime > ep.stat().st_mtime + 1:
            rel.pend("sistema", f"{exe} desatualizado em relação ao runtime_comum.c", str(comum), f"make -C {sistema}")

    pidfile = sistema / "espaco" / "locks" / "escuta-daemon.pid"
    lockfile = sistema / "espaco" / "locks" / "escuta.lock"
    pid = ""
    for pf in (pidfile, lockfile):
        try:
            txt = re.sub(r"\D", "", pf.read_text())
            if txt:
                os.kill(int(txt), 0)
                pid = txt
                break
        except Exception:
            pass
    if pid:
        rel.ok("sistema", "escuta ligada", f"pid={pid}")
    else:
        rel.aviso("sistema", "escuta desligada", f"base={sistema}", f"pp --ligar-escuta --base={sistema}")


def auditar_espaco(rel, sistema):
    esp = sistema / "espaco"
    if not esp.is_dir():
        rel.pend("sistema", "diretório espaco ausente", str(esp), "Inicialize escuta/espaco.")
        return
    if not os.access(esp, os.W_OK):
        rel.pend("sistema", "espaco sem permissão de escrita", f"{esp} ({fmt_modo(esp)})", "Ajuste dono/grupo/permissões.")
    else:
        rel.ok("sistema", "espaco gravável", f"{esp} ({fmt_modo(esp)})")
    ht = esp / ".htaccess"
    if not ht.is_file():
        rel.pend("sistema", "espaco sem .htaccess de bloqueio HTTP", str(ht), "Crie .htaccess negando acesso web ao espaco/.")
    else:
        rel.ok("sistema", "espaco protegido por .htaccess", str(ht))

    for d in ESPACO_DIRS:
        p = esp / d
        if not p.is_dir():
            rel.pend("sistema", f"subdiretório do espaco ausente: {d}", str(p), "Execute escuta --init ou recrie a estrutura do espaco.")
        elif not os.access(p, os.W_OK):
            rel.pend("sistema", f"subdiretório do espaco sem escrita: {d}", f"{p} ({fmt_modo(p)})", "Ajuste dono/grupo/permissões.")
        elif modo(p) is not None and (modo(p) & 0o002):
            rel.aviso("sistema", f"subdiretório do espaco world-writable: {d}", f"{p} ({fmt_modo(p)})", f"chmod 775 {p}")

    # Procura recursiva curta por diretórios de espaco sem escrita para o usuário atual.
    problemas = []
    for root, dirs, files in os.walk(esp):
        rp = Path(root)
        if not os.access(rp, os.W_OK):
            problemas.append(rp)
            if len(problemas) >= 10:
                break
    if problemas:
        rel.pend("sistema", "há diretórios internos do espaco sem escrita", "; ".join(f"{p} ({fmt_modo(p)})" for p in problemas), "Ajuste permissões/dono desses diretórios antes de processar mensagens.")


def destino_existe(raiz, sistema, destino):
    if not isinstance(destino, str):
        return False, "destino ausente ou não textual"
    if destino.startswith("sistema__"):
        return True, "destino sistema"
    if destino.startswith("agente__"):
        nome = destino[8:] + ".json"
        local = sistema / "agentes" / nome
        bib = raiz / "core" / "biblioteca-agentes-fixos" / nome
        if local.is_file():
            return True, f"agentes/{nome}"
        if bib.is_file():
            return True, f"fallback core/biblioteca-agentes-fixos/{nome}"
        return False, f"agente não encontrado: {nome}"
    if destino.startswith("conector__"):
        nome = destino[10:] + ".json"
        local = sistema / "conectores" / nome
        bib = raiz / "core" / "biblioteca-conectores" / nome
        if local.is_file():
            return True, f"conectores/{nome}"
        if bib.is_file():
            return True, f"fallback core/biblioteca-conectores/{nome}"
        return False, f"conector não encontrado: {nome}"
    return False, "prefixo inválido; use agente__, conector__ ou sistema__"


def auditar_api(rel, raiz, sistema):
    fmt = sistema / "formatos-escuta" / "protocolo-mensagem-siscore.v1.json"
    if not fmt.is_file():
        rel.pend("sistema", "formato canônico de escuta ausente", str(fmt), "Importe formatos-escuta do siscore/biblioteca.")
    else:
        _, err = ler_json(fmt)
        if err:
            rel.pend("sistema", "formato canônico de escuta inválido", err)
        else:
            rel.ok("sistema", "formato canônico de escuta presente", str(fmt))

    cat = sistema / "web-api" / "catalogo-mensagens.json"
    if not cat.is_file():
        rel.pend("sistema", "catálogo web-api ausente", str(cat), "Crie/importe web-api/catalogo-mensagens.json.")
        return
    j, err = ler_json(cat)
    if err or not isinstance(j, dict):
        rel.pend("sistema", "catálogo web-api inválido", f"{cat}: {err}")
        return
    msgs = j.get("mensagens")
    if not isinstance(msgs, list) or not msgs:
        rel.pend("sistema", "catálogo web-api sem mensagens", str(cat), "Cadastre idmensagem -> destino.")
        return
    pend = 0
    for m in msgs:
        if not isinstance(m, dict):
            pend += 1
            rel.pend("sistema", "mensagem inválida no catálogo", "entrada não é objeto")
            continue
        idm = m.get("idmensagem") or m.get("id") or "<sem-id>"
        dest = m.get("destino") or m.get("destinoSisc")
        ok, det = destino_existe(raiz, sistema, dest)
        if not ok:
            pend += 1
            rel.pend("sistema", "destino inválido no catálogo", f"{idm} -> {dest}: {det}", "Ajuste web-api/catalogo-mensagens.json ou crie o agente/conector.")
    if pend == 0:
        rel.ok("sistema", "catálogo web-api coerente", f"{len(msgs)} mensagem(ns)")


def auditar_api_http(rel, raiz, sistema):
    api = sistema / "api.php"
    if api.is_file():
        rel.ok("sistema", "api.php presente", str(api))
    else:
        rel.aviso("sistema", "api.php ausente", str(api), "Necessário apenas se o sistema expõe API HTTP.")

    token_dir = sistema / "token-externo"
    token_fontes = sorted(token_dir.glob("*.txt")) if token_dir.is_dir() else []
    if token_fontes:
        rel.ok("sistema", "token de API HTTP configurado", "fonte canonica token-externo/*.txt")
    else:
        rel.aviso("sistema", "token de API HTTP não encontrado", "token-externo/<cliente>.txt", "Necessário para publicar via api.php HTTP; variáveis de ambiente e secretos/ não são fonte válida para tokens de cliente.")


def auditar_gitignore(rel, raiz):
    gi = raiz / ".gitignore"
    if not gi.is_file():
        rel.aviso("sisc", ".gitignore ausente", str(gi), "Crie .gitignore para artefatos operacionais.")
        return
    txt = gi.read_text(encoding="utf-8", errors="ignore")
    if "espaco/" not in txt and "espaco/**" not in txt:
        rel.aviso("sisc", ".gitignore não ignora espaco", str(gi), "Inclua artefatos operacionais do espaco/.")
    else:
        rel.ok("sisc", ".gitignore cobre artefatos operacionais", str(gi))


def no(nome, filhos=None, ra=None):
    n = {"nome": str(nome), "filhos": filhos or []}
    if ra:
        n["ra"] = str(ra)
    return n


def itens_para_arvore(itens, total_pendencias, sistema, somente_pend):
    agora = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    grupos = {"ERRO": [], "PENDENTE": [], "AVISO": [], "OK": []}
    for it in itens:
        grupos.setdefault(it.get("nivel", "OUTRO"), []).append(it)

    filhos = [
        no("Resumo do preflight start", [
            no(f"sistema: {sistema.name}"),
            no(f"base: {sistema}"),
            no(f"geradoEm: {agora}"),
            no(f"pendências bloqueantes: {total_pendencias}"),
            no(f"modo: {'somente pendências' if somente_pend else 'relatório completo'}"),
            no("regra: ERRO/PENDENTE bloqueiam o início do trabalho; AVISO deve ser avaliado conforme o tipo de operação; OK confirma item pronto."),
        ])
    ]

    labels = [
        ("ERRO", "Erros bloqueantes"),
        ("PENDENTE", "Pendências bloqueantes"),
        ("AVISO", "Avisos"),
        ("OK", "Itens OK"),
    ]
    for nivel, titulo in labels:
        arr = grupos.get(nivel, [])
        if somente_pend and nivel not in ("ERRO", "PENDENTE"):
            continue
        filhos_itens = []
        for it in sorted(arr, key=lambda x: (x.get("escopo", ""), x.get("item", ""))):
            sub = []
            if it.get("detalhe"):
                sub.append(no(f"detalhe: {it['detalhe']}"))
            if it.get("solucao"):
                sub.append(no(f"solução: {it['solucao']}"))
            filhos_itens.append(no(f"[{nivel}] {it.get('escopo', '')}: {it.get('item', '')}", sub))
        if not filhos_itens:
            filhos_itens.append(no(f"nenhum item em {titulo.lower()}"))
        filhos.append(no(f"{titulo} [{nivel.lower()}] — {len(arr)} item(ns)", filhos_itens))

    if total_pendencias:
        filhos.append(no("Próximo passo", [no("Resolva as pendências bloqueantes antes de iniciar trabalho operacional no sistema."), no("Execute pp --start novamente após corrigir para recalcular esta árvore.")]))
    else:
        filhos.append(no("Próximo passo", [no("Nenhuma pendência bloqueante encontrada."), no("Reveja os avisos conforme o tipo de trabalho e execute pp --start novamente sempre que o ambiente mudar.")]))

    return {"filhos": filhos}


def arquivo_eh_arvore_sa(path):
    if not path.exists():
        return False
    j, err = ler_json(path)
    return not err and isinstance(j, dict) and j.get("tipo") == "arvore-sa"


def sa_add(pp, sistema, arvore, nome, em=None, mm=None, confirmar=False):
    args = [str(pp), f"--sa={arvore}", f"--ag={nome}"]
    if em:
        args.append(f"--em={em}")
    if mm:
        args.append(f"--mm={mm}")
    proc = subprocess.run(args, cwd=str(sistema), input=("s\n" if confirmar else None), text=True,
                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
    if proc.returncode != 0:
        raise RuntimeError(proc.stderr.strip() or f"pp --sa falhou ao criar {nome}")


def materializar_nos_start(pp, sistema, arvore, filhos, pai=None):
    for i, item in enumerate(filhos, start=1):
        nome = item.get("nome", "")
        mm = item.get("mm")
        confirmar = pai is None and i == 1
        sa_add(pp, sistema, arvore, nome, em=pai, mm=mm, confirmar=confirmar)
        meu_id = f"{pai}.{i}" if pai else str(i)
        materializar_nos_start(pp, sistema, arvore, item.get("filhos", []), meu_id)


def escrever_arvore_start(raiz, sistema, itens, total_pendencias, somente_pend):
    arvores = sistema / "arvores"
    arvores.mkdir(mode=0o775, exist_ok=True)
    try:
        os.chmod(arvores, 0o775)
    except PermissionError:
        pass
    path = arvores / "pendencias-start.json"
    if path.exists() and not arquivo_eh_arvore_sa(path):
        raise RuntimeError(f"arquivo existente não é árvore SA; recusando sobrescrever: {path}")
    pp = raiz / "pp"
    if not pp.is_file() or not os.access(pp, os.X_OK):
        raise RuntimeError("pp não encontrado para materializar via --sa")
    path.unlink(missing_ok=True)
    arv = itens_para_arvore(itens, total_pendencias, sistema, somente_pend)
    materializar_nos_start(pp, sistema, "arvores/pendencias-start", arv.get("filhos", []))
    try:
        os.chmod(path, 0o664)
    except PermissionError:
        pass
    return path


def apresentar_arvore_via_sa(raiz, sistema):
    pp = raiz / "pp"
    if not pp.is_file() or not os.access(pp, os.X_OK):
        print(f"start: árvore gerada em {sistema / 'arvores' / 'pendencias-start.json'}")
        print("start: pp não encontrado para apresentação via --sa", file=sys.stderr)
        return 1
    proc = subprocess.run([str(pp), "--sa=arvores/pendencias-start"], cwd=str(sistema))
    return proc.returncode


def main():
    args = sys.argv[1:]
    saida_json = "--json" in args
    somente_pend = "--somente-pendencias" in args
    if any(a in ("--ajuda", "--help", "-h") for a in args):
        uso()
        return 0
    invalidos = [a for a in args if a not in ("--json", "--somente-pendencias")]
    if invalidos:
        print(f"start: argumento inválido: {invalidos[0]}", file=sys.stderr)
        return 1

    rel = Relatorio()
    raiz = raiz_sisc_por_executavel()
    if not raiz or not (raiz / "core" / "comandos").is_dir():
        rel.erro("ambiente", "executável fora de uma instalação SISC", str(sys.argv[0]))
        sistema = None
    else:
        sistema = resolver_sistema_atual(raiz, rel)

    if sistema:
        rel.ok("ambiente", "sistema atual detectado", f"{sistema.name} em {sistema}")
        auditar_gitignore(rel, raiz)
        auditar_bibliotecas(rel, raiz)
        auditar_permissionamento_secretos(rel, "sistema", sistema / "secretos")
        contas_sistema = auditar_contas(rel, "sistema", sistema / "contas-llm", sistema / "secretos", exigir_llm=True)
        auditar_mapa(rel, raiz, sistema, contas_sistema)
        auditar_arvores(rel, sistema)
        auditar_escuta(rel, sistema)
        auditar_espaco(rel, sistema)
        auditar_api(rel, raiz, sistema)
        auditar_api_http(rel, raiz, sistema)

    itens = rel.itens
    if somente_pend:
        itens = [i for i in itens if i["nivel"] in ("PENDENTE", "ERRO")]

    arvore_path = None
    if sistema:
        try:
            arvore_path = escrever_arvore_start(raiz, sistema, itens, len(rel.pendencias()), somente_pend)
        except Exception as e:
            rel.pend("sistema", "falha ao gravar árvore pendencias-start.json", str(e), "Verifique permissão de escrita em arvores/.")

    if saida_json:
        print(json.dumps({
            "ok": len(rel.pendencias()) == 0,
            "pendencias": len(rel.pendencias()),
            "arvore": str(arvore_path) if arvore_path else None,
            "itens": itens,
        }, ensure_ascii=False, indent=2))
    elif sistema and arvore_path:
        apresentar_arvore_via_sa(raiz, sistema)
    else:
        print("SISC start/preflight")
        print(f"Pendências bloqueantes: {len(rel.pendencias())}")
        print("")
        ordem = {"ERRO": 0, "PENDENTE": 1, "AVISO": 2, "OK": 3}
        for it in sorted(itens, key=lambda x: (ordem.get(x["nivel"], 9), x["escopo"], x["item"])):
            print(f"[{it['nivel']}] {it['escopo']}: {it['item']}")
            if it.get("detalhe"):
                print(f"  - {it['detalhe']}")
            if it.get("solucao"):
                print(f"  > {it['solucao']}")
    return 1 if rel.pendencias() else 0

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