"""
Project Together — powered by AI: ENGENHARIA REVERSA — do quadro de volta ao Termo de Abertura.

PARA QUE SERVE (o pedido do Eduardo, 06/08/2026)
  O curso corre para a frente: o Termo de Abertura (Passo 1) gera a EAP (Passo 2), que
  ganha datas e dinheiro (Passo 4). Mas um projeto anda para os DOIS lados — voce ajusta
  o cronograma, descobre no meio do caminho uma entrega que faltava, o orcamento fecha
  num numero diferente do chute inicial. Quando isso acontece o Termo de Abertura fica
  VELHO: ele continua dizendo o que voce prometeu ANTES de saber o que sabe agora.

  Esta opcao anda no sentido contrario. Ela LE as listas de trabalho do seu quadro e
  RECONSTROI o que cada cartao do Termo de Abertura diria hoje. Mostra lado a lado o que
  esta no cartao e o que o quadro implica — e VOCE decide, cartao por cartao, se atualiza.

  Foi assim que ela nasceu: os cartoes 2.1 (entregas) e 2.2 (requisitos) se perderam de
  um quadro que ja tinha EAP, cronograma e orcamento prontos. Re-rodar o Passo 1 do zero
  teria pedido entregas NOVAS a IA — e entregas novas nao casam com a EAP que ja existe.
  Mas a EAP nao precisava de IA nenhuma: ela NASCEU daquelas entregas. Bastava ler.

A REGRA — SO' SE DERIVA O QUE TEM FONTE NO QUADRO
  Cada cartao do termo que tem uma lista de trabalho abaixo dele pode ser re-derivado
  dela. Os que nao tem, esta opcao NAO TOCA — nunca:

     cartao do termo                          fonte no quadro
     2.1  O QUE — entregas                    EAP → Cronograma (o nivel das entregas)
     2.2  O QUE — requisitos                  EAP → Cronograma (os pacotes + aceitacao)
     3    QUEM — partes interessadas          Partes Interessadas
     4.2  COMO — restricoes                   Riscos e Premissas (tipo Restricao)
     4.3  COMO — premissas                    Riscos e Premissas (tipo Premissa)
     4.4  COMO — riscos iniciais              Riscos e Premissas (os de maior prioridade)
     4.5  COMO — escopo inicial               EAP → Cronograma
     5    QUANDO — linha do tempo             as DATAS do cronograma
     6    QUANTO — custos                     Orcamento

     1.1  a justificativa    NUNCA — e a sua voz; nada no quadro a produz
     1.2  o OKR              NUNCA — e a promessa; o trabalho serve a ela, nao o contrario
     4.1  a abordagem        NUNCA — e uma decisao sua (script 01a)
     4.6  o ONDE             NUNCA — nenhuma lista do quadro registra o onde

A ALTITUDE DO TERMO DE ABERTURA E' RESPEITADA (elaboracao progressiva)
  Engenharia reversa nao e' despejar a EAP dentro do Passo 1. O termo fala de ENTREGAS
  (nao de pacotes de trabalho), de MARCOS (nao de cada data), dos riscos de ALTO NIVEL
  (nao do registro inteiro). O detalhe continua morando no passo que o produziu.

O QUE VOCE JA ESCREVEU GANHA DO QUE EU DERIVO
  Onde o cartao ja tem resposta sua para aquele item (o requisito de uma entrega, o KR a
  que ela serve), ela e' PRESERVADA. A derivacao so' preenche o que esta faltando.

SEGURANCA: ele SEMPRE mostra o antes e o depois e pergunta, cartao por cartao. Nada e'
gravado sem voce confirmar. Rodar de novo sem ter mudado nada nao faz nada.

Uso:
    py reverse_engineering.py               # mostra tudo e pergunta cartao por cartao
    py reverse_engineering.py --dry-run     # so' compara (nunca grava)
    py reverse_engineering.py --yes         # aplica tudo sem perguntar
    py reverse_engineering.py 2.1 2.2       # so' estes cartoes
"""
import sys

import entradas
import quadro
import vocabulario
from env import cfg, require
from idioma import T, Tf, escolher
from pa_client import get_card, limpar_cache, list_cards, update_card
from runner import done, header, ok, step, warn

# Quantos riscos do registro sobem para o cartao 4.4. O termo de abertura pede "os riscos
# de alto nivel que voce ja enxerga na largada — o suficiente para alguem decidir se
# autoriza o projeto"; o registro completo (Passo 3) e' outro documento. Sobem os de maior
# PRIORIDADE (probabilidade x impacto), que sao exatamente os que pesam numa autorizacao.
MAX_RISCOS_NO_TERMO = 6

# A linha que separa, no cartao 6, o numero de HOJE (vindo do orcamento) da estimativa
# PRELIMINAR que voce escreveu no Passo 1. Guardar as duas nao e' apego: e' a elaboracao
# progressiva a vista — a ordem de grandeza de quem ainda nao tinha escopo, e o numero de
# quem ja tem. E' tambem o que torna esta opcao idempotente: ao rodar de novo, tudo o que
# esta ACIMA da marca e' substituido e o que esta abaixo fica intacto.
#
# A marca e' reconhecida nos DOIS idiomas (um quadro montado em portugues pode ser aberto
# pelo pacote em ingles, e o contrario): procurar so' a frase em portugues faria a segunda
# rodada empilhar uma estimativa sobre a outra, para sempre.
_MARCA_ORIGINAL_PT = "── Estimativa preliminar do Termo de Abertura (Passo 1) ──"
_MARCA_ORIGINAL_EN = "── Preliminary estimate from the project charter (Step 1) ──"
_MARCAS_ORIGINAL = (_MARCA_ORIGINAL_PT, _MARCA_ORIGINAL_EN)
MARCA_ORIGINAL = escolher(_MARCA_ORIGINAL_PT, _MARCA_ORIGINAL_EN)

# Os cabecalhos de grupo da lista Partes, nos dois idiomas — o cartao nasce por T() no
# Passo 1 ("EQUIPE — quem faz" / "TEAM — who does the work"), entao comparar com a frase
# portuguesa escrita a mao devolveria False num quadro em ingles e o grupo de cada parte
# se perderia. Mesma armadilha (e mesma correcao) do quadro.e_grupo_equipe.
# A comparacao e' case-SENSITIVE: os cabecalhos sao MAIUSCULOS, entao uma parte
# interessada de nome misto ("Equipe de suporte") nunca e' confundida com o cabecalho.
_GRUPOS_PARTES = (("cliente", ("CLIENTES", "CLIENTS")),
                  ("equipe", ("EQUIPE", "TEAM")),
                  ("demais", ("DEMAIS", "OTHERS")))


def _chave(nome):
    """O nome de uma entrega como CHAVE de comparacao (sem acento, sem caixa, sem
    espaco duplo). E' por ela que a entrega lida da EAP reencontra a resposta que voce
    ja tinha dado no cartao 2.1/2.2 — o aluno escreve "Pagina de inscricao" onde a IA
    escreveu "Página de Inscrição" e continua sendo a mesma entrega."""
    return " ".join(entradas._sem_acento(nome or "").split())


# ─────────────────────────────────────────────────────────────────────────────
# LER O QUADRO — uma foto so', usada por todas as derivacoes
# ─────────────────────────────────────────────────────────────────────────────
def _entregas_da_eap(nos, tem_fases):
    """As ENTREGAS do termo de abertura, lidas da EAP — cada uma com os seus pacotes.

    Qual nivel da EAP e' "a entrega"? O nivel 2 quando a EAP tem fases (fase > entrega >
    pacote) e o nivel 1 quando nao tem. E' a mesma regra que o Passo 2 usou para montar a
    arvore, na direcao contraria — por isso ela devolve exatamente as entregas de onde a
    EAP nasceu.

    Uma raiz que nao tem filho nenhum tambem conta como entrega: num quadro AGIL a lista
    de escopo e' o Product Backlog, plano por natureza (cada item e' raiz e folha).
    """
    eap = [n for n in nos if n["tipo"] != "tarefa"]
    com_filho = {n["codigo"].rsplit(".", 1)[0] for n in eap if "." in n["codigo"]}
    nivel = 2 if tem_fases else 1
    out = []
    for n in eap:
        folha = n["codigo"] not in com_filho
        if n["nivel"] == nivel or (n["nivel"] < nivel and folha):
            out.append({"no": n, "pacotes": []})
        elif n["nivel"] > nivel and out:
            out[-1]["pacotes"].append(n)
    return out


def _partes_do_quadro(ids):
    """As partes interessadas da lista Partes, COM o grupo de cada uma.

    O grupo mora na hierarquia (cada parte e' subtarefa do cartao do seu grupo) e o GET de
    LISTA nao devolve `cartao_pai_id` — por isso lemos cada cartao. Mesma tecnica (e mesmo
    motivo) do quadro.texto_da_equipe.
    """
    from concurrent.futures import ThreadPoolExecutor
    lid = ids.get("PARTES")
    if not lid:
        return []
    cards = [c for c in list_cards(lid)
             if (c.get("titulo") or "").strip() and not quadro.e_guia(c.get("titulo"))]
    grupo_do_id = {}
    for c in cards:
        titulo = (c.get("titulo") or "").strip()
        for chave, prefixos in _GRUPOS_PARTES:
            if titulo.startswith(prefixos):
                grupo_do_id[str(c["id"])] = chave
    if not grupo_do_id:
        return []
    with ThreadPoolExecutor(max_workers=6) as ex:
        detalhes = list(ex.map(lambda c: (c, get_card(c["id"])), cards))
    out = []
    for c, full in detalhes:
        cid = str(c["id"])
        if cid in grupo_do_id:
            continue                      # e' o cabecalho de um grupo, nao uma parte
        pai = str(full.get("cartao_pai_id"))
        txt = entradas.texto_do_cartao(full.get("descricao")) or ""
        out.append({
            "nome": (c.get("titulo") or "").strip(),
            "grupo": grupo_do_id.get(pai, "demais"),
            "papel": quadro._val_desc(txt, r"papel|role"),
            "espera": quadro._val_desc(txt, r"o que espera do projeto|espera|expects|"
                                            r"what they expect from the project"),
            "poder": quadro._val_desc(txt, r"poder|power"),
            "interesse": quadro._val_desc(txt, r"interesse|interest"),
        })
    return out


# ─────────────────────────────────────────────────────────────────────────────
# ACHAR AS LISTAS SEM MEXER NO QUADRO
#
# Todo passo do curso comeca por `quadro.garantir_listas()`, e faz sentido: um passo
# CONSTROI o quadro. Esta opcao nao — ela LE o quadro para propor um texto, e quem le
# nao pode reformar a casa.
#
# E nao e' teorico. O `garantir_listas` acha cada lista PELO NOME no idioma corrente;
# nao achando, CRIA. Num quadro cujas listas estao com o nome em OUTRA lingua (o proprio
# quadro.py avisa: "o que NAO pode e' um aluno trocar IDIOMA no meio do curso"), rodar
# esta opcao criaria 8 listas VAZIAS e ainda reapontaria os LIST_* do .env para elas — o
# projeto inteiro sumiria da vista do curso, por causa de uma conferencia. Aconteceu num
# quadro real em 06/08/2026.
#
# Entao aqui a fonte e' o .env (os LIST_* que os passos ja gravaram), e o nome so' serve
# de reserva — nas DUAS linguas, porque o que importa e' encontrar a lista do aluno, nao
# ter razao sobre como ela se chama. Nada e' criado, nada e' renomeado, nada e' gravado.
# ─────────────────────────────────────────────────────────────────────────────
# Os nomes que uma lista nossa pode ter (os dois idiomas, e as versoes anteriores) moram
# no quadro.py desde 07/08/2026 — UMA definicao para quem monta o quadro, quem so' o le e
# quem o confere. Enquanto eram duas listas, a de la' aprendeu a reconhecer um quadro na
# outra lingua e a daqui nao: e' assim que o mesmo bug volta pela porta do lado.
_nomes_possiveis = quadro.nomes_conhecidos


def resolver_listas(c, abordagem):
    """{chave: id} das listas do quadro — SEM criar, renomear nem gravar nada.

    Devolve tambem os nomes das listas que nao foram encontradas, para o script poder
    dizer o que falta em vez de silenciosamente derivar de uma lista vazia.
    """
    from pa_client import list_lists
    listas = list_lists()
    por_id = {str(l["id"]): (l.get("nome") or "").strip() for l in listas}
    ids, ausentes = {}, []
    for chave, _titulo in quadro.listas_do_quadro(abordagem):
        do_env = str(c.get(f"LIST_{chave}") or "").strip()
        if do_env and do_env in por_id:
            ids[chave] = int(do_env)
            continue
        achado = next((l for l in listas
                       if (l.get("nome") or "").strip() in _nomes_possiveis(chave, abordagem)),
                      None)
        if achado:
            ids[chave] = int(achado["id"])
        else:
            ausentes.append(_titulo)
    return ids, ausentes


def ler_o_quadro(ids, abordagem):
    """A foto do quadro de que todas as derivacoes comem — lida UMA vez."""
    nos, tem_fases = quadro.arvore_da_eap(ids)
    return {
        "ids": ids,
        "abordagem": abordagem,
        "nos": nos,
        "tem_fases": tem_fases,
        "entregas": _entregas_da_eap(nos, tem_fases),
        "pontos": quadro.riscos_do_quadro(ids),
        "orcamento": quadro.orcamento_do_quadro(ids),
        "partes": _partes_do_quadro(ids),
        "respostas": quadro.respostas_do_termo(ids),
        "kr_por_entrega": {},        # preenchido por perguntar_os_krs()
    }


# ─────────────────────────────────────────────────────────────────────────────
# O KR DE CADA ENTREGA — a unica coisa que a EAP NAO sabe
#
# A EAP guarda o QUE se faz; ela nunca guardou a QUAL Key Result cada entrega serve (esse
# elo mora so' no cartao 2.1). Entao a engenharia reversa preserva o que o 2.1 ja diz e,
# para o que sobrar, PERGUNTA a voce — com os seus KRs na tela, numerados. Nao ha IA aqui
# de proposito: voce sabe a qual promessa a sua entrega serve, e uma pergunta de um
# caractere e' mais rapida (e mais certa) do que uma ida e volta a um modelo.
# ─────────────────────────────────────────────────────────────────────────────
def _krs_do_quadro(ctx):
    """[(rotulo, texto)] dos Key Results do cartao 1.2 — ex.: ("KR 1.1", "...")."""
    okr = entradas.okr(ctx["respostas"].get("1.2", ""))
    out = []
    for i, o in enumerate(entradas.objetivos_do_okr(okr), 1):
        for j, kr in enumerate(o.get("key_results") or [], 1):
            out.append((f"KR {i}.{j}", (kr.get("kr") or "").strip()))
    return out


def _krs_ja_ditos(ctx):
    """{chave da entrega: KR} — o que o cartao 2.1 ja diz hoje. O que voce escreveu
    sempre ganha da derivacao: aqui a engenharia reversa nunca reescreve um elo seu."""
    return {_chave(e["nome"]): e["kr"]
            for e in entradas.entregas(ctx["respostas"].get("2.1", "")) if e.get("kr")}


def perguntar_os_krs(ctx, interativo=True):
    """Descobre, para cada entrega da EAP, a qual Key Result ela serve.

    Preserva o que o cartao 2.1 ja diz; pergunta o resto (Enter = deixar em branco, e a
    conferencia do Passo 5 vai cobrar depois). Sem terminal interativo, deixa em branco —
    o cartao sai igual, so' que sem o elo, e uma proxima rodada pergunta de novo.
    """
    ja = _krs_ja_ditos(ctx)
    mapa = {}
    faltam = []
    for e in ctx["entregas"]:
        k = _chave(e["no"]["titulo"])
        if ja.get(k):
            mapa[k] = ja[k]
        else:
            faltam.append(e)
    ctx["kr_por_entrega"] = mapa
    if not faltam:
        return 0
    krs = _krs_do_quadro(ctx)
    if not krs:
        warn(T("o cartão 1.2 (OKR) está vazio — não tenho a que Key Result ligar as suas "
               "entregas. Responda o 1.2 e rode a engenharia reversa de novo."))
        return 0
    if not interativo or not sys.stdin.isatty():
        warn(Tf("{0} entrega(s) da sua EAP ainda não dizem a que Key Result servem — rode "
                "sem --yes para eu perguntar uma por uma.", len(faltam)))
        return 0
    print()
    print(Tf("  {0} entrega(s) da sua EAP ainda não dizem a que Key Result servem. Esse elo "
             "não mora na EAP — mora no cartão 2.1, e é ele que se perdeu.", len(faltam)))
    print(T("  Me diga qual é, e eu escrevo. (Enter pula; a conferência do Passo 5 cobra "
            "depois.)"))
    print()
    print(T("  OS SEUS KEY RESULTS (cartão 1.2):"))
    for n, (rot, texto) in enumerate(krs, 1):
        print(f"   {n:>2}. {rot}: {texto[:76]}")
    print()
    respondidas = 0
    for e in faltam:
        titulo = e["no"]["titulo"]
        try:
            resp = input(Tf('   "{0}" serve a qual KR? [nº, vários com vírgula, Enter = '
                            'pular] ', titulo[:52])).strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        escolhidos = []
        for parte in resp.replace(";", ",").split(","):
            parte = parte.strip()
            if parte.isdigit() and 1 <= int(parte) <= len(krs):
                escolhidos.append(krs[int(parte) - 1][0])
        if escolhidos:
            ctx["kr_por_entrega"][_chave(titulo)] = ", ".join(escolhidos)
            respondidas += 1
    return respondidas


# ─────────────────────────────────────────────────────────────────────────────
# AS DERIVACOES — uma por cartao do termo. Cada uma devolve o TEXTO que o cartao
# teria hoje, ou None quando a lista-fonte ainda nao existe (nada a comparar).
# ─────────────────────────────────────────────────────────────────────────────
def derivar_entregas(ctx):
    """2.1 — as entregas, no nivel em que a EAP as guarda."""
    if not ctx["entregas"]:
        return None
    itens = [{"nome": e["no"]["titulo"],
              "kr": ctx["kr_por_entrega"].get(_chave(e["no"]["titulo"]), "")}
             for e in ctx["entregas"]]
    return quadro.texto_das_entregas(itens)


def derivar_requisitos(ctx):
    """2.2 — o requisito e o criterio de aceitacao de cada entrega.

    De onde eles saem quando o cartao 2.2 esta vazio: o REQUISITO de uma entrega e' o
    conjunto de pacotes de trabalho que a compoem (e' o que ela precisa ter para existir —
    foi assim que o Passo 2 a decompos), e o CRITERIO DE ACEITACAO e' a soma dos criterios
    desses pacotes (a entrega so' e' aceita quando todos os pedacos dela sao). Onde voce ja
    escreveu um requisito, ele fica: derivacao nao reescreve resposta sua.
    """
    if not ctx["entregas"]:
        return None
    atuais = {_chave(e["nome"]): e
              for e in entradas.entregas(ctx["respostas"].get("2.2", ""))}
    itens = []
    for e in ctx["entregas"]:
        nome = e["no"]["titulo"]
        ja = atuais.get(_chave(nome), {})
        pacotes = [p["titulo"] for p in e["pacotes"]]
        aceites = [p["aceitacao"] for p in e["pacotes"] if p.get("aceitacao")]
        if not e["pacotes"] and e["no"].get("aceitacao"):
            aceites = [e["no"]["aceitacao"]]
        itens.append({"nome": nome,
                      "requisito": ja.get("requisito") or " · ".join(pacotes),
                      "aceitacao": ja.get("aceitacao") or " · ".join(aceites)})
    return quadro.texto_dos_requisitos(itens)


def derivar_partes(ctx):
    """3 — as partes interessadas, com grupo, papel, expectativa, poder e interesse."""
    if not ctx["partes"]:
        return None
    linhas = []
    for p in ctx["partes"]:
        linhas.append(f"• {p['nome']} [{p['grupo']}]")
        if p["papel"] or p["espera"]:
            linhas.append(Tf("    papel: {0}", p["papel"]))
            linhas.append(Tf("    espera: {0}", p["espera"]))
        if p["poder"] or p["interesse"]:
            linhas.append(Tf("    poder: {0} · interesse: {1}", p["poder"], p["interesse"]))
    return "\n".join(linhas)


def _pontos_do_tipo(ctx, *tipos):
    """Os pontos de atencao de um ou mais TIPOS. O tipo vem de quadro._tipo_do_ponto, que
    devolve sempre a forma canonica em portugues ('Restrição', 'Premissa', 'Ameaça'...)
    nos dois idiomas — por isso comparar com ela e' seguro em qualquer pacote."""
    return [p for p in ctx["pontos"] if p["tipo"] in tipos]


def derivar_restricoes(ctx):
    """4.2 — as restricoes, lidas dos pontos de atencao do tipo Restricao."""
    itens = _pontos_do_tipo(ctx, "Restrição")
    return "\n".join(f"• {p['risco']}" for p in itens) if itens else None


def derivar_premissas(ctx):
    """4.3 — as premissas, lidas dos pontos de atencao do tipo Premissa."""
    itens = _pontos_do_tipo(ctx, "Premissa")
    if not itens:
        return None
    return ("\n".join(f"• {p['risco']}" for p in itens) + "\n\n"
            + T("Premissa que cai vira risco — revise esta lista a cada passo."))


def derivar_riscos_iniciais(ctx):
    """4.4 — os riscos de MAIOR PRIORIDADE do registro (nao o registro inteiro).

    O termo de abertura pede o que basta para alguem decidir se autoriza o projeto; o
    registro completo e' o produto do Passo 3 e continua morando la'. Sobem os de maior
    prioridade (probabilidade x impacto) — e um risco ainda sem avaliacao entra por
    ultimo, nunca some.
    """
    itens = _pontos_do_tipo(ctx, *vocabulario.TIPOS_AVALIAVEIS)
    if not itens:
        return None

    def _peso(p):
        # prioridade_do_risco devolve (numero 1..25, nivel) — e (None, None) para um risco
        # que ainda nao foi avaliado. Ele nao some da lista: cai para o fim dela.
        num, _nivel = quadro.prioridade_do_risco(p["prob_ou_urgencia"], p["impacto"])
        return num or 0

    itens = sorted(itens, key=_peso, reverse=True)[:MAX_RISCOS_NO_TERMO]
    linhas = []
    for p in itens:
        linhas.append(f"• {p['risco']}")
        linhas.append(Tf("    probabilidade: {0} · impacto: {1}",
                         p["prob_ou_urgencia"], p["impacto"]))
        linhas.append(Tf("    resposta: {0}", p["resposta"]))
    return "\n".join(linhas)


def derivar_escopo_inicial(ctx):
    """4.5 — a primeira decomposicao do escopo: as fases e as entregas da EAP."""
    if not ctx["entregas"]:
        return None
    linhas = []
    if ctx["tem_fases"]:
        for n in ctx["nos"]:
            if n["tipo"] == "fase":
                linhas.append(f"• {n['titulo']}")
        entregas_por_fase = {}
        for e in ctx["entregas"]:
            pai = e["no"]["codigo"].rsplit(".", 1)[0]
            entregas_por_fase.setdefault(pai, []).append(e["no"]["titulo"])
        linhas = []
        for n in ctx["nos"]:
            if n["tipo"] != "fase":
                continue
            linhas.append(f"• {n['titulo']}")
            linhas += [f"    - {t}" for t in entregas_por_fase.get(n["codigo"], [])]
    else:
        linhas = [f"• {e['no']['titulo']}" for e in ctx["entregas"]]
    rodape = escolher(
        "No passo 2 cada entrega se abre em pacotes de trabalho (EAP).",
        "In step 2 each deliverable opens into work packages (WBS).")
    if ctx["abordagem"] == "AGIL":
        rodape = escolher(
            "No passo 2 cada entrega vira histórias priorizadas no Product Backlog.",
            "In step 2 each deliverable becomes prioritized stories in the Product Backlog.")
    return "\n".join(linhas) + "\n\n" + rodape


def derivar_quando(ctx):
    """5 — os marcos, agora com as DATAS REAIS do cronograma.

    O marco de uma abordagem preditiva e' a entrega concluida: aqui ele e' o TERMINO de
    cada elemento de primeiro nivel da EAP (as fases, quando ha fases; as entregas, quando
    nao ha). E' o termo de abertura na altitude dele — o cronograma tarefa a tarefa
    continua no Passo 4.

    A data de INICIO do projeto nao e' derivada: ela e' sua (o Passo 1 pergunta e grava no
    .env). Se o cronograma comecar noutro dia, eu aviso — mas quem decide a ancora e' voce.
    """
    raizes = [n for n in ctx["nos"] if n["nivel"] == 1 and n["tipo"] != "tarefa"]
    if not raizes:
        return None
    inicio = (cfg().get("START_DATE") or "").strip()[:10]
    linhas = []
    if inicio:
        linhas += [Tf("Data de início do projeto: {0}", inicio), ""]
    for n in raizes:
        linhas.append(f"• {n['titulo']} — {n['fim'] or '?'}")
    return "\n".join(linhas)


def derivar_quanto(ctx):
    """6 — o custo, agora com o numero do orcamento (Passo 4).

    O cartao 6 nasce como ORDEM DE GRANDEZA: um chute honesto, feito quando ainda nao
    havia escopo. Aqui ele ganha o numero de hoje — e a estimativa original continua
    logo abaixo, sob a marca. Ver o chute e o numero lado a lado e' a elaboracao
    progressiva ficando visivel, e nao custa nada guardar.
    """
    itens = ctx["orcamento"]
    if not itens:
        return None
    from artefatos import _reais
    total = sum(i["valor_num"] for i in itens)
    reservas = sum(i["valor_num"] for i in itens
                   if "reserv" in entradas._sem_acento(i.get("tipo", ""))
                   or "conting" in entradas._sem_acento(i.get("nome", "")))
    cabeca = Tf("Orçamento do projeto (Passo 4): {0} — base {1} + reserva {2}, em {3} "
                "item(ns) de custo.",
                _reais(total), _reais(total - reservas), _reais(reservas), len(itens))
    # O que o aluno escreveu no Passo 1 fica preservado abaixo da marca — e, numa segunda
    # rodada, e' de la' que ele e' relido (nunca da cabeca que esta opcao acabou de gravar).
    atual = ctx["respostas"].get("6", "")
    original = atual
    for marca in _MARCAS_ORIGINAL:
        if marca in atual:
            original = atual.split(marca, 1)[1].strip()
            break
    if not original:
        return cabeca
    return f"{cabeca}\n\n{MARCA_ORIGINAL}\n{original}"


# (rotulo do cartao, o que ele e', a lista de onde ele vem, a funcao que o deriva)
DERIVACOES = [
    ("2.1", T("Produtos e entregas (O QUÊ)"), "ESCOPO", derivar_entregas),
    ("2.2", T("Requisitos"), "ESCOPO", derivar_requisitos),
    ("3", T("Partes interessadas (QUEM)"), "PARTES", derivar_partes),
    ("4.2", T("Restrições"), "RISCOS", derivar_restricoes),
    ("4.3", T("Premissas"), "RISCOS", derivar_premissas),
    ("4.4", T("Riscos iniciais"), "RISCOS", derivar_riscos_iniciais),
    ("4.5", T("Escopo inicial"), "ESCOPO", derivar_escopo_inicial),
    ("5", T("Linha do tempo (QUANDO)"), "ESCOPO", derivar_quando),
    ("6", T("Custos (QUANTO)"), "ORCAMENTO", derivar_quanto),
]

# Os cartoes que esta opcao NUNCA toca, e por que. Aparece na tela: quem roda uma
# ferramenta que reescreve o termo de abertura merece saber onde ela nao entra.
INTOCAVEIS = [
    ("1.1", T("a justificativa — é a sua voz; nada no quadro a produz")),
    ("1.2", T("o OKR — é a sua promessa; o trabalho serve a ela, não o contrário")),
    ("4.1", T("a abordagem — é uma decisão sua (script 01a)")),
    ("4.6", T("o ONDE — nenhuma lista do quadro o registra")),
]


# ─────────────────────────────────────────────────────────────────────────────
# COMPARAR E APLICAR
# ─────────────────────────────────────────────────────────────────────────────
def _comparavel(texto):
    """O texto de um cartao reduzido ao que ele SIGNIFICA — para comparar, nunca para
    gravar.

    O editor rico do ProjectAdm devolve o mesmo conteudo com outro espacamento: cada
    linha vira um <p>, entao um cartao que o script gravou como quatro linhas seguidas
    volta com uma linha em branco entre cada uma, e o recuo dos campos se perde. Comparar
    o texto cru diria "mudou" em todo cartao, sempre — e a opcao pediria confirmacao para
    reescrever exatamente o que ja esta la'.

    Duas versoes que so' diferem em espaco em branco sao a MESMA resposta para todos os
    parsers do curso (entradas.py ignora recuo e linha vazia). Entao a comparacao ignora
    tambem: linha vazia sai, espaco repetido vira um so'.
    """
    linhas = [" ".join(l.split()) for l in (texto or "").splitlines()]
    return "\n".join(l for l in linhas if l)


def _mostrar(rotulo, nome, fonte, atual, novo):
    """O antes e o depois de um cartao, do jeito que o aluno decide olhando."""
    print()
    print("─" * 62)
    print(Tf("  CARTÃO {0} — {1}", rotulo, nome))
    print(Tf("  fonte: a lista {0} do seu quadro", quadro.rotulo_da_lista(fonte)))
    print("─" * 62)
    print(T("  HOJE no cartão:") if atual else T("  HOJE no cartão: (vazio — só o "
                                                 "texto-guia do curso)"))
    for l in (atual or "").splitlines()[:10]:
        print(f"    | {l[:88]}")
    if len((atual or "").splitlines()) > 10:
        print(Tf("    | ... (+{0} linha(s))", len(atual.splitlines()) - 10))
    print()
    print(T("  LIDO DO QUADRO (o que este cartão diria hoje):"))
    for l in (novo or "").splitlines()[:16]:
        print(f"    > {l[:88]}")
    if len((novo or "").splitlines()) > 16:
        print(Tf("    > ... (+{0} linha(s))", len(novo.splitlines()) - 16))


def _confirmar(rotulo):
    try:
        r = input(Tf("\n  Atualizo o cartão {0} com isto? [s/N] ", rotulo)).strip().lower()
    except (EOFError, KeyboardInterrupt):
        return False
    return r in ("s", "sim", "y", "yes")


def _cartao_do_rotulo(ids, rotulo):
    """O cartao da lista INPUTS cujo titulo comeca com este rotulo ("2.1", "5")."""
    pref = rotulo if "." in rotulo else rotulo + "."
    for c in list_cards(ids["INPUTS"]):
        if (c.get("titulo") or "").startswith(pref):
            return c
    return None


def _avisos_do_quadro(ctx):
    """O que a engenharia reversa VIU e voce precisa saber — mas que ela nao decide."""
    avisos = []
    inicio_env = (cfg().get("START_DATE") or "").strip()[:10]
    inicios = sorted(n["ini"] for n in ctx["nos"] if n.get("ini"))
    if inicio_env and inicios and inicios[0] != inicio_env:
        avisos.append(Tf("o seu cronograma começa em {0}, mas a data de início gravada no "
                         "Passo 1 é {1}. Quem decide a âncora é você — rode o Passo 1 para "
                         "mudá-la.", inicios[0], inicio_env))
    sem_valor = [i for i in ctx["orcamento"] if not i["valor_num"]]
    if ctx["orcamento"] and sem_valor:
        avisos.append(Tf("{0} de {1} item(ns) do seu orçamento estão com valor 0 ou "
                         "ilegível — o total que eu derivo do cartão 6 ficará menor do que "
                         "o projeto custa.", len(sem_valor), len(ctx["orcamento"])))
    sem_aceite = [e for e in ctx["entregas"]
                  if not any(p.get("aceitacao") for p in e["pacotes"])
                  and not e["no"].get("aceitacao")]
    if sem_aceite:
        avisos.append(Tf("{0} entrega(s) da EAP não têm critério de aceitação em nenhum "
                         "pacote — o cartão 2.2 vai sair sem eles.", len(sem_aceite)))
    return avisos


def main(argv=None):
    argv = list(sys.argv[1:] if argv is None else argv)
    dry = "--dry-run" in argv
    tudo = "--yes" in argv
    pedidos = [a for a in argv if not a.startswith("-")]

    limpar_cache()
    c = cfg()
    require(c, "PROJECTADM_TOKEN", "BOARD_ID")
    abordagem = quadro.abordagem_do_env() or quadro.abordagem_do_quadro(c)
    if not abordagem:
        raise SystemExit(T("Escolha a abordagem primeiro (rode 01a_escolher_abordagem.py)."))

    header(T("Engenharia reversa — o quadro de volta ao Termo de Abertura")
           + (T("   [SIMULAÇÃO]") if dry else ""))
    print(T("  O seu quadro andou: a EAP cresceu, o cronograma mudou, o orçamento fechou."))
    print(T("  Eu leio as listas de trabalho e reconstruo o que cada cartão do Termo de"))
    print(T("  Abertura diria HOJE. Você compara e decide, cartão por cartão."))
    print()
    print(T("  Eu NUNCA toco nestes — eles não se derivam de trabalho nenhum:"))
    for rot, porque in INTOCAVEIS:
        print(f"    · {rot:<4} {porque}")

    ids, ausentes = resolver_listas(c, abordagem)
    if ausentes:
        # Nao crio a lista que falta — criar aqui e' que produziria o quadro fantasma
        # descrito em resolver_listas(). Digo o que falta e devolvo a decisao ao aluno.
        warn(Tf("não achei no seu quadro: {0}. Rode o Passo 1 (ele monta as listas que "
                "faltam) e volte aqui.", ", ".join(ausentes)))
    if "ESCOPO" not in ids or "INPUTS" not in ids:
        raise SystemExit(T("Sem a lista do escopo e a lista INPUTS eu não tenho de onde ler "
                           "nem onde escrever — rode o Passo 1 e o Passo 2 primeiro."))
    step(T("Lendo o seu quadro..."))
    ctx = ler_o_quadro(ids, abordagem)
    if not ctx["entregas"] and not ctx["pontos"] and not ctx["orcamento"]:
        raise SystemExit(T("Ainda não há trabalho no seu quadro para derivar nada — rode o "
                           "Passo 2 (escopo) primeiro."))
    ok(Tf("{0} entrega(s) na EAP · {1} ponto(s) de atenção · {2} item(ns) de orçamento · "
          "{3} parte(s) interessada(s).", len(ctx["entregas"]), len(ctx["pontos"]),
          len(ctx["orcamento"]), len(ctx["partes"])))

    for aviso in _avisos_do_quadro(ctx):
        warn(aviso)

    alvos = [d for d in DERIVACOES if not pedidos or d[0] in pedidos]
    if pedidos and not alvos:
        raise SystemExit(Tf("Não conheço o cartão {0}. Os que eu derivo são: {1}.",
                            ", ".join(pedidos), ", ".join(d[0] for d in DERIVACOES)))

    # O elo entrega -> Key Result e' perguntado ANTES de derivar, porque o cartao 2.1
    # depende dele. So' quando o 2.1 esta entre os alvos: nao ha por que interromper quem
    # pediu so' o cartao 6.
    if any(d[0] == "2.1" for d in alvos):
        perguntar_os_krs(ctx, interativo=not tudo and not dry)

    aplicados, iguais, sem_fonte = [], [], []
    for rotulo, nome, fonte, derivar in alvos:
        novo = (derivar(ctx) or "").strip()
        if not novo:
            sem_fonte.append(rotulo)
            continue
        atual = ctx["respostas"].get(rotulo, "").strip()
        if _comparavel(atual) == _comparavel(novo):
            iguais.append(rotulo)
            continue
        _mostrar(rotulo, nome, fonte, atual, novo)
        if dry:
            continue
        if not tudo and not _confirmar(rotulo):
            print(T("  (deixei o cartão como estava)"))
            continue
        card = _cartao_do_rotulo(ids, rotulo)
        if not card:
            warn(Tf("não achei o cartão {0} na lista INPUTS do seu quadro.", rotulo))
            continue
        try:
            update_card(card["id"], descricao=novo)
            aplicados.append(rotulo)
            ok(Tf("cartão {0} atualizado.", rotulo))
        except Exception as e:
            warn(Tf("não consegui gravar o cartão {0}: {1}", rotulo, e))

    print()
    if iguais:
        ok(Tf("{0} cartão(ões) já estavam de acordo com o quadro: {1}.",
              len(iguais), ", ".join(iguais)))
    if sem_fonte:
        step(Tf("{0} cartão(ões) sem fonte no quadro ainda (a lista que os alimenta está "
                "vazia): {1}.", len(sem_fonte), ", ".join(sem_fonte)))
    if dry:
        done(T("Simulação — NADA foi alterado. Rode sem --dry-run para aplicar."))
        return
    if not aplicados:
        done(T("Nada a atualizar: o seu Termo de Abertura já conta a mesma história que o "
               "seu quadro."))
        return

    # Gravou cartao do termo? A memoria do curso (_saida/contexto.md) e o documento do
    # Passo 1 passam a mentir na hora seguinte — os dois nascem destes cartoes. Refazer os
    # dois aqui e' o mesmo desenho do recalcular.py: quem muda o plano regenera o retrato.
    limpar_cache()
    _refazer_memoria_e_documento(ids, abordagem)
    done(Tf("Termo de Abertura atualizado a partir do quadro ({0} cartão(ões): {1}).",
            len(aplicados), ", ".join(aplicados)))
    print(T("  O quadro continua sendo o seu projeto — e agora o Passo 1 volta a contar a"))
    print(T("  mesma história que ele."))


def _refazer_memoria_e_documento(ids, abordagem):
    """A memoria (_saida/contexto.md) e o Termo de Abertura em Word, relidos do quadro.
    Nunca derruba a opcao: os cartoes ja foram gravados, que e' o que importa."""
    import os
    try:
        import contexto
        dados = quadro.ler_inputs(ids)
        contexto.salvar(
            abordagem=abordagem, justificativa=dados.get("justificativa"),
            okr=dados.get("okr"), entregas=dados.get("entregas"),
            partes=dados.get("partes"), onde=dados.get("onde"),
            marcos=dados.get("marcos"), ordem_de_grandeza=dados.get("ordem_de_grandeza"),
            restricoes=dados.get("restricoes"), premissas=dados.get("premissas"),
            riscos_iniciais=dados.get("riscos_iniciais"))
        step(T("Memória do projeto atualizada (_saida/contexto.md)."))
    except Exception as e:
        warn(Tf("não consegui atualizar a memória do projeto agora ({0}).", e))
    try:
        import artefatos
        caminho, _av = artefatos.gerar_termo_de_abertura(ids, abordagem)
        print(Tf("  📄 _saida/documentos/{0}", os.path.basename(caminho)))
    except Exception as e:
        warn(Tf("não consegui regerar o Termo de Abertura em Word agora ({0}) — use a "
                "tecla D do menu.", e))


if __name__ == "__main__":
    main()
