"""
Project Together — powered by AI: o esqueleto COMUM dos passos 2 a 10.

Todo passo do curso tem a mesma forma — e essa forma E o metodo:
    1. a IA respondeu um bloco rotulado ([P3], [P4]...) e voce colou na
       "0. Caixa de entrada" do seu quadro;
    2. o script LE esse bloco do quadro (voce nunca edita arquivo);
    3. escreve os cartoes no lugar certo;
    4. tira o bloco usado da caixa (idempotente: rodar de novo nao duplica);
    5. registra o progresso e ja te entrega o prompt do PROXIMO passo, pronto,
       na area de transferencia.

Este modulo carrega os pedacos repetidos (ler o bloco, avisar quando falta,
entregar o proximo prompt) para cada script de passo ficar curto e so' cuidar do
que e' DELE: parsear o bloco e montar os cartoes.
"""
import re

import contexto
import entradas
import vocabulario
import progresso
import quadro
from env import cfg, require
from pa_client import limpar_cache
from prompts import nome_abordagem, rotulo
from runner import copiar, done, header, ok, step, warn
from idioma import T, Tf


def abrir(n, titulo=None):
    """Cabecalho + o basico que todo passo precisa. Devolve (c, ids, abordagem).

    `titulo=None` (o normal) monta o cabecalho com o nome do passo NA ABORDAGEM do
    aluno: quem escolheu o agil abre o Passo 5 lendo "Sprint Planning — planejar a
    Sprint", que e' o nome do evento na agenda do time dele. O numero e o mesmo nas
    duas abordagens — o que muda e' o vocabulario (ver prompts.rotulo)."""
    c = cfg()
    require(c, "PROJECTADM_TOKEN", "BOARD_ID")
    abordagem = quadro.abordagem_do_env()
    if not abordagem:
        raise SystemExit(
            T("Antes deste passo, escolha a sua abordagem de desenvolvimento:\n"
            "  rode 01a_escolher_abordagem.py (o COMO do Passo 1) — 5 perguntas, você decide."))
    # O nome do passo passa por Tf(): "Passo 4 — " era uma f-string CRUA, e num pacote
    # em ingles o cabecalho saia meio traduzido ("Passo 4 — Schedule and budget"), porque
    # so' o rotulo vinha do catalogo. A frase inteira e' que se traduz.
    nome = titulo or Tf("Passo {0} — {1}", n, rotulo(n, abordagem))
    header(f"{nome}  ({nome_abordagem(abordagem)})")
    ids = quadro.garantir_listas(abordagem, log=lambda _m: None)
    return c, ids, abordagem


# ── Rodar um passo DE NOVO: apagar antes, ou escrever por cima? ──────────────
# Eduardo, 2026-07-28: "quando for gerar novamente algum passo, perguntar se nao quer
# excluir o que ja foi feito".
#
# O problema e' real e ja cobrou o preco no board 270: o Passo 4 rodado duas vezes (com
# IAs diferentes) deixou 17 itens de orcamento de duas rodadas — total inflado. Escrever
# por cima e' idempotente para o item que VOLTA na resposta nova; o que NAO volta fica
# no quadro, ao lado do novo, sem nada dizendo de onde veio.
#
# So' perguntamos nos passos que REGERAM o mesmo artefato. Os passos 5, 7, 8, 9 e 10 sao
# ROTINA (o ciclo da Sprint, a Daily, cada mudanca): rodar de novo ali e' o uso normal —
# perguntar "quer apagar o que ja foi feito?" a cada Daily seria oferecer um estrago.
# O Passo 1 tambem fica de fora: ele e' uma CORRENTE de 6 elos, e rodar de novo e' andar
# nela (quem quer refazer um elo usa `reset.py 1 <elo>`).
REGERA = {
    2: T("o escopo (a EAP / o Product Backlog)"),
    3: T("os pontos de atenção (riscos, premissas e restrições)"),
    4: T("o cronograma (as datas na EAP) e o orçamento"),
}


def confirmar_regeracao(n, ids, abordagem=None):
    """Se este passo JA foi concluido e esta prestes a ser gerado de novo, mostra o que a
    rodada anterior criou e pergunta se o aluno quer APAGAR antes de escrever. Devolve
    True se apagou. Nunca derruba o passo: qualquer imprevisto segue sem apagar nada.

    O padrao e' NAO apagar (Enter = manter): a resposta destrutiva nunca e' a de quem
    apertou Enter sem ler."""
    if n not in REGERA:
        return False
    try:
        if n not in progresso.feitos():
            return False
        import reset
        previa = reset.limpar_passo(ids, n, dry=True)
    except Exception as e:
        warn(Tf("não consegui verificar o que o Passo {0} já tinha criado ({1}) — sigo "
                "escrevendo por cima, sem apagar nada.", n, e))
        return False

    # a trilha e a Caixa de entrada nao interessam ao aluno aqui: ele quer saber o que
    # sai do QUADRO (o escopo, os riscos, o orcamento...). O filtro e' pelos rotulos
    # que o proprio reset publica — procurar a palavra "trilha" no texto so' funciona
    # enquanto o texto e' portugues.
    itens = [(rot, q) for rot, q in previa.items()
             if q and rot not in reset.ROTULOS_DE_BASTIDOR]
    if not itens:
        return False

    print(Tf('\n  ⚠️  Você já concluiu o Passo {0} — e está gerando ele DE NOVO.', n))
    print(T("      Vou escrever por cima do que já está no quadro:"))
    print(f"        → {REGERA[n]}.")
    print(T("      O que a rodada anterior criou e não voltar na resposta nova NÃO some"))
    print(T("      sozinho: fica no quadro ao lado do novo (é assim que um orçamento"))
    print(T("      passa a ter itens de duas rodadas e o total infla)."))
    print(Tf('\n      O que o Passo {0} tinha criado, e eu posso apagar antes:', n))
    for rot, q in itens:
        print(f"        · {q}  {rot}")
    try:
        resp = input(Tf('\n  Apagar isso e começar o Passo {0} limpo? [s/N] ', n)).strip().lower()
    except EOFError:
        resp = ""
    if resp not in ("s", "sim", "y", "yes"):
        print(Tf('  Ok — mantenho o que já está lá e escrevo o novo por cima. (Se depois '
                 'sobrar algum\n   item da rodada anterior, a tecla Z do menu — "Zerar um '
                 'passo" — limpa o Passo {0}.)', n))
        return False
    reset.limpar_passo(ids, n, dry=False)
    limpar_cache()
    falhas = reset.avisos_pendentes()
    if falhas:
        warn(Tf("{0} coisa(s) o ProjectAdm não deixou limpar — continuam no quadro: {1}. "
                "Ajuste à mão e rode a tecla Z se precisar.",
                len(falhas), "; ".join(falhas[:4])))
    else:
        ok(Tf("Apaguei o que o Passo {0} tinha criado — o quadro está limpo para esta rodada.", n))
    _baseline_orfa(ids, n, reset)
    return True


def _baseline_orfa(ids, n, reset):
    """Refazer um passo de PLANEJAMENTO com a linha de base já congelada deixa a baseline
    descrevendo um plano que não existe mais — e é contra ela que o Passo 8 mede o
    planejado × realizado e o Passo 9 mede o custo de cada mudança. Uma baseline órfã não
    dá erro em lugar nenhum: ela só mente, em silêncio, até o fim do projeto (Eduardo,
    2026-07-29: "eu fiz o reset do passo 4, então a baseline deveria ter sido removida").

    A DECISÃO É DO LÍDER. Até 29/07 este trecho dizia "baseline não se apaga, se
    substitui" como se fosse lei — e o Eduardo corrigiu: guardar a linha de base é boa
    prática, e boa prática tem dono. Então aqui a gente oferece as DUAS coisas: tirar a
    aprovação do curso (o normal, e recapturar no Passo 5) e, para quem lidera o projeto,
    DESCARTAR uma linha de base de vez (baseline.escolher_e_descartar)."""
    try:
        if n > 4 or 5 not in progresso.feitos():
            return
    except Exception:
        return
    print(Tf('\n   ⚠️  Você já tinha APROVADO o plano no Passo 5 — e acabou de refazer o Passo '
             '{0}.', n))
    print(T("      A sua linha de base congelou um plano que está mudando agora. Enquanto ela"))
    print(T("      estiver lá, o Passo 8 compara o realizado com uma referência que já não é"))
    print(T("      verdade, e o Passo 9 mede mudanças contra o plano errado."))
    print(T("\n      O que você quer fazer? (a decisão é do líder do projeto)"))
    print(T("        1. Tirar a aprovação do plano e reaprovar no Passo 5  ← o normal"))
    print(T("        2. DESCARTAR uma linha de base (escolho qual) e registrar a decisão"))
    print(T("        Enter. Não mexer em nada"))
    try:
        resp = input(T("  Escolha [1/2/Enter]: ")).strip().lower()
    except EOFError:
        resp = ""
    if resp == "2":
        try:
            import baseline
            baseline.escolher_e_descartar(
                ids, motivo_padrao=f"O Passo {n} foi refeito depois da aprovação do plano.")
        except Exception as e:
            warn(Tf("não consegui abrir as linhas de base agora ({0}).", e))
        return
    if resp not in ("1", "s", "sim", "y", "yes"):
        print(T("  Ok — mantive a linha de base. Rode o Passo 5 de novo quando o plano estiver"))
        print(T("  fechado: ele captura uma baseline nova, que passa a valer."))
        return
    for rot, qtd in reset.limpar_passo(ids, 5, dry=False).items():
        if qtd:
            step(f"{qtd}  {rot}")
    limpar_cache()
    ok(T("Linha de base retirada da sua trilha — o plano voltou a 'não aprovado'. Rode o "
       "Passo 5 quando terminar de ajustar: ele captura a nova, que passa a valer. (A "
       "linha de base anterior continua congelada no ProjectAdm; para descartá-la de vez, "
       "escolha a opção 2 aqui ou use o Passo 9 — o ProjectAdm registra quem descartou, "
       "quando e por quê.)"))


def ler_bloco(ids, n, o_que, extras=None):
    """O bloco [PN] que o aluno colou na caixa. Se nao houver, MONTA o prompt DESTE
    passo (lido do quadro), poe na area de transferencia e ENCERRA — pronto para colar.

    E' o coracao do modo CHAT: a porta de entrada e a Caixa de entrada do quadro. E,
    desde 2026-07-15, todo passo SE BASTA: antes, quando faltava a resposta, o script so'
    dizia "voce ja deveria ter o prompt" — mas quem chegava ao passo N numa sessao nova
    (area de transferencia vazia) ou que rodou o passo N-1 noutro dia nao tinha o prompt
    em lugar nenhum, e ficava preso (Eduardo: "se rodei o passo 6 noutra sessao, o passo
    7 nao me traz o prompt"). Agora, rodou e nao ha resposta -> o passo te ENTREGA o
    prompt na hora.

    `extras` sao [INPUT x] que o PROPRIO PASSO calculou antes de chamar aqui — hoje so' o
    Passo 7, que roda o motor de execucao primeiro e passa a foto pronta
    ([INPUT SITUACAO_EXECUCAO]). E' parametro, e nao mais um setdefault dentro do
    entregar_prompt, porque calcular essa foto custa chamadas de API que os outros nove
    passos nao usam.
    """
    limpar_cache()
    bloco = quadro.blocos_da_caixa(ids).get(f"P{n}")
    if bloco:
        # ha' resposta nova para este passo: se ele JA foi feito, pergunte antes se e'
        # para apagar a rodada anterior (a resposta ja esta em memoria — apagar depois
        # daqui nao a perde).
        confirmar_regeracao(n, ids)
        return bloco
    copiou, _texto, _faltando = entregar_prompt(n, ids, extras=extras)
    # O "Ctrl+V" so' pode ser dito quando o prompt REALMENTE foi para a area de
    # transferencia. Faltando um [INPUT x] duro, o entregar_prompt nao copia de proposito
    # (o prompt iria pela metade) — e o passo 1 mandava dar Ctrl+V assim mesmo, o que
    # colava na IA o que estivesse no clipboard de antes. Duas frases, uma por caso.
    raise SystemExit(
        Tf('\n Ainda não colei nenhuma resposta do Passo {0} ({1}) no seu quadro —\n   por '
           'isso acabei de te ENTREGAR o prompt do Passo {2} (veja acima).\n ', n, o_que, n)
        + (T('\n   1. Cole o prompt na sua IA (Ctrl+V).') if copiou else
           T('\n   1. Pegue o prompt onde eu indiquei acima (o cartão da lista Referências\n'
             '      ou o arquivo) e cole-o na sua IA.'))
        + Tf('\n   2. Copie a resposta INTEIRA dela (ela começa com [P{0}]).\n   3. Cole no '
             'cartão "0. Caixa de entrada" do seu quadro.\n   4. Rode o Passo {1} de novo — '
             'eu leio o quadro e sigo com você.', n, n))


def bloco_opcional(ids, n, extras=None):
    """Como o `ler_bloco`, mas a resposta da IA e' OPCIONAL: devolve o bloco [PN] se ele
    estiver na caixa, ou **None** — sem encerrar o script.

    Existe por causa do Passo 7 (Eduardo, 19/08/2026: "nao seja necessario rodar a IA no
    passo 7"). O passo ja' dirige a execucao e gera o Kick-Off LENDO O QUADRO, e nada
    disso depende da IA; morrer no portao dela era cobrar uma ida e volta para entregar o
    que ja' estava pronto — e deixava o passo eternamente "nao concluido" na trilha de
    quem so' queria dirigir a execucao (quadro 293: passos 1-6 e 9 feitos, o 7 nunca).

    O prompt continua sendo montado e guardado (cartao da lista Referencias +
    _saida/prompt-passo-N.txt), mas NAO vai para a area de transferencia: quem manda no
    clipboard, no fim de um passo concluido, e' o prompt do passo SEGUINTE."""
    limpar_cache()
    bloco = quadro.blocos_da_caixa(ids).get(f"P{n}")
    if bloco:
        confirmar_regeracao(n, ids)
        return bloco
    entregar_prompt(n, ids, extras=extras, para_clipboard=False)
    return None


def entregar_prompt(n, ids, extras=None, para_clipboard=True):
    """Monta o prompt do passo N (lido do QUADRO), poe na area de transferencia, salva
    em _saida/ e imprime como o aluno o recebe. Devolve (copiou, texto, faltando).

    `para_clipboard=False` monta e guarda o prompt sem tocar na area de transferencia —
    para o passo que OFERECE a IA em vez de exigi-la (ver `bloco_opcional`).

    Os passos 5, 7, 8, 9 e 10 leem [INPUT QUADRO] (a foto viva do quadro) — por isso a
    foto entra sempre nos extras; um passo que nao a use simplesmente a ignora. O mesmo
    vale para [INPUT DOD] (a Definicao de Pronto): no ciclo da Sprint, a Planning, a
    Daily e a Review todas julgam contra ela, e ela nao mora num cartao do termo.
    """
    from prompts import NOME_DO_INPUT
    extras = dict(extras or {})
    extras.setdefault("QUADRO", quadro.snapshot(ids))
    extras.setdefault("DOD", quadro.texto_da_dod(ids))
    # A META DO PRODUTO entra pelo mesmo caminho da DoD, e pela mesma razao: ela e' o
    # compromisso do Product Backlog e nao mora num cartao do termo de abertura. Todo
    # passo que decide ORDEM ou julga PROGRESSO precisa dela em contexto — a Planning
    # (passo 5), a Review (passo 8, que pelo Guia Scrum inspeciona o progresso RUMO a
    # ela), a repriorizacao (passo 9) e o encerramento (passo 10).
    extras.setdefault("META_PRODUTO", quadro.texto_da_meta_do_produto(ids))
    texto, faltando = contexto.corpo_prompt_passo(n, ids, extras=extras)
    contexto.gerar_prompt_pronto(n, ids, extras=extras)
    rot = rotulo(n, quadro.abordagem_do_env())
    if not texto:
        return False, None, faltando
    duro = falta_dura(faltando)
    copiou = (not duro) and para_clipboard and copiar(texto)
    if not para_clipboard:
        # Quem chamou assim vai dizer, com as palavras DELE, o que fazer com este prompt
        # (o Passo 7 o oferece como extra, depois de ja' ter concluido o passo). Duas
        # vozes dizendo a mesma coisa em ordens diferentes confundem mais do que ajudam.
        return copiou, texto, faltando
    if copiou:
        print(Tf('\n  ✅ O prompt do PASSO {0} ({1}) está na sua ÁREA DE', n, rot))
        print(T("     TRANSFERÊNCIA — com o seu projeto dentro. Cole na sua IA (Ctrl+V)."))
        print(Tf('     (também no cartão "{0:02d} - Passo {1}" da lista Referências e em', n, n))
        print(Tf("      _saida/prompt-passo-{0}.txt.)", n))
    else:
        print(Tf('\n  O prompt do PASSO {0} ({1}) está no cartão "{2:02d} - Passo {3}"', n, rot, n, n))
        print(Tf("     da lista Referências e em _saida/prompt-passo-{0}.txt.", n))
        if duro:
            faltam = ", ".join(NOME_DO_INPUT.get(f, f) for f in duro)
            print(Tf("     (antes de colar, responda no quadro: {0}.)", faltam))
    return copiou, texto, faltando


# Alguns [INPUT x] sao OPCIONAIS: os campos preliminares do Passo 1 que muita gente
# deixa em branco de proposito (o QUANDO e o QUANTO preliminares, e o ONDE). Se so'
# faltam esses, o prompt do proximo passo ainda esta pronto para colar — a ausencia
# deles nao segura o aluno.
#
# DOD entra aqui e o motivo e' o oposto de "opcional por descuido": na PRIMEIRA Sprint
# Planning a Definicao de Pronto AINDA NAO EXISTE — ela NASCE nesse passo. Sem isto, o
# [INPUT DOD] contava como lacuna dura e o prompt do Passo 5 nao ia para a area de
# transferencia (Eduardo, 2026-07-27: "nao esta ficando o prompt no clipboard do passo
# 5") — o passo prendia o aluno pedindo justamente o que ele foi ali criar. O
# preencher_inputs ja diz a verdade a IA ("ainda em branco no meu quadro: a sua
# Definicao de Pronto") e o proprio prompt trata os dois casos: se ja existe, repete
# identica; se nao, escreve pela primeira vez. Vale igual no Passo 7 e no Passo 8.
# META_PRODUTO e' OPCIONAL de proposito, como a DoD. Quem ja estava no meio de um
# projeto quando ela passou a existir (2026-08-09) nao tem o cartao — e travar o Passo
# 5 desses alunos seria trocar uma falta por uma parada. O prompt AVISA que ela falta;
# a conferencia do Passo 2 cobra; o projeto nao para.
INPUTS_OPCIONAIS = {"5", "6", "4.6", "DOD", "META_PRODUTO"}


def falta_dura(faltando):
    """O que falta e' MESMO necessario (tirando os inputs preliminares opcionais)."""
    return [f for f in (faltando or []) if f not in INPUTS_OPCIONAIS]


# Passos que sao ROTINA / SOB DEMANDA — nao ha um "proximo prompt" pronto para entregar
# no fim deles: o aluno os roda quantas vezes precisar. O Passo 9 (controlar mudancas)
# comeca PERGUNTANDO qual e a mudanca, entao nao da para pre-montar o prompt dele aqui.
_SOB_DEMANDA = {9}


# ── A TAREFA TEM DE SE ANUNCIAR NA LISTA ─────────────────────────────────────
# A tarefa do Passo 7 nasce como SUBTAREFA do seu item (agil) ou do seu pacote
# (preditiva) — `cartao_pai_id`. So' que o vinculo aparece DENTRO do cartao pai (a
# secao "Sub-tarefas" do cartao); na FACE da lista ela e' um cartao como outro
# qualquer. Rodando a Daily de verdade, o Sprint Backlog ficou com 3 itens e 9 tarefas
# soltas ao lado deles, sem dizer de quem eram (Eduardo, 2026-07-27: "onde ficaram as
# atividades que eu escolhi para minha daily scrum?").
# Por isso a tarefa leva a alca do pai NO PROPRIO TITULO: "↳ B3 · Realizar a avaliacao…".
# Ler a lista basta — sem filtro, sem abrir cartao.
_PREFIXO_TAREFA = re.compile(r"^\s*↳\s*(?:[A-Za-z]{0,2}\d[\d.]*\s*·\s*)?")


def titulo_base(t):
    """O titulo da tarefa SEM o prefixo "↳ CODIGO · ".

    E' por ele que se sabe se a tarefa JA existe no quadro: comparar o titulo cheio
    faria a proxima Daily criar uma copia prefixada de cada tarefa antiga."""
    return _PREFIXO_TAREFA.sub("", (t or "").strip())


def titulo_da_tarefa(titulo, codigo):
    """'Realizar a avaliacao' + 'B3' -> '↳ B3 · Realizar a avaliacao'."""
    codigo = (codigo or "").strip().upper()
    return f"↳ {codigo} · {titulo}" if codigo else f"↳ {titulo}"


def normalizar_tarefas(lid, codigo_por_pai, cards=None, marca=None, log=step):
    """Da' a alca do pai as tarefas que JA estavam no quadro. Devolve quantas renomeou.

    Autocorrecao, e ela e' necessaria porque o Passo 5 (Sprint Planning) e o Passo 7
    (Daily/execucao) criam tarefas desde antes desta convencao: quem ja rodou o curso tem
    tarefas soltas, sem dizer de que item sao. Renomeia NO LUGAR (nunca cria copia), e'
    idempotente e so' mexe em cartao que e' TAREFA:
      - `codigo_por_pai` = {id_do_pai: codigo} — so' filho de um pai conhecido entra;
      - `marca` (opcional) = so' cartao com essa marca na descricao (na preditiva, e' o
        que separa a tarefa de execucao do PACOTE da EAP, que tambem e' filho);
      - cartao que tem codigo proprio (B3, um item) nunca e' tarefa — fica de fora.
    Falha SEGURA: renomear e' conforto, nao pode derrubar o passo.
    """
    try:
        from concurrent.futures import ThreadPoolExecutor

        from pa_client import get_card, list_cards, update_card
        cards = cards if cards is not None else list_cards(lid)
        alvo = [c for c in cards
                if not quadro.codigo_do_item(c.get("titulo") or "")
                and (marca is None or marca in (c.get("descricao") or ""))]
        if not alvo:
            return 0

        def _pai(c):
            try:
                return c, (get_card(c["id"]) or {}).get("cartao_pai_id")
            except Exception:
                return c, None
        with ThreadPoolExecutor(max_workers=8) as ex:
            pares = list(ex.map(_pai, alvo))
        n = 0
        for c, pai in pares:
            if pai is None:
                continue
            cod = codigo_por_pai.get(str(pai))
            if cod is None:
                continue
            novo = titulo_da_tarefa(titulo_base(c.get("titulo") or ""), cod)
            if (c.get("titulo") or "").strip() != novo and c.get("id"):
                try:
                    update_card(c["id"], titulo=novo)
                    n += 1
                except Exception as e:
                    log(Tf('  [aviso] não consegui renomear "{0}": {1}', novo[:34], e))
        return n
    except Exception as e:
        log(Tf("  [aviso] não consegui padronizar os títulos das tarefas: {0}", e))
        return 0


def links(cartoes, n=6):
    """Os links diretos dos cartoes (a MESMA fonte da auditoria: nao ha duas).

    Um passo que ESCREVE tem de dizer onde escreveu. O filtro por etiqueta do quadro
    voltou a funcionar em 30/07 (o Henry corrigiu o item 0e), mas o link continua sendo o
    caminho curto: ele abre o cartao EXATO do achado em um clique."""
    try:
        from auditoria import _links
        return _links(cartoes, n)
    except Exception:
        return ""


def conferir(n, ids, abordagem=None):
    """Roda as conferencias deterministicas do passo (auditoria.checar), as imprime e
    MARCA no quadro (etiqueta) os cartoes que precisam de revisao — assim o aluno filtra
    o quadro e vai direto ao que falta. NUNCA trava o passo: a revisao e a decisao
    continuam sendo dele. Falha em silencio se a auditoria nao rodar."""
    try:
        import auditoria
        abordagem = abordagem or quadro.abordagem_do_env()
        achados = auditoria.checar(n, ids, abordagem)
        print("\n" + auditoria.formatar_passo(n, achados))
        # marca os cartoes das listas que ESTE passo fecha (idempotente, reconcilia)
        listas = auditoria.STEP_LISTAS.get(n, ())
        if listas:
            marc, limpo, com_marca = auditoria.marcar_no_quadro(ids, abordagem, listas, log=step)
            # SEMPRE diz como o quadro ficou — nao so' quando MUDOU. Rodar o passo de novo
            # nao muda nada (os cartoes ja estavam marcados) e o silencio parecia "nao
            # marquei" (Eduardo, 23/07).
            if com_marca:
                extra = []
                if marc:
                    extra.append(Tf("{0} agora", marc))
                if limpo:
                    extra.append(Tf("{0} já resolvido(s) desmarcado(s)", limpo))
                detalhe = (Tf(" ({0})", ' · '.join(extra)) if extra
                           else T(" (já estavam marcados)"))
                ok(Tf("{0} cartão(ões) com a etiqueta 🔎 Revisar{1} — abra pelos links acima, "
                      "ou filtre o quadro pela etiqueta.", com_marca, detalhe))
            elif limpo:
                ok(Tf("{0} cartão(ões) resolvido(s): tirei a etiqueta 🔎 Revisar. Nada pendente "
                      "nesta etapa.", limpo))
            else:
                ok(T("Nenhum cartão marcado com 🔎 Revisar nesta etapa."))
    except Exception as e:
        warn(Tf("não consegui rodar as conferências automáticas do Passo {0} agora: {1}", n, e))


def finalizar(n, ids, resumo, mensagem_conclusao, conferir_passo=True, proximo=None):
    """Fecha o passo: tira o bloco usado da caixa, registra o progresso, roda as
    conferencias determinsticas do passo (o que ele acabou de escrever) e entrega o
    prompt do proximo passo (ja preenchido, com a foto viva do quadro) na area de
    transferencia. Devolve o proximo n (ou None).

    `conferir_passo`=False para o passo que ja roda a sua propria auditoria (Passo 5).
    `proximo` quebra a sequencia N -> N+1: e' o que faz o CICLO DA SPRINT existir sem
    inventar passos novos. No agil, a Retrospectiva (Passo 10) fecha a Sprint e o proximo
    passo do aluno nao e' "acabou" — e' o Passo 5, a Planning da Sprint seguinte."""
    quadro.consumir_blocos(ids, [f"P{n}"], log=step)
    progresso.registrar(n, resumo)
    done(mensagem_conclusao)
    if conferir_passo:
        conferir(n, ids, quadro.abordagem_do_env())

    if n in _SOB_DEMANDA:
        print(T("\n  Este passo é ROTINA — rode-o sempre que precisar (cada mudança é uma)."))
        print(T("  Quando quiser seguir, escolha o próximo passo no menu: ao rodar, ele já"))
        print(T("  te entrega o prompt pronto, com a foto do seu quadro dentro."))
        return None

    prox = proximo or (n + 1)
    if prox > 10:
        print(T("\n  🏁 Você percorreu os 10 passos. O seu projeto foi do porquê ao valor"))
        print(T("     entregue — tudo no seu quadro. Parabéns!"))
        return None

    if prox in _SOB_DEMANDA:
        # O Passo 9 e sob demanda e comeca PERGUNTANDO a mudanca — nao da para entregar
        # um prompt pronto aqui. Antes, o fim do Passo 8 TENTAVA copiar o prompt do 9 e
        # falhava em silencio (faltava o [INPUT MUDANCA]): o aluno lia "copiei" mas o
        # clipboard vinha vazio (Eduardo, 2026-07-15). Agora dizemos a verdade.
        print(Tf('\n  O PRÓXIMO é o Passo {0} (Controlar mudanças) — e ele é SOB DEMANDA:', prox))
        print(T("  rode-o quando receber um pedido de mudança. Ele pergunta o que mudou,"))
        print(T("  monta o prompt de análise de impacto e registra cada mudança como um"))
        print(T("  cartão na lista \"Mudanças\" do seu quadro."))
        return prox

    # Passo linear: entrega o proximo prompt ja pronto (a comodidade da primeira volta).
    copiou, texto, _faltando = entregar_prompt(prox, ids)
    if texto is None:
        # O proximo passo NAO usa IA nesta abordagem (ex.: Passo 5 na preditiva, que o
        # python audita sozinho). Nao ha prompt para colar — e' so' rodar.
        rot = rotulo(prox, quadro.abordagem_do_env())
        print(Tf('\n  O PRÓXIMO é o Passo {0} ({1}) — e a revisão do plano é SUA. Ao', prox, rot))
        print(T("  rodá-lo, fazemos algumas conferências automáticas para ajudar (completude"))
        print(T("  de OKR, escopo, riscos e orçamento) e apontamos o que falta; você revisa"))
        print(Tf("  e decide o que ainda está incompleto. É só escolher o Passo {0} no menu.", prox))
    elif copiou:
        print(Tf('\n  O RITMO É SEMPRE O MESMO: cole na IA → cole a resposta (começa com'))
        print(Tf('  [P{0}]) no cartão "0. Caixa de entrada" → rode o Passo {1}.', prox, prox))
    return prox


# ── helpers de escrita no quadro (usados pelos scripts dos passos) ──────────────
def escrever_cartoes(ids, chave_lista, itens, log=step):
    """Cria (idempotente, em paralelo) os cartoes `itens`=[(titulo, descricao, prioridade)]
    na lista `chave_lista`. Nao duplica: compara pelo titulo. Prioridade opcional."""
    from pa_client import criar_cartoes, update_card, list_cards
    lid = ids[chave_lista]
    existentes = {(c.get("titulo") or ""): c for c in list_cards(lid)}
    novos = [(t, d, None) for t, d, _p in itens if t and t not in existentes]
    if novos:
        ids_novos = criar_cartoes(lid, novos, log=log)
        for (t, _d, _p), cid in zip(novos, ids_novos):
            if cid:
                existentes[t] = {"id": cid}
    # prioridade (severidade) + atualizar descricao de quem ja existia
    for titulo, desc, prio in itens:
        cd = existentes.get(titulo)
        if not cd:
            continue
        campos = {}
        if prio:
            campos["prioridade"] = prio
        if desc and entradas.texto_do_cartao(cd.get("descricao", "")) != desc.strip():
            campos["descricao"] = desc
        if campos and cd.get("id"):
            try:
                update_card(cd["id"], **campos)
            except Exception as e:
                log(Tf('  [aviso] não consegui atualizar "{0}": {1}', titulo[:34], e))
    return sum(1 for t, _d, _p in itens if t in existentes)


# ── Os PONTOS DE ATENCAO viram cartoes (Passo 3 — e a mudanca do Passo 9) ────
# Estava tudo dentro do 03_riscos.py. Saiu para ca' quando o Passo 9 passou a poder
# DETALHAR os riscos que uma mudanca traz (bloco [P9.3]): o formato da resposta e' o
# mesmo do Passo 3, entao o codigo que a le tambem tem de ser o mesmo — dois leitores do
# mesmo formato viram dois comportamentos diferentes no primeiro ajuste.
_PREFIXO_TIPO = re.compile(r"(?i)^\s*(premissas?|restri[cç][aã]o|restricoes|restricao)\s*:\s*")


def _classe_do_ponto(r):
    """'ameaca'|'oportunidade'|'premissa'|'restricao' — aceita o campo 'tipo:' e tambem o
    prefixo 'PREMISSA:'/'RESTRICAO:' no proprio texto (varia conforme a IA)."""
    # Os radicais aceitam os dois idiomas (vocabulario.py): a IA do aluno pode
    # responder "type: threat" mesmo num curso em portugues.
    tipo_r = entradas._sem_acento(r.get("tipo") or "")
    txt = entradas._sem_acento(r.get("risco") or "")
    def _tem(*radicais):
        return any(x in tipo_r for x in radicais)
    if _tem("restric", "constraint") or txt.startswith(("restric", "constraint")):
        return "restricao"
    if _tem("premissa", "assumption") or txt.startswith(("premissa", "assumption")):
        return "premissa"
    return "oportunidade" if _tem("oportun", "opportun") else "ameaca"


def pontos_de_atencao(bloco):
    """Le um bloco no formato do Passo 3 ([P3] ou [P9.3]) -> [(classe, item)].

    A classificacao vem da SECAO (RISCOS:/PREMISSAS:/RESTRICOES:), nao de um rotulo por
    item — muito mais confiavel (a IA erra o 'tipo:' por item, mas acerta as secoes).
    Sem secoes (formato antigo), cai no _classe_do_ponto item a item."""
    secs = entradas.secoes(bloco, ["RISCOS", "PREMISSAS", "RESTRICOES", "RESTRIÇÕES"])
    tem_secoes = any(secs.get(k) for k in ("riscos", "premissas", "restricoes"))
    pontos = []
    if tem_secoes:
        for r in entradas.riscos(secs.get("riscos", "")):
            # "opportunity" NAO contem "oportun" (Eduardo, quadro 341, 21/08/2026).
            # O proprio prompt do pacote EN pede 'type: threat|opportunity', entao TODA
            # oportunidade escrita em ingles caia aqui como AMEACA — sem erro e sem aviso —
            # e o passo ainda fechava com "0 oportunidade(s)" e sugeria procurar uma. Os
            # dois radicais ja moram em vocabulario.TIPO_PONTO, que e' quem le a marca de
            # volta (quadro._tipo_do_ponto); a classificacao passa a usar a MESMA tabela.
            _tipo = entradas._sem_acento(r.get("tipo", ""))
            cl = "ameaca"
            for _chave, _canon in vocabulario.TIPO_PONTO:
                if _chave in _tipo:
                    cl = "oportunidade" if _canon == "Oportunidade" else "ameaca"
                    break
            pontos.append((cl, r))
        for it in entradas.itens(secs.get("premissas", "")):
            pontos.append(("premissa", it))
        for it in entradas.itens(secs.get("restricoes", "")):
            pontos.append(("restricao", it))
    else:
        for r in entradas.riscos(bloco):
            pontos.append((_classe_do_ponto(r), r))
    return pontos


def cartoes_de_riscos(pontos):
    """[(classe, item)] -> ([(titulo, descricao, prioridade)], contagem por classe).

    O TITULO tem teto de 255 na API; o ENUNCIADO COMPLETO vai na descricao logo apos a
    marca (🔴/🟢/🔵/🟠), que e' de onde o 'Registro dos pontos de atencao' le a coluna
    Descricao. A marca da 1a linha e' o que diz o TIPO — nao mude sem mudar a leitura."""
    cartoes = []
    contagem = {"ameaca": 0, "oportunidade": 0, "premissa": 0, "restricao": 0,
                # As duas classes que o Passo 3 nao produz, mas que a volta da planilha
                # produz (importar_riscos.py, 18/08/2026): o aluno escolhe "Problema"
                # quando o risco JA' aconteceu e "Risco" quando ainda nao qualificou se
                # e' ameaca ou oportunidade. Nascem aqui, e nao la', porque quem escreve
                # o cartao de um ponto de atencao e' esta funcao — duas funcoes
                # escrevendo o mesmo cartao divergem no primeiro ajuste.
                "problema": 0, "risco": 0}
    for classe, r in pontos:
        enun = _PREFIXO_TIPO.sub("", (r.get("risco") or r.get("_") or "")).strip() \
            or "(ponto de atenção sem descrição)"
        titulo = enun[:255]
        contagem[classe] = contagem.get(classe, 0) + 1
        # O CARTÃO DE RISCO NO IDIOMA DO ALUNO (06/08/2026). Era tudo f-string crua, então
        # o quadro em inglês nascia com "🔴 AMEAÇA / Probabilidade: / Impacto: /
        # Prioridade: / Resposta: / Dono:" — o cartão INTEIRO em português. A marca da 1ª
        # linha também é traduzida: quem a lê de volta é vocabulario.TIPO_PONTO, que já
        # conhece as duas línguas ("ameac"/"threat", "oportun"/"opportun"...), e os
        # leitores de quadro.py passaram a aceitar os dois rótulos na mesma data.
        if classe == "restricao":
            cartoes.append((titulo, T("🟠 RESTRIÇÃO") + f"\n{enun}", None))
        elif classe == "premissa":
            cartoes.append((titulo, T("🔵 PREMISSA") + f"\n{enun}\n"
                            + Tf("Dono: {0}", r.get('dono') or T("(a confirmar)")), None))
        else:
            marca = {"oportunidade": T("🟢 OPORTUNIDADE"),
                     "problema": T("🚧 PROBLEMA"),
                     "risco": T("⚪ RISCO")}.get(classe, T("🔴 AMEAÇA"))
            prob = (r.get("probabilidade") or "").strip()
            imp = (r.get("impacto") or r.get("gravidade") or "").strip()
            num, nivel = quadro.prioridade_do_risco(prob, imp)
            linha_prio = (Tf("Prioridade: {0} ({1})", num, quadro.rotulo_nivel(nivel))
                          if num else T("Prioridade: (defina probabilidade e impacto)"))
            desc = (f"{marca}\n{enun}\n"
                    + Tf("Probabilidade: {0}", prob or '?') + "\n"
                    + Tf("Impacto: {0}", imp or '?') + "\n"
                    + f"{linha_prio}\n"
                    + Tf("Resposta: {0}", r.get('resposta') or T("(a definir)")) + "\n"
                    + Tf("Dono: {0}", r.get('dono') or T("(a definir)")))
            cartoes.append((titulo, desc, prioridade_de(nivel)))
    return cartoes, contagem


_PRIORIDADE = {  # gravidade/severidade -> prioridade do cartao no ProjectAdm
    "urgente": "urgente", "critico": "urgente", "critica": "urgente",
    "alta": "alta", "alto": "alta",
    "media": "media", "medio": "media", "média": "media", "médio": "media",
    "baixa": "baixa", "baixo": "baixa",
}


def prioridade_de(palavra):
    """'alta'/'média'/'baixa'... -> a prioridade do cartao (ou None)."""
    return _PRIORIDADE.get(entradas._sem_acento(palavra or ""))


def rotulo_de_campo(chave):
    """O NOME DE UM CAMPO como o aluno o lê no cartão, no idioma dele.

    Vários passos montam o detalhe de um item repetindo a CHAVE do campo como rótulo —
    `" · ".join(f"{k}: {v}" for k in ("item", "responsavel"))`. A chave é canônica em
    português (é assim que o `entradas`/`vocabulario` normaliza a resposta da IA, de
    propósito: um vocabulário só para os dois idiomas), então o cartão do aluno em inglês
    saía com "responsavel: Ana · ate: 2026-08-20" — a chave interna vazando para a tela.

    Aqui a chave vira rótulo, e o rótulo fala o idioma do pacote. Os literais estão
    escritos DENTRO de T() (e não numa variável) para o `extrair_textos.py` os coletar —
    foi exatamente esse o buraco pelo qual o "COMO RODAR" em português chegou aos 11
    cartões da lista Referências (06/08/2026).
    """
    return {
        "item": T("item"),
        "responsavel": T("responsável"),
        "de quem": T("de quem"),
        "ate": T("até"),
        "opcoes": T("opções"),
        "meta": T("meta"),
        "realizado": T("realizado"),
        "por que": T("por quê"),
        "probabilidade": T("probabilidade"),
        "impacto": T("impacto"),
        "resposta": T("resposta"),
        "dono": T("dono"),
    }.get(chave, chave)
