"""
Project Together — powered by AI: a ESTRUTURA do seu quadro (modulo compartilhado).

POR QUE ESTE MODULO EXISTE
  O quadro nao e "um lugar para colar cartoes": ele E o seu projeto. E a estrutura
  dele responde as 7 perguntas do 5W2H — as MESMAS perguntas nas duas abordagens
  de desenvolvimento, como ensina o Guia PMBOK 8a ed. (um termo de abertura
  adaptativo "responde a um subconjunto ou a todas estas perguntas: por que? quem?
  o que? onde? quando? como? quanto?"). O que muda entre PREDITIVA e AGIL nao sao
  as perguntas — e a FORMA e o DETALHE da resposta.

O QUADRO E MONTADO EM DUAS ETAPAS (e ele acompanha a sua leitura do artigo)
  ETAPA 1 — assim que voce comeca: as listas que NAO dependem da abordagem, e a
    lista INPUTS com as 7 perguntas do 5W2H ja abertas em cartoes. Voce responde
    cada cartao conforme le o artigo: 1.1 justificativa, 1.2 OKR, 2.x o que, 3 quem,
    5 quando, 6 quanto. O bloco 4 (o COMO) nasce TRANCADO — e a proxima decisao.
        Referencias e Passos     o playbook: o que fazer em cada passo + o prompt
        INPUTS (Projeto 5W2H)    as 7 perguntas — 1 cartao por secao do termo
        Partes Interessadas      QUEM (clientes, equipe, demais) — passos 1 e 6

  ETAPA 2 — depois que voce escolhe a ABORDAGEM (o COMO do 5W2H, script 01a): o
    resto do quadro. So aqui da para montar, porque e a abordagem que define as
    listas de trabalho — decidir isso antes seria escolher o COMO antes de saber o
    POR QUE, o O QUE e o QUEM (e sao esses fatores que o Guia PMBOK 8 manda usar
    para escolher a abordagem, secao "Escolhendo a abordagem de desenvolvimento").
        4.1 a 4.5                o bloco COMO destrancado (restricoes, premissas,
                                 riscos iniciais, escopo inicial)
        EAP → Cronograma |       O QUE (passo 2) e o QUANDO (passo 4), no nivel do
        Product Backlog          trabalho — os mesmos cartoes ganham datas no passo 4
        Riscos e Premissas       o que pode dar errado (e o que pode dar muito certo)
        Orcamento                QUANTO — passos 1 (ordem de grandeza) e 4 (detalhe)
        Sprint Backlog | Incremento   (SO' na agil — fluxo Kanban; ver abaixo)
        Mudancas                 o que mudou depois de aprovado — passo 9
        Concluido (curso)        a sua trilha no curso (evidencia de conclusao)

O QUE MUDA COM A ABORDAGEM (a variavel ABORDAGEM do .env)
    PREDITIVA:  EAP → Cronograma   (sem listas de fluxo — o STATUS vive no cartao:
                                    a tarefa e' subtarefa do pacote e so' muda de status)
    AGIL:       Product Backlog  |  Sprint Backlog  |  Incremento (Pronto)  (o cartao
                                    MUDA de lista ao longo do fluxo)

  Essa e a resposta a pergunta certa. Nao "que listas o agil precisa para caber no
  5W2H", mas: "o 5W2H faz sentido no agil?" — faz. O PMBOK 8 responde que sim, e o
  quadro mostra como: a Visao do Produto e o POR QUE; o Product Backlog e o O QUE.

  TITULOS DE CARTAO SAO IGUAIS NAS DUAS ABORDAGENS (de proposito: o aluno VE que a
  pergunta nao muda). O que muda e a DESCRICAO — a forma da resposta. Assim, trocar
  de abordagem no meio do caminho atualiza o texto-guia e nunca duplica cartao.
"""
from env import cfg, save_cfg
from pa_client import (list_lists, find_list_id_by_name, create_list, rename_list,
                       delete_list, list_cards, create_card, update_card, get_card)
from prompts import normalizar_abordagem
from idioma import T, Tf

# ── Listas ───────────────────────────────────────────────────────────────────
# chave interna -> titulo no quadro. A chave vira LIST_<CHAVE> no .env.
#
# ETAPA 1: as tres listas que nao dependem da abordagem — o aluno ja pode responder
# o 5W2H nelas. ETAPA 2: as demais, que so existem depois da escolha do COMO.
_COMUNS_ANTES_PT = [
    ("REFERENCIA", "Referências e Passos"),
    ("INPUTS", "INPUTS (Projeto 5W2H)"),
    ("PARTES", "Partes Interessadas [QUEM]"),
]
_ESCOPO_PT = {
    "PREDITIVA": ("ESCOPO", "EAP → Cronograma [O QUÊ → QUANDO]"),
    "AGIL": ("ESCOPO", "Product Backlog [O QUÊ]"),
}
# Titulos ANTIGOS de listas ja renomeadas. Se um quadro que JA existe (o board de
# lancamento, ou o de um aluno que comecou numa versao anterior do curso) tiver uma
# lista com um nome legado, garantir_listas a RENOMEIA no lugar em vez de criar uma
# lista paralela — os cartoes do aluno nunca sao perdidos nem duplicados.
# chave interna -> [nomes antigos].
TITULOS_LEGADOS = {
    "ESCOPO": ["EAP — Entregas e Pacotes [O QUÊ]"],
}
_COMUNS_MEIO_PT = [
    ("RISCOS", "Riscos e Premissas"),
    ("ORCAMENTO", "Orçamento [QUANTO]"),
]
# Listas de FLUXO (o trabalho MUDA de lista: Product Backlog -> Sprint Backlog ->
# Incremento) — existem SO' na abordagem AGIL (Kanban). Na PREDITIVA o trabalho NAO
# muda de coluna: a tarefa e' subtarefa do pacote (a decomposicao da EAP/cronograma) e
# o que muda e' o STATUS do cartao (Aberto -> Em Progresso -> Concluida). E' assim que o
# proprio ProjectAdm modela progresso ("status no cartao, nao na lista") e como o MS
# Project trata um cronograma: nao ha colunas "em execucao"/"entregue", ha status por
# tarefa. Por isso a preditiva nao tem estas duas listas.
_FLUXO_AGIL_PT = [
    ("EXECUCAO", "Sprint Backlog"),
    ("PRONTO", "Incremento (Pronto)"),
]
_COMUNS_DEPOIS_PT = [
    ("MUDANCAS", "Mudanças"),
    ("CONCLUIDO", "Concluído (curso)"),
]

# Os nomes das listas no pacote EN, escritos aqui de proposito: o pacote PT nao leva o
# quadro_en.py, e e' justamente o pacote PT que precisa RECONHECER um quadro montado em
# ingles em vez de montar outro ao lado. No pacote EN esta tabela nao e' usada (os nomes
# vem do quadro_en.py, que e' a fonte); o teste `listas_bilingues.py` prova que as duas
# dizem a mesma coisa, para que ela nao envelheça em silencio.
_NOMES_EN = {
    "REFERENCIA": "References and Steps",
    "INPUTS": "INPUTS (5W2H Project)",
    "PARTES": "Stakeholders [WHO]",
    "ESCOPO": ("WBS → Schedule [WHAT → WHEN]", "Product Backlog [WHAT]"),
    "RISCOS": "Risks and Assumptions",
    "ORCAMENTO": "Budget [HOW MUCH]",
    "EXECUCAO": "Sprint Backlog",
    "PRONTO": "Increment (Done)",
    "MUDANCAS": "Changes",
    "CONCLUIDO": "Completed (course)",
}

# Listas que o curso JA criou em versoes anteriores mas NAO quer mais nesta abordagem.
# Se existirem no quadro e estiverem VAZIAS, garantir_listas as remove; se tiverem
# cartoes, preserva e avisa (o dado do aluno nunca e apagado). A preditiva deixou de ter
# fluxo Kanban — o status vive no cartao.
LISTAS_RETIRADAS = {
    "PREDITIVA": ["Em execução", "Entregue"],
    "AGIL": [],
}

# Marca (1a linha da descricao) das TAREFAS DE EXECUCAO que o Passo 7 cria como
# subtarefas de um pacote, dentro da lista de escopo (EAP → Cronograma). Serve para:
# (1) o reset achar e apagar SO' elas; (2) a arvore_da_eap as IGNORAR — senao um pacote
# com tarefa-filha seria lido como tendo "fases" e a numeracao da EAP quebraria.
_MARCA_EXEC_P7_PT = "🔧 Tarefa de execução (Passo 7)"

# Marca do elemento da EAP que NASCEU no Passo 4 — estimar a duração revelou trabalho que
# faltava no escopo (elaboração progressiva; é normal e é bom). Ao contrário da marca do
# Passo 7, este cartão É parte da WBS (a arvore_da_eap o inclui): ele é um pacote de
# trabalho de verdade. A marca serve para o passo dizer ao aluno o que apareceu, e para a
# auditoria cobrar dele o que todo pacote tem (critério de aceitação, dono, data).
_MARCA_NOVO_P4_PT = "🆕 Descoberto no Passo 4 (cronograma)"


def card_concluido(c):
    """True se o cartao esta CONCLUIDO — sempre por aqui, nunca `if c.get("concluido")`.

    Historia que justifica a funcao: ate 2026-07-30 a API era INCONSISTENTE — o GET de
    CARTAO devolvia `concluido` como booleano e o GET de LISTA devolvia a STRING "0"/"1".
    Como "0" e' TRUTHY em Python, ler o campo direto contava TODO cartao como concluido
    (e o Passo 8 e o verificar_okrs relatavam 100% de entrega com o projeto no comeco).
    O Henry corrigiu (item 0b do NEEDS_FOR_HENRY): medido em 30/07 no quadro 270, os 132
    cartoes vem com booleano nas DUAS rotas. A normalizacao fica: ela custa nada, cobre os
    dois formatos e e' o unico lugar onde este julgamento e' feito."""
    return str(c.get("concluido")).strip().lower() in ("1", "true", "sim")


def listas_do_quadro(abordagem=None):
    """As listas do quadro, NA ORDEM.

    abordagem=None (ETAPA 1) -> so as listas que nao dependem da abordagem.
    abordagem escolhida (ETAPA 2) -> o quadro completo, na forma dessa abordagem.
    A PREDITIVA nao tem as listas de fluxo (execucao/pronto): o status vive no cartao.
    """
    if not abordagem:
        return list(COMUNS_ANTES)
    a = normalizar_abordagem(abordagem)
    if a == "AGIL":
        # As TRES listas de fluxo ficam JUNTAS e nesta ordem, porque elas sao o caminho
        # do trabalho: Product Backlog -> Sprint Backlog -> Incremento. Ate' 09/08/2026
        # Riscos e Orcamento ficavam entre a primeira e a segunda, e o quadro deixava de
        # se ler da esquerda para a direita (Eduardo: "a lista do Product Backlog deve
        # estar a esquerda da Sprint Backlog, que deve estar a esquerda do Incremento").
        # Na PREDITIVA nao ha fluxo (o status vive no cartao), entao la o escopo continua
        # antes dos riscos — que e' a ordem em que se planeja: escopo, depois o que pode
        # dar errado com ele.
        listas = COMUNS_ANTES + COMUNS_MEIO + [ESCOPO[a]] + FLUXO_AGIL
    else:
        listas = COMUNS_ANTES + [ESCOPO[a]] + COMUNS_MEIO
    return listas + COMUNS_DEPOIS


# ─────────────────────────────────────────────────────────────────────────────
# OS NOMES QUE UMA LISTA NOSSA PODE TER  (a porta que custou 24 listas vazias)
#
# Uma lista tem UM nome, mas quem a procura pode estar em outra lingua — ou numa versao
# do curso em que ela se chamava outra coisa. Ate' 07/08/2026 a busca so' conhecia o
# nome no idioma corrente: nao achando, CRIAVA. Num quadro montado em ingles, uma
# execucao em portugues criava as 8 listas de novo, vazias, e ainda reapontava os LIST_*
# do .env para elas — o projeto inteiro sumia da vista do curso. Aconteceu tres vezes no
# quadro 324 (as duas primeiras foram limpas a mao; a terceira estava la' em 07/08).
#
# A resposta nao e' "procurar melhor no idioma certo": e' aceitar que o QUADRO manda no
# nome, e o pacote se adapta. Quem encontra uma lista sua com outro nome RENOMEIA no
# lugar — id preservado, cartoes preservados, .env apontando para a lista de sempre.
# ─────────────────────────────────────────────────────────────────────────────
def _nomes_en(chave, a):
    """Os nomes EN desta chave — do quadro_en.py (pacote EN) ou da tabela local (PT)."""
    if _QEN is not None:
        tabelas = (list(_qen('COMUNS_ANTES') or []) + list(_qen('COMUNS_MEIO') or [])
                   + list(_qen('FLUXO_AGIL') or []) + list(_qen('COMUNS_DEPOIS') or [])
                   + list((_qen('ESCOPO') or {}).values()))
        return [n for k, n in tabelas if k == chave]
    v = _NOMES_EN.get(chave)
    if not v:
        return []
    if not isinstance(v, tuple):
        return [v]
    if not a:                                    # ESCOPO: um nome por abordagem
        return list(v)
    return [v[1]] if a == "AGIL" else [v[0]]


def _nomes_pt(chave, a):
    """Os nomes PT desta chave — as tabelas deste arquivo, sempre presentes."""
    tabelas = _COMUNS_ANTES_PT + _COMUNS_MEIO_PT + _FLUXO_AGIL_PT + _COMUNS_DEPOIS_PT
    nomes = [n for k, n in tabelas if k == chave]
    if chave == "ESCOPO":
        escopos = [_ESCOPO_PT[a]] if a in _ESCOPO_PT else list(_ESCOPO_PT.values())
        nomes += [n for _k, n in escopos]
    return nomes


def nomes_conhecidos(chave, abordagem=None):
    """TODOS os nomes que esta lista pode ter no quadro: os dois idiomas do curso e os
    titulos de versoes anteriores. O primeiro e' o titulo ALVO (o do idioma corrente).

    Usada por quem MONTA o quadro (garantir_listas), por quem so' o LE
    (reverse_engineering) e por quem o CONFERE (conformidade). Uma definicao so': se
    cada um mantivesse a sua lista de nomes, a proxima lista renomeada voltaria a
    duplicar em um deles.
    """
    a = normalizar_abordagem(abordagem) if abordagem else None
    alvo = dict(listas_do_quadro(abordagem)).get(chave)
    nomes = [alvo] if alvo else []
    for n in _nomes_pt(chave, a) + _nomes_en(chave, a) + list(TITULOS_LEGADOS.get(chave, ())):
        if n and n not in nomes:
            nomes.append(n)
    return nomes


def indice_de_nomes(abordagem=None):
    """{nome da lista (minusculo) -> chave interna} para as listas desta etapa.

    E' o que permite olhar um quadro e dizer "esta lista aqui e' a minha lista de riscos,
    mesmo escrita na outra lingua"."""
    idx = {}
    for chave, _titulo in listas_do_quadro(abordagem):
        for nome in nomes_conhecidos(chave, abordagem):
            idx.setdefault(nome.strip().lower(), chave)
    return idx


def idioma_do_quadro(listas, abordagem=None):
    """Em que idioma este quadro foi MONTADO, pelos nomes das listas: 'PT', 'EN' ou None.

    None quando nao da' para afirmar (quadro novo, quadro misturado, empate). Nao ha
    palpite aqui de proposito: quem chama usa isto para decidir se renomeia o quadro de
    alguem."""
    a = normalizar_abordagem(abordagem) if abordagem else None
    nomes = {(l.get('nome') or '').strip().lower() for l in listas}
    contagem = {"PT": 0, "EN": 0}
    for chave, _t in listas_do_quadro(abordagem):
        pt = {n.strip().lower() for n in _nomes_pt(chave, a)}
        en = {n.strip().lower() for n in _nomes_en(chave, a)}
        for lingua, esperado in (("PT", pt - en), ("EN", en - pt)):
            if nomes & esperado:
                contagem[lingua] += 1
    if contagem["PT"] == contagem["EN"]:
        return None
    return "PT" if contagem["PT"] > contagem["EN"] else "EN"


# Listas padrao do ProjectAdm (quadro novo) -> reaproveitadas por renomeacao,
# para que a Referencia caia na posicao 0 sem precisar de API de reordenacao.
#
# ⚠️ O ProjectAdm MUDOU os nomes padrao: quadro novo nasce com "To Do / In
# Progress / Done" (medido em 15/08/2026, quadro 334) — antes era "A Fazer / Em
# Progresso / Concluido". Faltava "in progress": a lista do meio nao virava a
# nossa e a limpeza logo abaixo tambem a ignorava (so olha nomes DESTE mapa),
# entao o aluno ficava com uma "In Progress" orfa no quadro. As duas linguas
# ficam aqui de proposito — quadro antigo e quadro novo pelo mesmo caminho.
PADRAO_PARA_CHAVE = {
    "a fazer": "REFERENCIA", "to do": "REFERENCIA", "todo": "REFERENCIA",
    "em progresso": "INPUTS", "em andamento": "INPUTS", "fazendo": "INPUTS",
    "in progress": "INPUTS", "doing": "INPUTS",
    "concluido": "PARTES", "concluído": "PARTES", "feito": "PARTES", "done": "PARTES",
}


# ── Os 7 cartoes do Termo de Abertura (as 7 perguntas) ───────────────────────
# Mesmos titulos nas duas abordagens — de proposito: o aluno VE que as perguntas
# nao mudam. O que muda e a descricao (a forma da resposta).
# As secoes sao AS MESMAS do Termo de Abertura 5W2H (Modelos/) — o documento que o (o documento que o
# passo 1 GERA): 1 Por que · 2 O que · 3 Quem · 4 Como · 5 Quando · 6 Quanto.
# O cartao do quadro = a secao do documento = a saida do script. Uma coisa so.
#
# DUAS MUDANCAS em relacao ao template atual — ambas com fonte:
#   4.1 ABORDAGEM DE DESENVOLVIMENTO — o "Como?" comeca aqui. E a resposta a pergunta
#       "How will it be carried out?" do termo de abertura adaptativo (Guia PMBOK 8).
#       Restricoes, premissas, riscos e EAP sao o DETALHE desse como.
#   2.3 ONDE — o Guia pergunta "Where will it occur?" e o artigo /5w2h/ do Eduardo ja
#       ensina as 7 perguntas. O template atual nao tem essa secao.
# (E o 1.2 passa de "Objetivos SMART" para OKR, como no livro reestruturado.)

_CAIXA_PT = "0. Caixa de entrada — cole aqui a resposta da IA"
_CAIXA_DESC_PT = (
    "COLE AQUI a resposta da sua IA — a resposta INTEIRA, como ela veio.\n\n"
    "Cada resposta vem com rótulos em colchetes ([1.2], [2.1]...). Ao rodar o script, "
    "ele lê esta caixa, distribui cada bloco no cartão certo desta lista e limpa a "
    "caixa. Você não precisa acertar o cartão: só colar aqui.\n\n"
    "Se preferir, pode escrever direto no cartão — o script lê os dois jeitos.\n\n")

_TERMO_PREDITIVA_PT = [
    ("1.1 POR QUÊ — Justificativa do projeto",
     "A dor de hoje e o valor que o projeto gera. VOCÊ escreve este cartão — é a "
     "única coisa que a IA não pode fazer por você, porque é o seu contexto.\n"
     "3 a 6 frases: qual é a dor hoje · quem sofre com ela · o que acontece se nada "
     "for feito · o que muda quando o projeto entregar.\n\n"
     "É o começo da corrente: todos os próximos passos partem deste texto."),
    ("1.2 POR QUÊ — Objetivos e Key Results (OKR)",
     "Quantos objetivos o projeto pedir — a maioria tem mais de um. Cada objetivo diz "
     "ONDE você quer chegar (qualitativo, curto). Em CADA objetivo, de 2 a 5 Key "
     "Results, que provam que você chegou: específico, mensurável (número + unidade), "
     "realista — medindo RESULTADO, nunca tarefa.\n\n"
     "Um KR não tem responsável nem prazo — é só o resultado e a meta. Quem faz e "
     "quando entram nas entregas e nas tarefas (o cronograma e a execução), não no KR.\n\n"
     "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
     "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
     "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'"),
    ("2.1 O QUÊ — Produtos e entregas esperadas",
     "O que precisa EXISTIR para os Key Results acontecerem. Cada entrega é um "
     "resultado (substantivo), não uma atividade: 'Política de compras publicada', não "
     "'Escrever a política'.\n\n"
     "Nenhum Key Result pode ficar sem entrega: se sobrar KR sem nada que o alcance, "
     "falta entrega (ou o KR não era deste projeto).\n\n"
     "PROMPT 2: 'com base nestes objetivos, quais entregas você recomenda?'\n"
     "No passo 2 estas entregas viram a EAP."),
    ("2.2 O QUÊ — Requisitos",
     "Para cada entrega: o que ela precisa ter para ser aceita (as características "
     "solicitadas) e o critério de aceitação — como saberemos que ficou pronta.\n\n"
     "Requisito não é desejo: é condição verificável."),
    ("3. QUEM — Partes interessadas",
     "Quem recebe o valor (CLIENTES), quem faz (EQUIPE) e quem influencia, aprova, "
     "regula ou é impactado (DEMAIS). O PATROCINADOR é quem autoriza e banca — se você "
     "não sabe quem é, você ainda não tem projeto autorizado.\n\n"
     "PROMPT 3: 'com base nestas entregas, quem são as partes interessadas?'\n"
     "O detalhe de cada uma vai para a lista 'Partes Interessadas [QUEM]'."),
    ("4.1 COMO — Abordagem de desenvolvimento",
     "PREDITIVA. 'O meio usado para criar e evoluir o produto ao longo do ciclo de vida' "
     "(Guia PMBOK 8). O 'Como?' do 5W2H começa AQUI: primeiro você decide COMO vai "
     "conduzir; restrições, premissas, riscos e EAP são o detalhe desse como.\n\n"
     "Você escolheu a preditiva porque o escopo é conhecido e estável o bastante para "
     "planejar antes de executar: define-se o escopo, decompõe-se em EAP, estima-se "
     "prazo e custo, aprova-se uma baseline e controla-se a mudança contra ela.\n\n"
     "O script 01a_escolher_abordagem.py gera a sua justificativa (os 5 eixos do "
     "Guia) — e o que observar para saber quando pegar emprestado do ágil."),
    ("4.2 COMO — Restrições",
     "Os limites impostos de fora: prazo, orçamento, norma, tecnologia, contrato, "
     "pessoas disponíveis.\n\n"
     "Restrição não se negocia dentro do projeto — se contorna ou se aceita."),
    ("4.3 COMO — Premissas",
     "O que você está ASSUMINDO como verdade sem ter certeza ('o fornecedor entrega em "
     "30 dias', 'a equipe estará disponível em julho').\n\n"
     "Toda premissa que cai vira um risco. Por isso elas moram lado a lado."),
    ("4.4 COMO — Riscos iniciais",
     "Os riscos de alto nível que você já enxerga na largada — o suficiente para alguém "
     "decidir se autoriza o projeto.\n"
     "Escreva como CAUSA -> EVENTO -> IMPACTO. 'Atraso' não é risco: é o impacto.\n\n"
     "No passo 3 você aprofunda na lista 'Riscos e Premissas' (probabilidade, impacto, "
     "resposta e dono) e o script gera o Registro dos pontos de atenção."),
    ("4.5 COMO — Escopo inicial (EAP ou Product Backlog)",
     "PREDITIVA: a primeira decomposição do escopo — as entregas abertas nas grandes "
     "partes do trabalho. Aqui é visão inicial: o suficiente para dimensionar o projeto.\n\n"
     "No passo 2 ela vira a EAP completa (entregas -> pacotes de trabalho) na lista "
     "'EAP → Cronograma', e o script gera o Dicionário da EAP. No passo 4 esses mesmos "
     "pacotes ganham datas, durações e dependências — a EAP vira cronograma."),
    ("4.6 ONDE — Contexto e limites",
     "Onde o projeto acontece e até onde ele vai: área, unidade, sistema, região, "
     "presencial ou remoto — e, principalmente, o que fica de FORA.\n\n"
     "O ONDE é parte do COMO: onde o trabalho acontece depende da maneira de "
     "conduzi-lo.\n\n"
     "É a menos decisiva das sete perguntas — se você ainda não sabe, DEIXE EM BRANCO e "
     "decida no momento certo. O que vale mesmo aqui é o que fica de FORA: é a linha que "
     "mais evita briga depois.\n\n"
     "O Guia PMBOK 8 inclui esta pergunta na lista do termo de abertura ('Where will it "
     "occur?')."),
    ("5. QUANDO — Linha do tempo",
     "Os marcos que já dá para prometer. Em prazo relativo ('Semana 4', 'Mês 2') se você "
     "ainda não tem data — é preliminar de propósito.\n\n"
     "No passo 4 isto vira cronograma com datas, dependências e caminho crítico."),
    ("6. QUANTO — Custos (ordem de grandeza)",
     "Uma estimativa grosseira, com a BASE e a MARGEM ('R$ 20 a 30 mil — 2 pessoas x 3 "
     "meses; margem -25%/+50%').\n\n"
     "Dizer 'ainda não sei, e esta é a base do meu chute' é honesto. Fingir precisão é o "
     "que quebra a confiança no passo 8."),
]

_TERMO_AGIL_PT = [
    ("1.1 POR QUÊ — Justificativa do projeto",
     "A dor de hoje e o valor que o projeto gera. VOCÊ escreve este cartão — é a unica "
     "coisa que a IA nao pode fazer por voce, porque e o seu contexto.\n"
     "3 a 6 frases: qual é a dor hoje · quem sofre com ela · o que acontece se nada for "
     "feito · o que muda quando o projeto entregar.\n\n"
     "Na abordagem ágil este texto é a base da VISÃO DO PRODUTO — e o Guia PMBOK 8 diz "
     "que o ciclo de vida adaptativo começa exatamente aqui: 'define project and product "
     "visions'."),
    ("1.2 POR QUÊ — Objetivos e Key Results (OKR)",
     "A Visão responde ao POR QUÊ; o OKR a torna verificável. Quantos objetivos o "
     "projeto pedir — a maioria tem mais de um. Cada objetivo diz ONDE chegar; e cada "
     "um leva de 2 a 5 Key Results, que provam que você chegou: mensurável (número + "
     "unidade), medindo RESULTADO (não tarefa).\n\n"
     "Um KR não tem responsável nem prazo — é só o resultado e a meta. Quem faz e "
     "quando entram nas entregas e nas tarefas (o cronograma e a execução).\n\n"
     "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
     "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
     "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'\n"
     "No ágil os KRs também dizem o que a próxima Sprint precisa mover."),
    ("2.1 O QUÊ — Produtos e entregas esperadas",
     "No ágil este cartão é a sua VISÃO DO PRODUTO + as entregas.\n"
     "O que precisa EXISTIR para os Key Results acontecerem. Aqui a resposta é mais "
     "enxuta DE PROPÓSITO: uma visão clara + as entregas que você já enxerga hoje.\n\n"
     "Guia PMBOK 8: 'uma visão clara é estabelecida no início e os requisitos conhecidos "
     "são elaborados progressivamente... na forma do product backlog'.\n\n"
     "PROMPT 2: 'com base nestes objetivos, quais entregas você recomenda?'"),
    ("2.2 O QUÊ — Requisitos",
     "No ágil, o requisito de cada item do backlog vira o seu CRITÉRIO DE ACEITAÇÃO — "
     "como você saberá que aquele item está pronto.\n\n"
     "Aqui, no cartão 2.2, ficam os requisitos que valem para o produto TODO (o que não "
     "cabe num item só): desempenho, segurança, norma, compatibilidade. Eles alimentam a "
     "Definição de Pronto (DoD) no passo 5."),
    ("3. QUEM — Partes interessadas",
     "Quem recebe o valor (CLIENTES/usuários), quem faz (EQUIPE) e quem influencia, aprova "
     "ou é impactado (DEMAIS). O PATROCINADOR autoriza e banca.\n\n"
     "Identifique também quem faz o papel de PRODUCT OWNER (quem decide a prioridade do "
     "backlog) e quem é o usuário real das histórias — sem ele, a história é chute.\n\n"
     "PROMPT 3: 'com base nestas entregas, quem são as partes interessadas?'"),
    ("4.1 COMO — Abordagem de desenvolvimento",
     "ÁGIL — o Guia PMBOK 8 chama esta família de ADAPTATIVA; o Scrum é o método mais usado "
     "dela. 'O meio usado para criar e evoluir o produto ao longo do ciclo de vida'. O "
     "'Como?' do 5W2H começa AQUI: primeiro você decide COMO vai conduzir.\n\n"
     "Você escolheu a ágil porque os requisitos não estão claros o bastante para planejar "
     "tudo antes — e o feedback frequente vale mais do que a previsibilidade: entrega-se em "
     "incrementos curtos, aprende-se e reprioriza-se.\n\n"
     "O script 01a_escolher_abordagem.py gera a sua justificativa (os 5 eixos do Guia) — "
     "e o que observar para saber quando pegar emprestado do preditivo."),
    ("4.2 COMO — Restrições",
     "Os limites impostos de fora: data de mercado, orçamento, norma, tecnologia, tamanho "
     "do time.\n\n"
     "No ágil a restrição típica é a DATA: ela costuma ser fixa — e é o escopo que flutua "
     "para caber dentro dela."),
    ("4.3 COMO — Premissas",
     "O que você está ASSUMINDO como verdade sem ter certeza ('o usuário vai querer isto', "
     "'a API do parceiro aguenta').\n\n"
     "No ágil a premissa tem um destino melhor do que uma lista: ela vira HIPÓTESE — e a "
     "Sprint 1 existe para testá-la."),
    ("4.4 COMO — Riscos iniciais",
     "Os riscos de alto nível que você já enxerga na largada. Escreva como CAUSA -> EVENTO "
     "-> IMPACTO.\n\n"
     "No ágil o risco não mora num documento paralelo: ele entra no BACKLOG. O que é "
     "incerteza de conhecimento ('não sei se funciona') vira um SPIKE com tempo fixo; o que "
     "é ameaça de verdade sobe na prioridade — aprender cedo o que mata o projeto é a melhor "
     "resposta a risco que existe."),
    ("4.5 COMO — Escopo inicial (EAP ou Product Backlog)",
     "ÁGIL: os primeiros itens de valor que você já enxerga, em ordem de prioridade — o "
     "Product Backlog inicial. Aqui é visão inicial: o suficiente para dimensionar o "
     "produto e começar.\n\n"
     "No passo 2 ele vira o Product Backlog completo (histórias INVEST com critérios de "
     "aceitação e pontos) na lista 'Product Backlog', e o script gera a planilha."),
    ("4.6 ONDE — Contexto e limites",
     "Onde o produto vive e até onde ele vai: área, sistema, público — e o que fica de "
     "FORA (nem tudo que é possível está na visão).\n\n"
     "O ONDE é parte do COMO: onde o trabalho acontece depende da maneira de conduzi-lo "
     "(um time que entrega em incrementos curtos e recebe feedback do usuário real "
     "trabalha em outro 'onde' de um que entrega tudo no fim).\n\n"
     "É a menos decisiva das sete perguntas — se você ainda não sabe, DEIXE EM BRANCO e "
     "decida no momento certo. O que vale mesmo aqui é o que fica de FORA: no ágil o "
     "limite não congela o escopo; ele protege o foco.\n\n"
     "O Guia PMBOK 8 inclui esta pergunta no termo de abertura adaptativo ('Where will "
     "it occur?')."),
    ("5. QUANDO — Linha do tempo",
     "O horizonte: quando você quer a primeira entrega de valor (o primeiro incremento útil) "
     "e quais marcos já estão prometidos.\n\n"
     "No ágil a data costuma ser fixa e o escopo é que flutua. No passo 4 isto vira um "
     "roadmap de Sprints."),
    ("6. QUANTO — Custos (ordem de grandeza)",
     "Uma estimativa grosseira, com a BASE e a MARGEM.\n\n"
     "No ágil o custo por Sprint tende a ser FIXO (o time é o mesmo, a Sprint tem o mesmo "
     "tamanho) — o que varia é quanto valor cabe dentro dele. O passo 4 detalha isso na "
     "lista 'Orçamento'."),
]


# ── ETAPA 1: os mesmos cartoes, com o texto que NAO depende da abordagem ─────
# O aluno abre o quadro no comeco da leitura e responde 1.1, 1.2, 2.1, 2.2 e 3
# (por que · o que · quem). Dali para baixo, TUDO nasce TRANCADO — o bloco 4 (o COMO,
# onde mora tambem o ONDE) e os cartoes 5 (QUANDO) e 6 (QUANTO).
#
# Isto nao e enfeite, e a ordem correta de pensar, e ela tem duas metades:
#   - o COMO vem DEPOIS do por que / o que / quem, porque os fatores que o Guia PMBOK 8
#     (secao 4.3) manda usar para ESCOLHER a abordagem sao os da entrega, do projeto e
#     da organizacao. Escolher antes seria chute;
#   - o QUANDO, o QUANTO e o ONDE vem DEPOIS do COMO, porque e ele que define como os
#     tres se calculam. "Quando fica pronto?" tem respostas diferentes se a resposta e
#     um marco de entrega ou o fim de uma Sprint.
_TRANCADO_PT = (
    "🔒 Este cartão faz parte do bloco COMO — e o COMO você ainda não escolheu.\n\n"
    "O 'Como?' do 5W2H começa pela ABORDAGEM DE DESENVOLVIMENTO: 'o meio usado para "
    "criar e evoluir o produto ao longo do ciclo de vida' (Guia PMBOK 8). Restrições, "
    "premissas, riscos, escopo inicial e o ONDE são o DETALHE desse como — e mudam de "
    "forma conforme a abordagem.\n\n"
    "Responda primeiro os cartões 1.1 a 3 (por quê · o quê · quem). Depois rode o "
    "script 01a_escolher_abordagem.py (cartão 1.4 da lista Referências): são 5 "
    "perguntas — os eixos do Guia PMBOK 8 — e ele recomenda, você decide.\n\n"
    "Quando você escolher, o Passo 1 destranca este bloco e monta o resto do seu "
    "quadro na sua abordagem.")

# 5 e 6 tambem nascem trancados — mas por um motivo proprio, que vale a pena o aluno
# ler: o COMO e que define COMO SE CALCULA o quando e o quanto.
_TRANCADO_QUANDO_QUANTO_PT = (
    "🔒 Este cartão abre depois que você escolher a ABORDAGEM DE DESENVOLVIMENTO "
    "(cartão 4.1).\n\n"
    "Por quê? Porque é o COMO que define como se calcula o QUANDO e o QUANTO:\n"
    "· PREDITIVA: o marco é a entrega concluída; o custo é o do projeto inteiro, "
    "estimado a partir do escopo.\n"
    "· ÁGIL: o marco é o primeiro incremento útil e a cadência; a data tende a ser "
    "fixa e o escopo a flutuar — e o custo é o do time por Sprint.\n\n"
    "Pedir uma data antes de saber isso é pedir um número sem saber o que ele "
    "significa.\n\n"
    "Rode o script 01a_escolher_abordagem.py (cartão 1.4 da lista Referências): 5 "
    "perguntas, ele recomenda, você decide.")

_TERMO_BASE_PT = [
    ("1.1 POR QUÊ — Justificativa do projeto",
     "A dor de hoje e o valor que o projeto gera. VOCÊ escreve este cartão — é a "
     "única coisa que a IA não pode fazer por você, porque é o seu contexto.\n"
     "3 a 6 frases: qual é a dor hoje · quem sofre com ela · o que acontece se nada "
     "for feito · o que muda quando o projeto entregar.\n\n"
     "É o começo da corrente: todos os próximos passos partem deste texto.\n"
     "Travou? Cartão 90 da lista Referências: a IA te faz perguntas (o texto continua "
     "seu)."),
    ("1.2 POR QUÊ — Objetivos e Key Results (OKR)",
     "Quantos objetivos o projeto pedir — a maioria tem mais de um. Cada objetivo diz "
     "ONDE você quer chegar (qualitativo, curto). Em CADA objetivo, de 2 a 5 Key "
     "Results, que provam que você chegou: específico, mensurável (número + unidade), "
     "realista — medindo RESULTADO, nunca tarefa.\n\n"
     "Um KR não tem responsável nem prazo — é só o resultado e a meta. Quem faz e "
     "quando entram nas entregas e nas tarefas (o cronograma e a execução), não no KR.\n\n"
     "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
     "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
     "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'"),
    ("2.1 O QUÊ — Produtos e entregas esperadas",
     "O que precisa EXISTIR para os Key Results acontecerem. Cada entrega é um "
     "resultado (substantivo), não uma atividade: 'Política de compras publicada', não "
     "'Escrever a política'.\n\n"
     "Nenhum Key Result pode ficar sem entrega: se sobrar KR sem nada que o alcance, "
     "falta entrega (ou o KR não era deste projeto).\n\n"
     "PROMPT 2: 'com base nestes objetivos, quais entregas você recomenda?'"),
    ("2.2 O QUÊ — Requisitos",
     "Para cada entrega: o que ela precisa ter para ser aceita (as características "
     "solicitadas) e o critério de aceitação — como saberemos que ficou pronta.\n\n"
     "Requisito não é desejo: é condição verificável."),
    ("3. QUEM — Partes interessadas",
     "Quem recebe o valor (CLIENTES), quem faz (EQUIPE) e quem influencia, aprova, "
     "regula ou é impactado (DEMAIS). O PATROCINADOR é quem autoriza e banca — se você "
     "não sabe quem é, você ainda não tem projeto autorizado.\n\n"
     "PROMPT 3: 'com base nestas entregas, quem são as partes interessadas?'\n"
     "O detalhe de cada uma vai para a lista 'Partes Interessadas [QUEM]'."),
    ("4.1 COMO — Abordagem de desenvolvimento", _TRANCADO_PT),
    ("4.2 COMO — Restrições", _TRANCADO_PT),
    ("4.3 COMO — Premissas", _TRANCADO_PT),
    ("4.4 COMO — Riscos iniciais", _TRANCADO_PT),
    ("4.5 COMO — Escopo inicial (EAP ou Product Backlog)", _TRANCADO_PT),
    ("4.6 ONDE — Contexto e limites", _TRANCADO_PT),
    ("5. QUANDO — Linha do tempo", _TRANCADO_QUANDO_QUANTO_PT),
    ("6. QUANTO — Custos (ordem de grandeza)", _TRANCADO_QUANDO_QUANTO_PT),
]

# Cartoes que mudaram de titulo (um quadro montado por uma versao anterior do curso
# tem o titulo antigo). Renomeamos — nunca criamos um cartao novo por cima, senao o
# aluno perderia o texto que escreveu.
LEGADO = {
    "2.1 O QUÊ — Visão do Produto e entregas esperadas": "2.1 O QUÊ — Produtos e entregas esperadas",
    "4.5 COMO — EAP (visão inicial)": "4.5 COMO — Escopo inicial (EAP ou Product Backlog)",
    "4.5 COMO — Product Backlog (visão inicial)": "4.5 COMO — Escopo inicial (EAP ou Product Backlog)",
    "1.2 POR QUÊ — Objetivo e Key Results (OKR)": "1.2 POR QUÊ — Objetivos e Key Results (OKR)",
    # O ONDE virou parte do COMO (fim do bloco 4). Renomear — nunca recriar: o cartao
    # antigo pode ter a resposta do aluno dentro.
    "2.3 ONDE — Contexto e limites": "4.6 ONDE — Contexto e limites",
}

# Texto-guia APOSENTADO: o que ja escrevemos neste cartao em versoes anteriores do
# curso. Sem esta lista, o quadro de quem comecou antes trava: garantir_termo veria o
# guia velho, nao o reconheceria como NOSSO e o trataria como resposta do ALUNO —
# nunca mais atualizaria o cartao (e ler_inputs leria o guia como se fosse a resposta).
# REGRA: mudou um texto de _TERMO_*? O texto ANTIGO desce para ca, na mesma edicao.
_APOSENTADOS = {
    "1.2 POR QUÊ — Objetivos e Key Results (OKR)": (
        # v1 (base/preditiva) — um objetivo so
        "O objetivo diz ONDE você quer chegar (qualitativo, curto). Os Key Results provam "
        "que chegou: específico, mensurável (número + unidade), com responsável, realista "
        "e com prazo — medindo RESULTADO, nunca tarefa.\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'",
        # v1 (ágil)
        "A Visão responde ao POR QUÊ; o OKR a torna verificável. O objetivo diz ONDE chegar; "
        "cada Key Result prova que chegou: mensurável, com responsável e prazo, medindo "
        "RESULTADO (nao tarefa).\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'\n"
        "No ágil os KRs também dizem o que a próxima Sprint precisa mover.",
        # v2 (base/preditiva) — N objetivos, mas ainda pedia responsável e prazo no KR
        "Quantos objetivos o projeto pedir — a maioria tem mais de um. Cada objetivo diz "
        "ONDE você quer chegar (qualitativo, curto). Em CADA objetivo, de 2 a 4 Key "
        "Results, que provam que você chegou: específico, mensurável (número + unidade), "
        "com responsável, realista e com prazo — medindo RESULTADO, nunca tarefa.\n\n"
        "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
        "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'",
        # v2 (ágil) — idem, ainda pedia responsável e prazo no KR
        "A Visão responde ao POR QUÊ; o OKR a torna verificável. Quantos objetivos o "
        "projeto pedir — a maioria tem mais de um. Cada objetivo diz ONDE chegar; e cada "
        "um leva de 2 a 4 Key Results, que provam que você chegou: mensurável, com "
        "responsável e prazo, medindo RESULTADO (não tarefa).\n\n"
        "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
        "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'\n"
        "No ágil os KRs também dizem o que a próxima Sprint precisa mover.",
        # v3 (base/preditiva) — 2 a 4 KRs; responsável e prazo já num parágrafo à parte ("acrescenta DEPOIS")
        "Quantos objetivos o projeto pedir — a maioria tem mais de um. Cada objetivo diz "
        "ONDE você quer chegar (qualitativo, curto). Em CADA objetivo, de 2 a 4 Key "
        "Results, que provam que você chegou: específico, mensurável (número + unidade), "
        "realista — medindo RESULTADO, nunca tarefa.\n\n"
        "O responsável e o prazo de cada KR você acrescenta DEPOIS: o responsável quando "
        "definir QUEM (as partes interessadas), e o prazo na linha do tempo (QUANDO). "
        "Aqui, foque no resultado e na meta.\n\n"
        "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
        "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'",
        # v3 (ágil)
        "A Visão responde ao POR QUÊ; o OKR a torna verificável. Quantos objetivos o "
        "projeto pedir — a maioria tem mais de um. Cada objetivo diz ONDE chegar; e cada "
        "um leva de 2 a 4 Key Results, que provam que você chegou: mensurável (número + "
        "unidade), medindo RESULTADO (não tarefa).\n\n"
        "O responsável e o prazo de cada KR você acrescenta DEPOIS: o responsável quando "
        "definir QUEM (as partes interessadas), e o prazo na linha do tempo (QUANDO).\n\n"
        "Numere-os: KR 1.1, KR 1.2 (do objetivo 1), KR 2.1... — é por esse número que as "
        "entregas do cartão 2.1 dizem a qual Key Result elas servem.\n\n"
        "PROMPT 1: 'com base nesta justificativa, quais objetivos você recomenda?'",
    ),
    "2.1 O QUÊ — Produtos e entregas esperadas": (
        # v1 (preditiva) — sem a regra "nenhum KR sem entrega"
        "O que precisa EXISTIR para os Key Results acontecerem. Cada entrega é um "
        "resultado (substantivo), não uma atividade: 'Política de compras publicada', não "
        "'Escrever a política'.\n\n"
        "PROMPT 2: 'com base nestes objetivos, quais entregas você recomenda?'\n"
        "No passo 2 estas entregas viram a EAP.",
        # v1 (base)
        "O que precisa EXISTIR para os Key Results acontecerem. Cada entrega é um "
        "resultado (substantivo), não uma atividade: 'Política de compras publicada', não "
        "'Escrever a política'.\n\n"
        "PROMPT 2: 'com base nestes objetivos, quais entregas você recomenda?'",
    ),
    "2.2 O QUÊ — Requisitos": (
        # v1 (ágil) — a historia de usuario vinha com <angulos>, que o cartao comia como
        # tag HTML e devolvia como lixo ("</quem></o>") no fim da descricao.
        "No ágil o requisito vive na história: 'Como <quem>, quero <o que>, para <benefício>' "
        "+ critérios de aceitação verificáveis.\n\n"
        "Aqui ficam os requisitos que valem para o produto TODO (o que não cabe numa história "
        "só): desempenho, segurança, norma, compatibilidade. Eles alimentam a Definição de "
        "Pronto (DoD) no passo 5.",
        # v2 (ágil) — {chaves}; ainda dizia "o requisito vive na história", antes de a
        # história de usuário virar OPCIONAL (o padrão passou a ser RESULTADO, 2026-07-24).
        "No ágil o requisito vive na história: 'Como {quem}, quero {o quê}, para {benefício}' "
        "+ critérios de aceitação verificáveis.\n\n"
        "Aqui ficam os requisitos que valem para o produto TODO (o que não cabe numa história "
        "só): desempenho, segurança, norma, compatibilidade. Eles alimentam a Definição de "
        "Pronto (DoD) no passo 5.",
    ),
    # O ONDE era o cartao 2.3 (dentro do O QUE) e virou 4.6 (dentro do COMO). O LEGADO
    # renomeia o cartao; estes sao os guias que ele tinha quando ainda era 2.3.
    "4.6 ONDE — Contexto e limites": (
        # v1 (base/preditiva)
        "Onde o projeto acontece e até onde ele vai: área, unidade, sistema, região — e, "
        "principalmente, o que fica de FORA.\n\n"
        "É a pergunta que mais se esquece e a que mais evita briga depois. O Guia PMBOK 8 "
        "a inclui na lista do termo de abertura ('Where will it occur?').",
        # v1 (ágil)
        "Onde o produto vive e até onde ele vai: área, sistema, público — e o que fica de "
        "FORA (nem tudo que é possível está na visão).\n\n"
        "No ágil o limite não congela o escopo; ele protege o foco. O Guia PMBOK 8 inclui esta "
        "pergunta no termo de abertura adaptativo ('Where will it occur?').",
    ),
    # 5 e 6 eram abertos na etapa 1; agora nascem trancados (o COMO e que define como se
    # calcula o quando e o quanto). Estes sao os guias abertos que eles tinham.
    "5. QUANDO — Linha do tempo": (
        "Os marcos que já dá para prometer. Em prazo relativo ('Semana 4', 'Mês 2') se você "
        "ainda não tem data — é preliminar de propósito.\n\n"
        "No passo 4 isto vira cronograma.",
    ),
    "6. QUANTO — Custos (ordem de grandeza)": (
        "Uma estimativa grosseira, com a BASE e a MARGEM ('R$ 20 a 30 mil — 2 pessoas x 3 "
        "meses; margem -25%/+50%').\n\n"
        "Dizer 'ainda não sei, e esta é a base do meu chute' é honesto. Fingir precisão é o "
        "que quebra a confiança no passo 8.",
    ),
    # v1 (preditiva) — dizia "gravidade" onde a 2a entrada do risco e' o IMPACTO (a
    # prioridade = probabilidade x impacto sai calculada). Corrigido 2026-07-19.
    "4.4 COMO — Riscos iniciais": (
        "Os riscos de alto nível que você já enxerga na largada — o suficiente para alguém "
        "decidir se autoriza o projeto.\n"
        "Escreva como CAUSA -> EVENTO -> IMPACTO. 'Atraso' não é risco: é o impacto.\n\n"
        "No passo 3 você aprofunda na lista 'Riscos e Premissas' (probabilidade, gravidade, "
        "resposta e dono) e o script gera o Registro dos pontos de atenção.",
    ),
}


def cartoes_do_termo(abordagem=None):
    """Os 14 cartoes da lista INPUTS (a caixa de entrada + as 7 perguntas do 5W2H).

    Os TITULOS sao sempre os mesmos — a pergunta nao muda. O que muda e o texto-guia:
      abordagem=None (ETAPA 1) -> texto neutro; bloco COMO (4.x) trancado.
      abordagem escolhida      -> o texto na forma daquela abordagem.
    """
    if not abordagem:
        return [(CAIXA, _CAIXA_DESC)] + _TERMO_BASE
    a = normalizar_abordagem(abordagem)
    base = _TERMO_PREDITIVA if a == "PREDITIVA" else _TERMO_AGIL
    return [(CAIXA, _CAIXA_DESC)] + base


def textos_de_fabrica(titulo):
    """TODO texto-guia que o curso ja escreveu neste cartao — nas 3 formas de hoje
    (neutro, preditivo, agil) E nas versoes anteriores do curso (_APOSENTADOS).

    Serve para responder a unica pergunta que importa antes de escrever num cartao:
    'isto aqui e texto NOSSO ou e o que o ALUNO escreveu?'. Texto nosso pode ser
    atualizado; o do aluno, jamais.
    """
    textos = {t.strip() for t in _APOSENTADOS.get(titulo, ())}
    for lista in (_TERMO_BASE, _TERMO_PREDITIVA, _TERMO_AGIL):
        for t, d in lista:
            if t == titulo:
                textos.add(d.strip())
    # O texto-guia da CAIXA conta como NOSSO em QUALQUER cartao do termo. Ate 2026-08-04
    # o pacote EN gravava a propria Caixa dentro do cartao 1.1 (o guarda que reconhecia o
    # texto-guia era o portugues "COLE AQUI" — ver caixa_no_guia). Sem isto, o quadro de
    # quem rodou aquela versao segue para sempre dizendo "[OK] 1.1 Justificativa" para uma
    # justificativa que ninguem escreveu. Texto nosso e' nosso: o Passo 1 o troca pelo
    # guia certo e o painel volta a cobrar o cartao.
    textos |= set(_CAIXA_FABRICA)
    textos |= set(_PLACEHOLDERS_JUSTIFICATIVA)
    return textos


# ── Guia das demais listas (1 cartao por lista, para nenhuma lista nascer "vazia
#    e misteriosa"). Titulo unico por lista para o upsert ser idempotente.
_GUIA_TITULO_PT = "Guia desta lista"
_GUIAS_ESCOPO_PT = {
    "PREDITIVA": (
        "O QUÊ, no nível em que o trabalho acontece: a EAP (Estrutura Analítica do "
        "Projeto).\n"
        "Cada entrega do cartão 2.1 se abre aqui em PACOTES DE TRABALHO — o nível mais "
        "baixo, aquele que tem custo, duração e critério de aceitação.\n"
        "Regra: pacote é um RESULTADO (substantivo), não uma atividade. 'Política de "
        "compras publicada', não 'escrever a política'.\n"
        "Preenchida pelo passo 2 — e é dela que saem o cronograma e o orçamento."),
    "AGIL": (
        "O QUÊ, no nível em que o trabalho acontece: o PRODUCT BACKLOG.\n"
        "Guia PMBOK 8: 'uma lista dinâmica e priorizada de itens de trabalho e "
        "funcionalidades', focada em RESULTADOS ORIENTADOS POR VALOR. Cada entrega do "
        "cartão 2.1 vira aqui itens de valor, sempre ordenados por valor: o mais "
        "importante no topo.\n"
        "Cada item é um RESULTADO que passa a existir (ex.: 'Rotina de força 2x/semana "
        "em prática'). A história de usuário — 'Como <quem>, quero <o quê>, para "
        "<valor>' — é OPCIONAL (você escolhe a forma no Passo 1); não é obrigatória "
        "para ser ágil.\n"
        "O backlog não nasce completo nem fica pronto: ele é refinado a cada Sprint "
        "(elaboração progressiva).\n"
        "Preenchida pelo passo 2."),
}
_GUIAS_PT = {
    "PARTES": (
        "QUEM. Um cartão por parte interessada, em três grupos: CLIENTES (quem "
        "recebe o valor), EQUIPE (quem faz) e DEMAIS (quem influencia, aprova, "
        "regula ou é impactado).\n"
        "Passo 1: quem são e o que esperam. Passo 6: como engajar cada uma — de "
        "onde ela está (resistente/neutro/apoiador/líder) para onde precisa estar.\n"
        "Projeto não morre de cronograma. Morre de gente que não foi ouvida."),
    "RISCOS": (
        "O que pode dar errado — e o que pode dar muito certo (oportunidade).\n"
        "Escreva o risco como CAUSA -> EVENTO -> IMPACTO ('porque depende de um "
        "fornecedor único, o contrato pode atrasar, empurrando a entrega X'). "
        "'Atraso' não é risco: é o impacto.\n"
        "Aqui também moram as PREMISSAS (o que você assume como verdade sem ter "
        "certeza — se cair, vira risco) e as RESTRIÇÕES (os limites impostos).\n"
        "Preenchida pelo passo 3."),
    "ORCAMENTO": (
        "QUANTO. Passo 1: a ordem de grandeza (chute honesto, com margem). Passo 4: "
        "o orçamento por item, com o mês em que o dinheiro sai e uma reserva de "
        "contingência proporcional aos riscos.\n"
        "Um cartão por item de custo."),
    "EXECUCAO": (
        "O trabalho que está acontecendo AGORA. Puxe para cá o que está em execução "
        "e mova para a lista de pronto quando terminar — é isso que alimenta o "
        "Status Report (passo 8): planejado x realizado sai daqui.\n"
        "Regra que separa projeto de lista de desejos: nada entra aqui sem "
        "responsável e data."),
    "PRONTO": (
        "O que está PRONTO de verdade — pronto segundo a Definição de Pronto que "
        "você escreveu no passo 5, não 'pronto na minha cabeça'.\n"
        "Cada cartão que chega aqui é valor entregue. No passo 10 você compara o que "
        "chegou com os Key Results do passo 1."),
    "MUDANCAS": (
        "Tudo muda. Projeto sério não é o que não muda: é o que sabe o que a mudança "
        "custa.\n"
        "Um cartão por solicitação de mudança: o que pedem, por quê, e o impacto em "
        "escopo, prazo, custo, qualidade, riscos e partes interessadas (passo 9). "
        "Aprovou? A baseline muda — e o cartão registra a nova.\n"
        "Na abordagem ágil, a mudança entra como repriorização do backlog: o que "
        "sobe, e o que desce para caber."),
    "CONCLUIDO": (
        "A sua trilha no curso: cada passo concluído vira um cartão aqui, com a data. "
        "É a sua evidência de progresso (e a base do seu certificado).\n"
        "Não mova cartões do projeto para cá — esta lista é do CURSO."),
}


def guia_da_lista(chave, abordagem=None):
    """Texto do cartao 'Guia desta lista' (ou None se a lista nao tem guia).

    A lista do escopo e a unica cujo guia MUDA com a abordagem — porque ela e a
    abordagem: EAP (preditiva) ou Product Backlog (agil)."""
    if chave == "ESCOPO":
        return _GUIAS_ESCOPO.get(normalizar_abordagem(abordagem)) if abordagem else None
    return _GUIAS.get(chave)


def e_guia(titulo):
    """Este cartao e' o "Guia desta lista" que o CURSO cria — em qualquer idioma?

    Toda lista de trabalho nasce com um cartao-guia para nenhuma lista ficar "vazia e
    misteriosa". Ele nao e' trabalho: nao e' entrega, nao e' risco, nao e' parte
    interessada — e por isso todo lugar que CONTA ou LISTA os cartoes de uma lista
    precisa exclui-lo.

    Ate 04/08/2026 cada um desses lugares (dezenove, em oito arquivos) comparava o titulo
    com a frase EM PORTUGUES, escrita a mao. Num quadro em ingles o cartao se chama
    "Guide to this list": nenhum daqueles testes casava, e o guia entrava na conta como
    se fosse trabalho do aluno — uma parte interessada a mais, um pacote a mais na EAP,
    um risco sem avaliacao para a auditoria cobrar. O mesmo defeito ja' tinha sido
    encontrado uma vez, no reset (que APAGAVA o guia de um quadro EN por nao reconhece-lo);
    a correcao de la' virou o PROTEGIDOS_PARTES, mas os outros vinte lugares ficaram.

    Agora a pergunta se faz num lugar so', contra os titulos de TODOS os idiomas — um
    quadro montado em PT e aberto pelo pacote EN continua sendo reconhecido.
    """
    t = (titulo or "").strip()
    return bool(t) and any(t.startswith(g) for g in _GUIA_TITULOS)


# PT e EN na mesma tupla, como o _PLACEHOLDERS_PROJETO: quem cria o cabecalho de grupo
# e' o 01 (T("EQUIPE — quem faz")), entao num pacote em ingles o cartao se chama
# "TEAM — who does the work". Ate 05/08/2026 os dois lugares que procuravam o grupo
# comparavam com "EQUIPE" escrito a mao — num quadro em ingles NENHUM casava, e o
# [INPUT EQUIPE] saia SEMPRE vazio. Consequencia dupla, e nenhuma delas se anunciava:
# o passo achava que faltava responder o quadro (entao NAO copiava o prompt para a area
# de transferencia — Eduardo, 05/08, nos passos 3 e 4), e a IA recebia "(ainda em branco
# no meu quadro)" no lugar da lista fechada de responsaveis — justamente o que essa
# lista existe para impedir. Um quadro montado em PT e aberto pelo pacote EN (e o
# contrario) continua sendo reconhecido.
_EQUIPE_TITULOS = ("EQUIPE", "TEAM")


def e_grupo_equipe(titulo):
    """Este cartao e' o cabecalho de grupo "EQUIPE — quem faz" da lista Partes?

    Em qualquer idioma do pacote. A comparacao e' case-SENSITIVE de proposito, como em
    _partes_de_trabalho (auditoria.py) e no PROTEGIDOS_PARTES: os cabecalhos de grupo sao
    MAIUSCULOS, entao uma parte interessada real de nome misto ("Equipe de suporte") nao
    e' confundida com o cabecalho. O codigo antigo fazia .upper() antes de comparar — e
    com isso um `next()` podia devolver a parte no lugar do grupo (o bug do board 270)."""
    t = (titulo or "").strip()
    return bool(t) and any(t.startswith(p) for p in _EQUIPE_TITULOS)


def palavras_do_titulo(titulo):
    """As palavras do titulo, sem acento, sem pontuacao e sem ordem — um conjunto."""
    limpo = _re.sub(r"[^0-9a-z]+", " ", entradas._sem_acento(titulo or "").lower())
    return set(limpo.split())


def parte_ja_existe(titulo, existentes):
    """O titulo do cartao que JA representa esta parte interessada, ou None.

    O casamento do Passo 1 era o texto EXATO, e a cada nova rodada a IA reescreve a mesma
    pessoa com as palavras noutra ordem: no quadro 326, "Henry — infraestrutura e
    ProjectAdm" ganhou um gemeo "Henry — ProjectAdm e infraestrutura", vazio, e foi o
    gemeo que o Passo 1 passou a perguntar — a conta que o aluno tinha ligado estava no
    OUTRO cartao. Mesmas palavras = mesma parte, seja qual for a ordem.

    Reescrita de verdade (palavra nova) NAO e' fundida aqui, de proposito: fundir a mais
    perde uma parte interessada, e' pior do que duplicar. Para essas fica `parecidas_com`,
    que AVISA e deixa a decisao com o aluno — a mesma regra do `casar_pessoa`: tolerante
    serve para avisar, nunca para decidir.
    """
    if titulo in existentes:
        return titulo
    alvo = palavras_do_titulo(titulo)
    if not alvo:
        return None
    return next((t for t in existentes if palavras_do_titulo(t) == alvo), None)


def parecidas_com(titulo, existentes):
    """Os cartoes que PARECEM ser a mesma parte, para o aluno decidir (nunca o script).

    So' compara palavras de 4 letras ou mais: "de", "e", "do" aproximam qualquer coisa.
    """
    grandes = {p for p in palavras_do_titulo(titulo) if len(p) >= 4}
    if len(grandes) < 2:
        return []
    achados = []
    for t in existentes:
        outras = {p for p in palavras_do_titulo(t) if len(p) >= 4}
        comuns = grandes & outras
        if len(comuns) >= 2 and len(comuns) / min(len(grandes), len(outras)) >= 0.6:
            achados.append(t)
    return achados


# ── Construcao idempotente do quadro ─────────────────────────────────────────
def _vazia(lid):
    try:
        return len(list_cards(lid)) == 0
    except Exception:
        return False   # na duvida, NAO mexe


def garantir_nome_do_quadro(log=print):
    """O quadro nasce com o nome do curso (o bootstrap cria assim, antes de o aluno
    saber qual e o projeto dele). Quando ele escreve o PROJETO no .env, o quadro
    passa a se chamar como o projeto — o quadro E o projeto. Idempotente."""
    from pa_client import board_name, rename_board
    c = cfg()
    nome = (c.get("PROJECT", "") or "").strip()
    if not nome or nome in _PLACEHOLDERS_PROJETO:
        return
    try:
        if (board_name() or "").strip() != nome:
            rename_board(c["BOARD_ID"], nome)
            log(Tf('  [quadro]     renomeado para "{0}"', nome))
    except Exception as e:                       # nunca derruba o passo por causa do nome
        log(Tf("  [aviso] nao consegui renomear o quadro ({0}) — segue o jogo.", e))


# PT e EN — ver o comentario em 01_iniciar_projeto.py. Fonte: course.json (env_pt / env_en).
_PLACEHOLDERS_PROJETO = ("ESCREVA O NOME DO SEU PROJETO AQUI",   # PT
                         "WRITE YOUR PROJECT NAME HERE")         # EN


def _tem_cartoes(l):
    """Esta lista tem cartoes? Usa o total que vem na propria listagem; so' vai a' API
    quando o payload nao traz o campo (servidor antigo)."""
    total = l.get('total_cartoes')
    if total is not None:
        try:
            return int(total) > 0
        except (TypeError, ValueError):
            pass
    return not _vazia(l['id'])


def _preferida(cands, titulo):
    """Entre varias listas que servem para a MESMA chave, qual e' a do aluno.

    A ordem e' deliberada: (1) a que TEM CARTOES — o trabalho dele vale mais que o nome;
    (2) a que ja' se chama como o alvo; (3) a mais antiga (menor id), que e' a que ele
    vem usando desde o comeco. Sem esta regra, um quadro com a lista certa em ingles e
    uma sobra vazia em portugues podia acabar apontando para a sobra."""
    if len(cands) <= 1:                          # o caso normal: nem pergunta a' API
        return cands[0] if cands else None
    def chave_de_ordem(l):
        try:
            ident = int(l['id'])
        except (TypeError, ValueError):
            ident = 0
        return (0 if _tem_cartoes(l) else 1,
                0 if (l.get('nome') or '').strip() == titulo else 1,
                ident)
    return sorted(cands, key=chave_de_ordem)[0]


def garantir_listas(abordagem=None, log=print):
    """Deixa o quadro com as listas da etapa (acha as suas em QUALQUER idioma, renomeia
    as padrao, cria o que falta, remove sobras VAZIAS, preserva qualquer lista com
    cartoes). Salva LIST_* no .env. Idempotente: rodar de novo nao duplica nada.

    abordagem=None -> ETAPA 1 (so as 3 listas que nao dependem da abordagem).
    abordagem escolhida -> ETAPA 2 (o quadro completo; as 3 da etapa 1 sao reusadas).

    ACHAR ANTES DE CRIAR (ver "OS NOMES QUE UMA LISTA NOSSA PODE TER", acima). Criar uma
    lista e' a ULTIMA saida, nao a primeira: primeiro procuramos a lista do aluno por
    todos os nomes que ela pode ter — nos dois idiomas e nas versoes anteriores do curso.

    RENOMEAR O QUADRO DE ALGUEM PEDE CERTEZA. Trazer um quadro montado em portugues para
    um pacote em ingles e' migracao, e e' o certo a fazer — desde que o idioma tenha sido
    ESCOLHIDO (o .env do aluno, ou um override explicito). Quando ninguem escolheu
    (idioma.escolhido() == False, ex.: pasta sem .env legivel), o quadro manda: achamos
    as listas pelos nomes que elas ja' tem e nao renomeamos nada. Assim um palpite nosso
    nunca reescreve o quadro de ninguem.

    Devolve {chave: list_id}.
    """
    import idioma
    garantir_nome_do_quadro(log)
    alvos = listas_do_quadro(abordagem)
    listas = list_lists()                        # UMA leitura; mantida a par das escritas
    do_quadro = idioma_do_quadro(listas, abordagem)
    pode_renomear = idioma.escolhido() or do_quadro in (None, idioma.lang())
    if not pode_renomear:
        log(Tf("  [aviso] este quadro foi montado em {0} e ninguém escolheu o idioma desta "
               "execução — uso as listas como elas estão, sem renomear nada.", do_quadro))
    ids, reivindicados = {}, set()
    for chave, titulo in alvos:
        nomes = [n.strip().lower() for n in nomes_conhecidos(chave, abordagem)]
        cands = [l for l in listas
                 if (l.get('nome') or '').strip().lower() in nomes
                 and str(l['id']) not in reivindicados]
        escolhida = _preferida(cands, titulo)
        if escolhida is None:
            # Tolerancia de sempre: o aluno pode ter acrescentado algo ao nome
            # ("Riscos e Premissas 2026"). Busca por TRECHO, so' do titulo alvo.
            lid = find_list_id_by_name(titulo, listas)
            escolhida = next((l for l in listas if str(l['id']) == str(lid)
                              and str(l['id']) not in reivindicados), None) if lid else None
        if escolhida is None:
            # Lista padrao do ProjectAdm ("A fazer"/"To do"), vazia: vira a nossa.
            escolhida = next((l for l in listas
                              if PADRAO_PARA_CHAVE.get((l.get('nome') or '').strip().lower()) == chave
                              and str(l['id']) not in reivindicados and not _tem_cartoes(l)), None)
        if escolhida is None:
            lid = create_list(titulo)
            log(Tf("  [criada]     {0}  (ID={1})", titulo, lid))
            nova = {'id': str(lid), 'nome': titulo, 'total_cartoes': 0}
            listas.append(nova)
            ids[chave] = lid
            reivindicados.add(str(lid))
            continue
        nome_atual = (escolhida.get('nome') or '').strip()
        if nome_atual == titulo:
            log(Tf("  [ja existe]  {0}  (ID={1})", titulo, escolhida['id']))
        elif pode_renomear:
            rename_list(escolhida['id'], titulo)
            escolhida['nome'] = titulo
            log(Tf('  [renomeada]  "{0}"  ->  {1}  (ID={2})', nome_atual, titulo, escolhida['id']))
        else:
            log(Tf('  [do quadro]  "{0}" é a sua lista {1}  (ID={2})',
                   nome_atual, titulo, escolhida['id']))
        ids[chave] = escolhida['id']
        reivindicados.add(str(escolhida['id']))
        # As OUTRAS candidatas sao sobras da mesma lista (tipicamente o mesmo papel
        # escrito na outra lingua). Vazia -> sai; com cartoes -> fica, e o aluno decide.
        for l in cands:
            if str(l['id']) in reivindicados:
                continue
            reivindicados.add(str(l['id']))
            if _tem_cartoes(l):
                log(Tf('  [mantida]    "{0}" tem cartões e faz o mesmo papel de {1} — '
                       'preservada. Mova os cartões e apague-a quando quiser.',
                       (l.get('nome') or '').strip(), titulo))
            elif delete_list(l['id']):
                log(Tf('  [removida]   lista duplicada e vazia "{0}"', (l.get('nome') or '').strip()))
                listas[:] = [x for x in listas if str(x['id']) != str(l['id'])]

    # Sobras das listas padrao (quadro ja mexido): remove as VAZIAS, preserva as
    # que tem cartoes (o dado do aluno nunca e apagado).
    # Como STRING, sempre: as duas varreduras abaixo APAGAM lista, e o unico freio delas e'
    # "esta e' uma das minhas". O GET de LISTA devolve `id` como string e o POST de criacao
    # pode devolver inteiro — misturar os dois faz o freio falhar justamente na lista que a
    # gente acabou de criar. Mesmo motivo do str() no garantir_referencia (quadro 270,
    # 06/08/2026): comparacao de id neste modulo e' sempre entre strings.
    alvo_ids = {str(v) for v in ids.values()}
    for l in list_lists():
        nome = (l.get('nome') or '').strip().lower()
        if str(l['id']) in alvo_ids or nome not in PADRAO_PARA_CHAVE:
            continue
        if _vazia(l['id']):
            if delete_list(l['id']):
                log(Tf('  [removida]   lista padrão vazia "{0}"', l.get('nome')))
        else:
            log(Tf('  [mantida]    "{0}" tem cartões — preservada.', l.get('nome')))

    # Listas retiradas nesta abordagem (ex.: a preditiva nao tem mais "Em execução"/
    # "Entregue"). Num quadro que ja existe, remove as VAZIAS; se o aluno ja pos cartoes
    # la, preserva e avisa. Nunca apaga dado do aluno.
    a = normalizar_abordagem(abordagem) if abordagem else None
    for nome_retirado in LISTAS_RETIRADAS.get(a, ()):
        for l in list_lists():
            if (l.get('nome') or '').strip() != nome_retirado or str(l['id']) in alvo_ids:
                continue
            if _vazia(l['id']):
                if delete_list(l['id']):
                    log(Tf('  [removida]   lista retirada "{0}" (vazia)', l.get('nome')))
            else:
                log(Tf('  [mantida]    "{0}" tem cartões — preservada (mova-os para a EAP → '
                       'Cronograma e apague a lista quando quiser).', l.get('nome')))

    # Ordem das listas = a ordem da jornada (Referencias -> 5W2H -> ... -> Concluido).
    # Sem isto, uma lista criada depois (ex.: num quadro que ja existia) cai no fim —
    # e o aluno abre o quadro e nao ve o Termo de Abertura onde deveria.
    # So' mexemos na lista que esta FORA de lugar: reordenar as 10 a cada execucao eram
    # 10 chamadas de API (~6s) para, quase sempre, nao mudar nada.
    from pa_client import api as _api
    ordem_atual = {l['id']: l.get('ordem') for l in list_lists()}
    for pos, (chave, _t) in enumerate(alvos):
        if ordem_atual.get(ids[chave]) == pos:
            continue
        try:
            _api("PUT", f"/listas/{ids[chave]}", {"ordem": pos})
        except Exception as e:
            log(Tf("  [aviso] não consegui reordenar a lista {0}: {1}", _t, e))

    save_cfg({f"LIST_{k}": v for k, v in ids.items()})
    return ids


def _upsert_card(list_id, titulo, descricao, sobrescrever=True):
    """Cria ou atualiza (pelo titulo) um cartao NA PROPRIA lista. Nunca duplica e
    nunca apaga cartao do aluno (que tem outro titulo).

    sobrescrever=False: se o cartao JA existe, nao toca na descricao. E o que
    protege o que VOCE escreveu — o texto-guia so aparece no cartao vazio; assim
    que o passo 1 grava a sua resposta, ela nunca mais e sobrescrita por uma
    re-execucao do script.
    """
    existente = next((c for c in list_cards(list_id)
                      if (c.get('titulo') or '') == titulo), None)
    if existente:
        if sobrescrever:
            update_card(existente['id'], descricao=descricao)
        return existente['id'], False
    return create_card(list_id, titulo, descricao), True


def cards_do_termo_ids(ids):
    """{titulo do cartao: id} dos cartoes do Termo de Abertura (para preencher)."""
    return {(c.get('titulo') or ''): c['id'] for c in list_cards(ids["INPUTS"])}


def _migrar_titulos(list_id, log=print):
    """Cartoes de uma versao anterior do curso: renomeia (preservando o que o aluno
    escreveu) em vez de criar um cartao novo com o titulo novo."""
    atuais = {(c.get('titulo') or ''): c for c in list_cards(list_id)}
    for antigo, novo in LEGADO.items():
        if antigo in atuais and novo not in atuais:
            update_card(atuais[antigo]['id'], titulo=novo)
            log(Tf('  [renomeado] "{0}"  ->  "{1}"', antigo, novo))


def garantir_termo(ids, abordagem=None, log=print):
    """Cria/atualiza os 14 cartoes da lista INPUTS (a caixa + as 7 perguntas do 5W2H).

    A REGRA: o texto que VOCE escreveu nunca e sobrescrito. O texto-guia (o nosso) e
    atualizado — e e assim que a ETAPA 2 destranca o bloco COMO (4.1 a 4.5) e troca o
    guia dos demais cartoes pela forma da SUA abordagem, sem tocar no que voce ja
    respondeu.
    """
    from pa_client import criar_cartoes
    _migrar_titulos(ids["INPUTS"], log)
    existentes = {(c.get('titulo') or ''): c for c in list_cards(ids["INPUTS"])}
    criados = atualizados = 0

    # Os que faltam nascem TODOS DE UMA VEZ (em paralelo). A ordem certa vem logo
    # abaixo, no _ordenar_termo — que ja existia para outro motivo.
    novos = [(t, d, None) for t, d in cartoes_do_termo(abordagem) if t not in existentes]
    if novos:
        criar_cartoes(ids["INPUTS"], novos, log=log)
        criados = len(novos)
        for t, _d, _p in novos:
            log(Tf("  [criado]     {0}", t))

    # As trocas de texto-guia (a etapa 2 destranca o bloco COMO) vao TODAS de uma vez.
    trocas = []
    for titulo, desc in cartoes_do_termo(abordagem):
        card = existentes.get(titulo)
        if not card:
            continue                       # acabou de nascer, com o texto certo
        atual = (card.get('descricao') or '').strip()
        if atual == desc.strip():
            log(f"  [ok]         {titulo}")
        elif atual in textos_de_fabrica(titulo) or not atual:
            trocas.append((card['id'], {"descricao": desc}))   # texto NOSSO: pode trocar
            log(Tf("  [atualizado] {0}", titulo))
        else:
            log(Tf("  [preservado] {0}  (você já respondeu — não toquei)", titulo))
    if trocas:
        from pa_client import atualizar_cartoes
        atualizar_cartoes(trocas, log=log)
        atualizados = len(trocas)
    _ordenar_termo(ids["INPUTS"], abordagem, log)
    return criados, atualizados


def _ordenar_termo(lista_id, abordagem, log=print):
    """A lista INPUTS na ordem do Termo de Abertura (1 por que ... 6 quanto).

    Um cartao RENOMEADO nao muda de lugar sozinho: quando o ONDE saiu do 2.3 (dentro
    do O QUE) e virou 4.6 (dentro do COMO), o cartao continuou fisicamente entre o 2.2
    e o 3 — e o aluno via o termo fora de ordem. Aqui a ordem visivel passa a ser
    sempre a do documento. Cartao que o ALUNO criou nesta lista vai para o fim, na
    ordem em que ele os criou (nao sumem, nao se misturam).
    """
    from pa_client import ordenar_cartoes
    cards = list_cards(lista_id)
    por_titulo = {(c.get('titulo') or ''): c for c in cards}
    ordem_certa = [por_titulo[t]['id'] for t, _d in cartoes_do_termo(abordagem)
                   if t in por_titulo]
    nossos = set(ordem_certa)
    do_aluno = [c['id'] for c in cards if c['id'] not in nossos]
    atual = [c['id'] for c in cards]
    final = ordem_certa + do_aluno
    if final == atual:
        return False
    try:
        ordenar_cartoes(lista_id, final)
        log(T("  [ordem]      cartões reordenados na sequência do termo de abertura"))
        return True
    except Exception as e:
        log(Tf("  [aviso] não consegui reordenar os cartões: {0}", e))
        return False


def garantir_guias(ids, abordagem=None, log=print):
    """Cria/atualiza o cartao 'Guia desta lista' nas listas que comecam vazias.

    A etapa 2 abre 7 listas de uma vez — 7 guias, um POST atras do outro, sao ~5s de
    espera pura. Vao todos juntos.
    """
    from concurrent.futures import ThreadPoolExecutor
    titulos = dict(listas_do_quadro(abordagem))
    alvos = [(chave, lid, guia_da_lista(chave, abordagem)) for chave, lid in ids.items()]
    alvos = [(c, l, t) for c, l, t in alvos if t]

    def _um(alvo):
        chave, lid, texto = alvo
        _cid, novo = _upsert_card(lid, GUIA_TITULO, texto)
        return chave, novo

    with ThreadPoolExecutor(max_workers=6) as ex:
        for chave, novo in ex.map(_um, alvos):
            # Cada caso e' uma FRASE INTEIRA no catalogo. Escrever
            # f"[{'guia criado' if novo else 'guia atualizado'}]" escondia o portugues
            # DENTRO da expressao da f-string, onde nem o T() nem o lint chegavam.
            log(Tf("  [guia criado] {0}", titulos.get(chave, chave)) if novo
                else Tf("  [guia atualizado] {0}", titulos.get(chave, chave)))


def garantir_referencia(ids, abordagem=None, log=print, resolvidos=None):
    """Sincroniza a lista 'Referencias e Passos' com o playbook do curso (um cartao
    por passo, com o prompt na abordagem do aluno).

    ESTES CARTOES SAO 100% NOSSOS (nao tem dado do aluno), entao dava para apagar todos
    e recriar. Era o que se fazia — e custava 44 chamadas de API e ~26 SEGUNDOS em CADA
    execucao do Passo 1. Como o Passo 1 e' rodado varias vezes de proposito (e o ritmo do
    curso), o aluno pagava esse pedagio a cada volta (Eduardo, 2026-07-14: "esta demorando
    muito"). Agora e' UPSERT: le a lista uma vez, cria o que falta, atualiza SO' o que
    mudou (tipicamente o prompt de um elo que acabou de ser preenchido) e apaga so' o que
    sobrou. Uma volta sem novidade custa ~2 chamadas.

    O reconhecimento e por FORMA (referencia_cards.e_cartao_do_curso), nao por titulo
    exato: um cartao do curso que MUDOU DE NOME (a corrente do Passo 1 foi reordenada)
    nao seria pego pelo titulo atual — e sobreviveria, duplicando o prompt no quadro.

    `resolvidos` (contexto.prompts_da_corrente) = os prompts dos 6 elos ja resolvidos
    contra o quadro. E' o que faz o prompt aparecer no cartao JA PREENCHIDO, em vez de
    um "[INPUT 1.1]" cru que obrigaria o aluno a copiar na mao um texto que o script
    acabou de ler.
    """
    import entradas
    from pa_client import api, delete_card
    from referencia_cards import (build_cards, cards_da_corrente_p1, all_titles,
                                  e_cartao_do_curso)
    cards = build_cards(abordagem)
    corrente = cards_da_corrente_p1(resolvidos)
    alvo = dict(cards)                                   # titulo -> descricao (playbook)
    alvo.update(dict(corrente))                          # + os elos do Passo 1
    existentes = {(c.get('titulo') or ''): c for c in list_cards(ids["REFERENCIA"])}
    criados = atualizados = removidos = 0

    # 1) sobras: cartao do curso que nao esta mais no playbook (mudou de nome, mudou de
    #    mudou de abordagem). So' dentro da Referencia — um cartao do
    #    aluno noutra lista jamais e tocado.
    for titulo, cd in list(existentes.items()):
        if titulo not in alvo and e_cartao_do_curso(titulo) and delete_card(cd['id']):
            del existentes[titulo]                       # sumiu do quadro: sumiu daqui
            removidos += 1
            log(Tf("  [removido] {0}", titulo))

    # 2) o playbook. Os que faltam nascem todos de uma vez (em paralelo); a ordem certa
    #    e' garantida no fim, com uma chamada so'.
    from pa_client import criar_cartoes, ordenar_cartoes
    faltam = [(t, d, None) for t, d in cards if t not in existentes]
    if faltam:
        novos = criar_cartoes(ids["REFERENCIA"], faltam, log=log)
        criados += sum(1 for i in novos if i)
        for (t, _d, _p), i in zip(faltam, novos):
            if i:
                existentes[t] = {"id": i, "descricao": dict(cards)[t]}
                log(f"  [playbook] {t}")
    for titulo, desc in cards:
        cd = existentes.get(titulo)
        if cd and entradas.texto_do_cartao(cd.get('descricao')) != desc.strip():
            update_card(cd['id'], descricao=desc)
            atualizados += 1
    pai_01 = next((cd['id'] for t, cd in existentes.items() if t.startswith("01 - ")), None)

    # 3) os 6 elos: subtarefas do cartao do Passo 1 (a corrente fica visivel como
    #    corrente, e cada cartao carrega um prompt so). So' podem nascer depois do "01".
    faltam_elos = [(t, d, pai_01) for t, d in corrente if t not in existentes]
    if faltam_elos:
        novos = criar_cartoes(ids["REFERENCIA"], faltam_elos, log=log)
        criados += sum(1 for i in novos if i)
        for (t, _d, _p), i in zip(faltam_elos, novos):
            if i:
                existentes[t] = {"id": i, "descricao": dict(corrente)[t]}
                log(Tf("    [elo] {0}", t))
    for titulo, desc in corrente:
        cd = existentes.get(titulo)
        if cd and entradas.texto_do_cartao(cd.get('descricao')) != desc.strip():
            # e' aqui que o prompt do elo passa de "⏳ falta responder" para
            # "✅ pronto, com o seu texto dentro" — sem recriar o cartao.
            update_card(cd['id'], descricao=desc)
            atualizados += 1
            log(Tf("    [elo atualizado] {0}", titulo))

    # 4) a ordem visivel: 00, 01 (+ os 6 elos), 02 ... 99. Criar em paralelo troca a
    #    ordem de chegada — esta chamada devolve o playbook a' ordem do curso.
    if criados:
        ordem = [existentes[t]['id'] for t, _d in cards if t in existentes]
        ordem += [existentes[t]['id'] for t, _d in corrente if t in existentes]
        try:
            ordenar_cartoes(ids["REFERENCIA"], ordem)
        except Exception as e:
            log(Tf("  [aviso] não consegui ordenar o playbook: {0}", e))

    # 4) o cartao do playbook que o ALUNO ARRASTOU para outra lista: ele sumiu da
    #    Referencia, entao acabamos de recriar um irmao dele — e agora ha' dois. Varremos
    #    as outras listas SO' quando algo foi criado (numa volta sem novidade, `criados`
    #    e' zero e nao se paga nada por esta protecao).
    if criados:
        for l in list_lists():
            # str() NOS DOIS LADOS, e nao por elegancia: o GET de LISTA devolve `id` como
            # STRING e um `ids` montado a partir de inteiros (ou de um .env convertido com
            # int()) faria esta comparacao dar False para a PROPRIA lista Referencia. A
            # varredura entao passaria por ela e apagaria, como "duplicata", os 20 cartoes
            # que acabaram de nascer duas linhas acima — a lista some inteira e o log jura
            # que criou tudo. Aconteceu no quadro 270 em 06/08/2026.
            if str(l['id']) == str(ids["REFERENCIA"]):
                continue
            for cd in list_cards(l['id']):
                if (cd.get('titulo') or '') in alvo and delete_card(cd['id']):
                    removidos += 1
                    log(Tf("  [duplicata removida de outra lista] {0}", cd.get('titulo')))

    log(Tf("  Referências: {0} cartão(ões) — {1} novo(s), {2} atualizado(s), {3} removido(s).", len(alvo), criados, atualizados, removidos))
    return len(alvo)


# O texto-guia da ABORDAGEM traz a instrucao depois das primeiras palavras
# ("ESCOLHA NO PASSO 1 (rode ...)" / "CHOOSE IN STEP 1 (run ...)"), entao comparamos por
# PREFIXO — e nos dois idiomas. So o prefixo PT estava aqui, e por isso o pacote EN tratava
# o texto-guia como uma escolha real e pulava direto para a etapa 2 como PREDITIVA, sem
# perguntar nada (02/08/2026). Fonte: course.json (env_pt / env_en).
_PREFIXOS_ABORDAGEM = ("ESCOLHA", "CHOOSE")


def abordagem_do_env():
    """A abordagem escolhida (PREDITIVA/AGIL) — ou None se ainda nao foi escolhida."""
    v = (cfg().get("APPROACH") or "").strip()
    if not v or v.upper().startswith(_PREFIXOS_ABORDAGEM):
        return None
    return normalizar_abordagem(v)


# Deteccao da abordagem a partir do TEXTO do cartao 4.1. So palavras-raiz claras — um
# termo isolado ("preditiv") decide; se as duas familias aparecerem (prosa que compara
# as duas) ou nenhuma, e ambiguo e devolvemos None (o aluno roda o 01a).
_DET_PREDITIVA = ("preditiv", "predictive", "cascata", "waterfall")
_DET_AGIL = ("agil", "agile", "scrum", "adaptativ", "adaptive")  # texto ja vem sem acento


def _detectar_abordagem(txt):
    t = entradas._sem_acento((txt or "").lower())
    tem_p = any(w in t for w in _DET_PREDITIVA)
    tem_a = any(w in t for w in _DET_AGIL)
    if tem_p and not tem_a:
        return "PREDITIVA"
    if tem_a and not tem_p:
        return "AGIL"
    return None


def abordagem_do_quadro(c=None):
    """A FONTE DA VERDADE E O QUADRO: se o aluno ESCREVEU a abordagem no cartao 4.1
    (em vez de rodar o 01a), honramos essa decisao.

    Le o cartao 4.1 da lista INPUTS (via LIST_INPUTS ja salvo no .env) e tenta
    reconhecer PREDITIVA/AGIL. O texto-guia de fabrica (inclusive o cadeado da etapa 1)
    NAO conta como resposta — respostas_do_termo ja o filtra. Devolve a abordagem
    reconhecida ou None (texto ambiguo/ausente -> o aluno decide pelo 01a).
    """
    c = c or cfg()
    ids = {k[5:]: v for k, v in c.items() if k.startswith("LIST_")}
    if not ids.get("INPUTS"):
        return None
    try:
        txt = respostas_do_termo(ids).get("4.1")
    except Exception:
        return None
    return _detectar_abordagem(txt)


# ── INPUTS: ler o que o aluno (ou a IA dele) escreveu NO QUADRO ──────────────
# Este e o padrao Together: no modo CHAT o aluno NAO edita script. A IA devolve
# texto, ele cola no quadro, o script le o quadro. O codigo sai do caminho.
import re as _re                                                    # noqa: E402
import entradas                                                     # noqa: E402
import vocabulario                                                  # noqa: E402

# prefixo do cartao -> (chave do contexto, parser)
MAPA_INPUTS = {
    "1.1": ("justificativa", None),                # texto puro
    "1.2": ("okr", entradas.okr),
    "2.1": ("entregas", entradas.entregas),
    "3.": ("partes", entradas.partes),
    "4.1": ("abordagem_justificativa", None),
    "4.2": ("restricoes", entradas.simples),
    "4.3": ("premissas", entradas.simples),
    "4.4": ("riscos_iniciais", entradas.riscos),
    "4.6": ("onde", None),                         # o ONDE e parte do COMO
    "5.": ("marcos", entradas.marcos),
    "6.": ("ordem_de_grandeza", None),
}


def _cards_inputs(ids):
    return {(c.get("titulo") or ""): c for c in list_cards(ids["INPUTS"])}


# ── O TEXTO dos cartoes 2.1 e 2.2 ────────────────────────────────────────────
# DOIS scripts escrevem estes cartoes: o Passo 1 (a partir da resposta da IA) e a
# ENGENHARIA REVERSA (a partir da EAP). Enquanto o formato era uma f-string repetida nos
# dois, so' nao divergiu por sorte — e ele importa: e' este mesmo texto que o
# entradas.entregas() le de volta na rodada seguinte. Um lugar so' escreve, um lugar so'
# muda.
#
# O rotulo passa por Tf() porque o cartao e' PRODUTO: num quadro em ingles ele tem de
# dizer "serves KR:", nao "serve ao KR:". A leitura nao se abala com isso — o
# vocabulario.CAMPOS ja mapeia 'serves kr', 'requirement' e 'acceptance criterion' para os
# canonicos portugueses, entao um cartao escrito por qualquer um dos dois pacotes continua
# sendo lido pelos dois.
def texto_das_entregas(entregas):
    """Cartao 2.1 — cada entrega e o Key Result a que ela serve."""
    return "\n".join(
        Tf("• {0}\n    serve ao KR: {1}", e.get("nome", ""), e.get("kr", ""))
        for e in entregas)


def texto_dos_requisitos(entregas):
    """Cartao 2.2 — o requisito e o criterio de aceitacao de cada entrega."""
    return "\n".join(
        Tf("• {0}\n    requisito: {1}\n    critério de aceitação: {2}",
           e.get("nome", ""), e.get("requisito", ""), e.get("aceitacao", ""))
        for e in entregas)


def rotulo_da_lista(chave):
    """"ESCOPO" -> "EAP → Cronograma [O QUÊ → QUANDO]" — o nome que a lista tem NO
    QUADRO do aluno, na abordagem e no idioma dele. Serve a quem precisa DIZER de onde
    leu alguma coisa: mandar o aluno olhar a chave interna ("a lista ESCOPO") o faria
    procurar no quadro uma lista que nao existe com esse nome."""
    for ab in (abordagem_do_env(), "PREDITIVA", "AGIL"):
        for c, titulo in listas_do_quadro(ab or None):
            if c == chave:
                return titulo
    return chave


# ─────────────────────────────────────────────────────────────────────────────
# RESOLVER OS PARAMETROS [INPUT x] DOS PROMPTS  (a ideia do Eduardo, 2026-07-14)
#
# O prompt declara de que cartao ele come ([INPUT 1.1]); aqui a gente vai ao QUADRO,
# le o cartao, limpa o HTML do editor e devolve o prompt PRONTO. O quadro e' a unica
# fonte da verdade — nem o .py, nem a memoria em _saida: o QUADRO.
# ─────────────────────────────────────────────────────────────────────────────
_LISTAS_FONTE = ("ESCOPO", "RISCOS", "PARTES", "CRONOGRAMA", "ORCAMENTO", "MUDANCAS")


def _rotulo(titulo):
    """"2.1 O QUE — Produtos..." -> "2.1"   ·   "3. QUEM — ..." -> "3" """
    m = _re.match(r"^(\d+(?:\.\d+)?)", (titulo or "").strip())
    return m.group(1).rstrip(".") if m else None


def respostas_do_termo(ids):
    """{rotulo: o que o ALUNO escreveu} nos cartoes do termo de abertura.

    O texto-guia de fabrica (o nosso) NAO conta como resposta — senao o prompt sairia
    com a nossa instrucao dentro dele, e a IA responderia a pergunta errada.
    """
    out = {}
    for titulo, card in _cards_inputs(ids).items():
        if titulo == CAIXA:
            continue
        rot = _rotulo(titulo)
        if not rot:
            continue
        desc = entradas.texto_do_cartao(card.get("descricao"))
        if desc and desc not in textos_de_fabrica(titulo):
            out[rot] = desc
    return out


def texto_da_lista(ids, chave):
    """Os cartoes de uma lista de trabalho (EAP, Riscos, Partes...) como TEXTO —
    e' o que vai dentro de [INPUT ESCOPO], [INPUT RISCOS]..."""
    lid = ids.get(chave)
    if not lid:
        return ""
    linhas = []
    for c in list_cards(lid):
        titulo = (c.get("titulo") or "").strip()
        if not titulo or e_guia(titulo):
            continue
        desc = entradas.texto_do_cartao(c.get("descricao"))
        linhas.append(f"• {titulo}")
        for l in (desc or "").splitlines():
            if l.strip():
                linhas.append(f"    {l.strip()}")
    return "\n".join(linhas)


def texto_da_equipe(ids):
    """SO' os membros da EQUIPE (os filhos do cartao 'EQUIPE — quem faz' na lista Partes)
    -> [INPUT EQUIPE]. E' a lista FECHADA de quem pode ser responsavel por trabalho no
    projeto (pacote da EAP, acao de risco, tarefa). Existe porque texto_da_lista(PARTES)
    devolve as partes PLANAS — cliente, equipe e demais misturados, sem dizer quem e' de
    qual grupo (o list-endpoint nao traz cartao_pai_id). Sem esta lista dedicada, a IA nao
    tinha como saber quais partes sao o time e atribuia responsavel a um cliente/area
    (Eduardo, 2026-07-20: 'como a IA vai saber que nao pode?'). Aqui a hierarquia e'
    resolvida pelo GET de cada cartao (o unico que traz o pai)."""
    from concurrent.futures import ThreadPoolExecutor
    lid = ids.get("PARTES")
    if not lid:
        return ""
    cards = list_cards(lid)
    equipe = next((c for c in cards if e_grupo_equipe(c.get("titulo"))), None)
    if not equipe:
        return ""
    equipe_id = str(equipe["id"])
    with ThreadPoolExecutor(max_workers=6) as ex:
        detalhes = list(ex.map(lambda c: (c, get_card(c["id"])), cards))
    linhas = []
    for c, full in detalhes:
        if str(full.get("cartao_pai_id")) != equipe_id:
            continue
        titulo = (c.get("titulo") or "").strip()
        if titulo:
            linhas.append(f"• {titulo}")
    return "\n".join(linhas)


# ── Pontos de atencao: riscos e problemas ────────────────────────────────────
# PMBOK 8 avalia um risco por PROBABILIDADE x IMPACTO (Perform Risk Analysis: "risk
# probability and impact assessment", "probability and impact matrix"). O PRODUTO e' a
# PRIORIDADE do ponto de atencao — o que se trata primeiro (nao "gravidade", que e' a
# seriedade do impacto). Um problema ja' aconteceu -> no lugar da probabilidade entra a
# URGENCIA (Urgencia x Impacto). O curso pede alta/media/baixa; a planilha 'Registro dos
# pontos de atencao' usa a escala de 5 niveis. Convertemos para 1..5:
#   muito baixa=1 · baixa=2 · media=3 · alta=4 · muito alta=5.
_ROT_PROB_PT = {1: "1-Muito baixa", 2: "2-Baixa", 3: "3-Média",
             4: "4-Alta", 5: "5-Muito alta"}
_ROT_IMP_PT = {1: "1-Muito baixo", 2: "2-Baixo", 3: "3-Médio",
            4: "4-Alto", 5: "5-Muito alto"}


def nivel_1a5(palavra):
    """'alta' / 'muito alto' / '3' / 'médio'... -> 1..5 (ou None). Eixo-agnostico."""
    p = entradas._sem_acento((palavra or "").strip())
    if not p:
        return None
    m = _re.match(r"([1-5])\b", p)
    if m:
        return int(m.group(1))
    # Os radicais moram no vocabulario do kit, com os equivalentes em ingles: a IA
    # do aluno pode responder 'very high' num curso em portugues, e vice-versa.
    muito = any(x in p for x in vocabulario.NIVEL_MUITO)
    if any(x in p for x in vocabulario.NIVEL_BAIXO):
        return 1 if muito else 2
    if any(x in p for x in vocabulario.NIVEL_MEDIO):
        return 3
    if any(x in p for x in vocabulario.NIVEL_ALTO):
        return 5 if muito else 4
    return None


def escala_probabilidade(palavra):
    """palavra -> (numero 1..5 ou None, rotulo da planilha, ex.: (4, '4-Alta'))."""
    n = nivel_1a5(palavra)
    return n, (_ROT_PROB.get(n, "") if n else "")


def escala_impacto(palavra):
    n = nivel_1a5(palavra)
    return n, (_ROT_IMP.get(n, "") if n else "")


def prioridade_do_risco(probabilidade, impacto):
    """Probabilidade (ou Urgencia) x Impacto -> (numero 1..25 ou None, nivel
    'baixa'/'media'/'alta'/'urgente'). E' a PRIORIDADE do ponto de atencao."""
    np_, _ = escala_probabilidade(probabilidade)
    ni, _ = escala_impacto(impacto)
    if not np_ or not ni:
        return None, None
    num = np_ * ni
    nivel = ("urgente" if num >= 15 else "alta" if num >= 9
             else "media" if num >= 5 else "baixa")
    return num, nivel


# Rotulos conhecidos de um "ponto de atencao"/cronograma — usados para DELIMITAR o valor
# quando o editor do ProjectAdm junta os campos na mesma linha (sem o '·').
_ROTULOS_CAMPO = vocabulario.ROTULOS_CAMPO


def _val_desc(txt, pat):
    """O valor de um campo 'rotulo: valor'. TOLERANTE ao que o editor rico do ProjectAdm
    faz quando o aluno edita o cartao: o rotulo pode estar no INICIO da linha OU inline
    (as linhas se juntam), e os campos podem vir grudados so' por espaco. O valor vai ate:
    um separador (· |), o PROXIMO rotulo conhecido, ou o fim da linha.

    Conserta o bug do Eduardo (board 270): 'Probabilidade: muito baixa' colado no fim do
    enunciado nao era reconhecido (exigia inicio de linha) -> a prioridade nao calculava;
    e 'Impacto: medio Prioridade: (defina...)' devolvia 'medio Prioridade: (defina...)'
    em vez de 'medio'. Tambem passa a ler 'responsavel:' inline depois de um '·'."""
    m = _re.search(
        rf"(?im)(?:^|[\s·|])(?:{pat})\s*:\s*(.+?)"
        rf"(?=\s*(?:[·|]|(?:{_ROTULOS_CAMPO})\s*:|$))",
        txt)
    return m.group(1).strip() if m else ""


# Um "ponto de atencao" e' um de quatro TIPOS (o template 'Registro dos pontos de
# atencao' valida essa lista): Risco (ameaca/oportunidade), Problema (ja aconteceu),
# Premissa (assumo verdadeiro sem prova) e Restricao (limite imposto de fora). So'
# Risco/Problema tem probabilidade x impacto -> prioridade; Premissa/Restricao NAO.
def rotulo_nivel(nivel):
    """'alta' -> 'high'. O NÍVEL da prioridade é calculado (probabilidade × impacto), e
    por isso é a única parte do "Prioridade: 12 (alta)" que não vem do aluno — logo, é
    texto do curso e fala o idioma dele. A chave continua em português: quem a consome
    é `passos.prioridade_de()`, que traduz para a prioridade do cartão no ProjectAdm."""
    return {"urgente": T("urgente"), "alta": T("alta"),
            "media": T("media"), "baixa": T("baixa")}.get(nivel, nivel or "")


def _tipo_do_ponto(titulo, txt):
    """Descobre o TIPO de um cartao da lista Riscos e Premissas, robusto ao formato.
    Prioriza (1) o prefixo do titulo — cartoes antigos escreviam 'PREMISSA:'/'RESTRICAO:';
    (2) o marcador da 1a linha da descricao (🔵 PREMISSA / 🟠 RESTRICAO / 🔴 AMEACA /
    🟢 OPORTUNIDADE / problema) que o Passo 3 grava; e so' por ultimo (3) uma palavra-chave
    solta — evitando o falso-positivo de um risco que MENCIONA 'premissa' no meio do texto."""
    t = entradas._sem_acento(titulo)
    if _re.match(r"^\s*(restric|constraint)", t):
        return "Restrição"
    if _re.match(r"^\s*(premissa|assumption)", t):
        return "Premissa"
    primeira = entradas._sem_acento(txt).split("\n", 1)[0]
    for chave, tipo in vocabulario.TIPO_PONTO:
        if chave in primeira:
            return tipo
    corpo = entradas._sem_acento(txt)
    if "problema" in corpo or "issue" in corpo:
        return "Problema"
    return "Risco"


def _titulo_sem_prefixo(titulo):
    """Tira o prefixo redundante 'PREMISSA:'/'RESTRICAO:' do titulo — o Tipo ja o diz,
    entao a coluna Descricao do registro fica limpa."""
    return _re.sub(rf"(?i)^\s*({vocabulario.PREFIXO_TIPO})\s*:\s*", "", titulo).strip()


# O titulo do cartao tem teto de 255 chars na API do ProjectAdm, mas um enunciado de risco
# (CAUSA->EVENTO->IMPACTO) pode passar de 700. Por isso o Passo 3 grava o ENUNCIADO COMPLETO
# na DESCRICAO (entre a marca e o 1o campo) e o registro le dali — nada se perde.
_MARCA_LINE = _re.compile(r"^\s*(?:🔴|🟢|🔵|🟠|🟡)")
# O 1o campo rotulado — no INICIO da linha OU inline (colado no fim do enunciado, como o
# editor do ProjectAdm as vezes deixa: '...Operacional Probabilidade: muito baixa'). O
# grupo 1 e' o rotulo, para sabermos onde CORTAR o enunciado.
_CAMPO_LABEL_INLINE = _re.compile(
    r"(?i)(?:^|[\s·|])((?:probabilidade|urg[eê]ncia|impacto|prioridade|resposta|"
    r"a[cç][aã]o|dono|respons[aá]vel|probability|likelihood|urgency|impact|priority|"
    r"response|action|owner|responsible)\s*:)")


def _enunciado_do_ponto(txt, titulo):
    """O enunciado COMPLETO do ponto de atencao (sem teto de 255): o texto da descricao
    entre a marca (🔴/🟢/🔵/🟠) e o 1o campo rotulado — mesmo que esse campo esteja INLINE,
    grudado no fim do enunciado (o enunciado NAO absorve 'Probabilidade: muito baixa').
    Cartao antigo (enunciado so' no titulo) degrada para o titulo (o melhor disponivel)."""
    linhas = (txt or "").split("\n")
    i = 1 if (linhas and _MARCA_LINE.match(linhas[0])) else 0
    corpo = []
    for ln in linhas[i:]:
        m = _CAMPO_LABEL_INLINE.search(ln)
        if m:
            antes = ln[:m.start(1)].strip()   # o que vem ANTES do rotulo ainda e' enunciado
            if antes:
                corpo.append(antes)
            break
        corpo.append(ln.strip())
    enun = " ".join(s for s in corpo if s).strip()
    return enun or _titulo_sem_prefixo(titulo)


def riscos_do_quadro(ids):
    """Le a lista 'Riscos e Premissas' e devolve os pontos de atencao ja parseados,
    prontos para a planilha 'Registro dos pontos de atencao':
      [{tipo, risco, prob_ou_urgencia, usa_urgencia, impacto, resposta, dono}].
    A fonte da verdade e' o QUADRO — o mesmo cartao que o Passo 3 escreveu.
    Premissa/Restricao entram no registro tipadas corretamente e SEM probabilidade,
    impacto ou acao (esses campos so' fazem sentido para Risco/Problema)."""
    lid = ids.get("RISCOS")
    if not lid:
        return []
    out = []
    for c in list_cards(lid):
        titulo = (c.get("titulo") or "").strip()
        if not titulo or e_guia(titulo):
            continue
        txt = entradas.texto_do_cartao(c.get("descricao")) or ""
        tipo = _tipo_do_ponto(titulo, txt)
        eh_risco = vocabulario.avaliavel(tipo)
        usa_urg = tipo == "Problema" or bool(_re.search(r"(?im)^\s*urg[eê]ncia\s*:", txt))
        out.append({
            "id": c.get("id"),
            "tipo": tipo,
            "risco": _enunciado_do_ponto(txt, titulo),
            # NOS DOIS IDIOMAS — o Passo 3 escreve estes rótulos no idioma do pacote
            # desde 06/08/2026, e um quadro montado antes disso (ou pelo pacote PT)
            # continua sendo lido. Sem isto, o quadro em inglês devolvia risco sem
            # probabilidade, sem impacto e sem dono — em silêncio.
            "prob_ou_urgencia": _val_desc(txt, r"probabilidade|urg[eê]ncia|probability|"
                                               r"likelihood|urgency") if eh_risco else "",
            "usa_urgencia": usa_urg,
            "impacto": _val_desc(txt, r"impacto|impact") if eh_risco else "",
            "resposta": _val_desc(txt, r"resposta|a[cç][aã]o|response|action") if eh_risco else "",
            "dono": _val_desc(txt, r"dono|respons[aá]vel|owner|responsible"),
        })
    return out


# a linha 'Prioridade: <valor>' (no inicio da linha OU inline, grudada no Impacto)
_PRIORIDADE_LINHA = _re.compile(
    rf"(?im)((?:^|[\s·|])(?:prioridade|priority)\s*:\s*)(.+?)"
    rf"(?=\s*(?:[·|]|(?:{_ROTULOS_CAMPO})\s*:|$))")


def _set_linha_campo(txt, rotulo_re, rotulo_out, valor):
    """Substitui o VALOR de um campo 'Rotulo: X' (inline ou em linha) por `valor`; se o
    campo nao existir no texto, acrescenta 'rotulo_out: valor' no fim. Cirurgico: nao mexe
    no resto. E' como o espelho de texto dos campos personalizados fica em dia."""
    if valor in (None, ""):
        return txt
    valor = str(valor)
    pat = _re.compile(
        rf"(?im)((?:^|[\s·|])(?:{rotulo_re})\s*:\s*)(.+?)(?=\s*(?:[·|]|(?:{_ROTULOS_CAMPO})\s*:|$))")
    if pat.search(txt):
        return pat.sub(lambda m: m.group(1) + valor, txt, count=1)
    return txt.rstrip() + f"\n{rotulo_out}: {valor}"


def espelhar_risco_no_texto(txt, prob=None, imp=None, num=None, nivel=None, dono=None):
    """Escreve Probabilidade/Impacto/Prioridade/Dono no TEXTO do cartao — o espelho que a
    auditoria e o Registro leem. Usado quando o aluno editou os CAMPOS (dropdowns) e a
    gente traz esses valores de volta para o texto. So' toca os campos informados."""
    # O RÓTULO QUE ELE ESCREVE VAI NO IDIOMA DO ALUNO — e é preciso, porque esta função
    # roda TODA VEZ que o painel abre (trazer_escolhas_dos_campos). Com o rótulo fixo em
    # português ela reescrevia "Probabilidade:/Impacto:" no quadro em inglês a cada
    # execução: o conserto de rótulos limpava, e a execução seguinte sujava de novo —
    # uma esteira. Achado ao rodar o painel duas vezes seguidas (Eduardo, 06/08/2026).
    if prob:
        txt = _set_linha_campo(txt, r"probabilidade|urg[eê]ncia|probability|likelihood|"
                                    r"urgency", T("Probabilidade"), prob)
    if imp:
        txt = _set_linha_campo(txt, r"impacto|impact", T("Impacto"), imp)
    if num:
        txt = _set_linha_campo(txt, r"prioridade|priority", T("Prioridade"),
                               f"{num} ({rotulo_nivel(nivel)})")
    if dono and entradas._sem_acento(dono) not in ("a confirmar", "definir", "a definir", "?"):
        txt = _set_linha_campo(txt, r"dono|respons[aá]vel|owner|responsible",
                               T("Dono"), dono)
    return txt


def reparar_prioridades_dos_riscos(ids, log=print):
    """A máquina calcula a PRIORIDADE para o aluno: para cada ponto de atenção com
    Probabilidade e Impacto preenchidos, recalcula a Prioridade (Prob × Impacto) e
    atualiza a linha 'Prioridade:' do cartão (e a cor/prioridade do cartão) quando está
    desatualizada — ex.: o aluno preencheu prob+impacto na revisão e o cartão ainda dizia
    'Prioridade: (defina probabilidade e impacto)'. Cirúrgico: só troca o VALOR da
    prioridade, preserva o resto do cartão. Só escreve o que mudou; devolve quantos
    cartões atualizou. Falha em silêncio por cartão (nunca derruba o passo)."""
    lid = ids.get("RISCOS")
    if not lid:
        return 0
    from pa_client import list_cards, update_card
    n = 0
    for c in list_cards(lid):
        titulo = (c.get("titulo") or "").strip()
        if not titulo or e_guia(titulo):
            continue
        txt = entradas.texto_do_cartao(c.get("descricao")) or ""
        # Sem filtro por tipo: quem preencheu probabilidade e impacto leva a prioridade,
        # inclusive numa premissa (decisão do Eduardo, 23/07). Quem não preencheu não
        # entra — o `if not num` logo abaixo é o filtro que sobra, e ele é o certo.
        num, nivel = prioridade_do_risco(
            _val_desc(txt, r"probabilidade|urg[eê]ncia|probability|likelihood|urgency"),
            _val_desc(txt, r"impacto|impact"))
        if not num:                       # ainda sem prob/impacto válidos — nada a calcular
            continue
        valor = f"{num} ({nivel})"
        if _PRIORIDADE_LINHA.search(txt):
            novo = _PRIORIDADE_LINHA.sub(lambda m: m.group(1) + valor, txt, count=1)
        else:
            novo = txt.rstrip() + f"\nPrioridade: {valor}"
        if novo.strip() == txt.strip():   # já estava certo
            continue
        try:
            update_card(c["id"], descricao=novo, prioridade=nivel)  # nivel = urgente/alta/media/baixa
            n += 1
        except Exception as e:
            log(Tf('  [aviso] não recalculei a prioridade de "{0}": {1}', titulo[:34], e))
    return n


def _valor_num(valor):
    """'R$ 12.500,00' / '12500' / '-5000' / '1.200,50' -> float (0.0 se não der).

    Um sinal de menos à esquerda é PRESERVADO: o orçamento pode ter um item negativo
    (ex.: 'Ajuste de ordem de grandeza: -5000' que a IA gera para reconciliar com a
    estimativa do Passo 1). Antes o `[^\\d.,]` comia o '-' e o abatimento virava soma —
    o total do orçamento ficava inflado (Eduardo, board 270: R$ 105k em vez de 95k)."""
    bruto = (valor or "").strip()
    neg = bruto.startswith("-") or bruto.startswith("(-") or bruto.startswith("- ")
    s = _re.sub(r"[^\d.,]", "", bruto)
    if not s:
        return 0.0
    # pt-BR: '.' milhar, ',' decimal
    if "," in s:
        s = s.replace(".", "").replace(",", ".")
    try:
        v = float(s)
    except ValueError:
        return 0.0
    return -v if neg else v


def orcamento_do_quadro(ids):
    """Le a lista 'Orçamento [QUANTO]' e devolve os itens de custo já parseados,
    para o 'Orçamento do projeto.docx':
      [{nome, tipo, valor, valor_num, quando, pacote}]. O Passo 4 grava cada item com
    'Tipo:/Valor:/Quando:/Pacote:' na descrição — esta é a fonte da verdade.

    `pacote` = a que elemento da EAP o custo pertence (o nome ou o código do cartão da
    EAP), quando a IA soube dizer. É o que leva o dinheiro para a coluna Custo do
    Dicionário da EAP (Eduardo, 2026-07-28): o orçamento deixa de ser uma lista à parte
    e passa a pousar na entrega que o consome. Vazio = custo do projeto todo (ex.: a
    reserva de contingência) — fica no orçamento, fora da EAP."""
    lid = ids.get("ORCAMENTO")
    if not lid:
        return []
    out = []
    for c in list_cards(lid):
        titulo = (c.get("titulo") or "").strip()
        if not titulo or e_guia(titulo):
            continue
        txt = entradas.texto_do_cartao(c.get("descricao")) or ""
        # NOS DOIS IDIOMAS. Ate 06/08/2026 o cartao de custo era escrito com os rotulos
        # em portugues nos DOIS pacotes ("Tipo:/Valor:/Quando:/Pacote:") — o quadro 324
        # do Eduardo, todo em ingles, tinha 11 cartoes assim. Agora o Passo 4 escreve
        # "Type:/Value:/When:/Package:" no pacote EN, e a leitura aceita os dois: um
        # quadro montado antes desta versao continua sendo lido inteiro.
        valor = _val_desc(txt, r"valor|value|amount")
        out.append({
            "id": c.get("id"),
            "nome": titulo,
            "tipo": _val_desc(txt, r"tipo|type"),
            "valor": valor,
            "valor_num": _valor_num(valor),
            "quando": _val_desc(txt, r"quando|per[íi]odo|when|period"),
            "pacote": _val_desc(txt, r"pacote|eap|entrega|package|wbs|deliverable"),
        })
    return out


# ── A EAP (WBS) com a hierarquia pai/filho ───────────────────────────────────
# O ROTULO ESTA NO CARTAO, e o cartao esta' no idioma do aluno: o Passo 2 escreve
# "Criterio de aceitacao:" no pacote PT e "Acceptance criterion:" no EN (desde que a
# descricao passou por Tf, em 04/08/2026 — antes disso ela saia em portugues nos dois
# pacotes, e por isso este regex so' em portugues funcionava). Procurar so' o portugues
# fazia a coluna "Acceptance criteria" do Dicionario da EAP sair VAZIA no pacote ingles
# (Eduardo, 05/08/2026) — e em silencio, porque "sem criterio" e' um estado legitimo.
_ACEIT_RE = _re.compile(
    r"(?:crit[ée]rios?\s+de\s+aceita[cç][aã]o|acceptance\s+criteri(?:on|a))"
    r"\s*:?\s*(.+?)(?:\n\s*\n|no passo 4\b|in step 4\b|$)",
    _re.IGNORECASE | _re.DOTALL)


def _aceitacao_do_pacote(desc):
    """O 'Criterio de aceitacao: ...' (ou 'Acceptance criterion: ...') que o passo 2
    gravou na descricao do pacote."""
    txt = entradas.texto_do_cartao(desc) or ""
    m = _ACEIT_RE.search(txt)
    return _re.sub(r"\s+", " ", m.group(1)).strip() if m else ""


def arvore_da_eap(ids, com_tarefas=False):
    """Le a lista ESCOPO e reconstroi a EAP (WBS) com a hierarquia pai/filho.

    Devolve (nos, tem_fases):
      nos = [{codigo, nivel, tipo, titulo, aceitacao}, ...] na ORDEM da EAP —
            fase 1, suas entregas, os pacotes de cada entrega, fase 2, ...
            tipo e' 'fase' | 'entrega' | 'pacote'; codigo e' o numero da EAP
            (1, 1.1, 1.1.1, 1.1.1.1 ... — a profundidade que a arvore tiver).
      tem_fases = True quando a EAP tem 3 ou mais niveis (Fig. 5-7 do PMBOK 8: a fase
                  do ciclo de vida como um nivel da decomposicao). Os documentos usam
                  este sinal so' para escolher o nivel de titulo/recuo.

    QUANTOS NIVEIS? OS QUE O PROJETO PEDIR (Eduardo, 2026-07-30). A regra geral do curso
    continua sendo tres — FASE > ENTREGA > PACOTE DE TRABALHO —, e e' o que os prompts
    ensinam, porque atende a grande maioria dos projetos. Mas a decomposicao de uma EAP
    nao tem limite: um projeto complexo pode precisar de um nivel a mais (lote > filial >
    ...), e ate esta versao esse nivel simplesmente SUMIA — a leitura parava no terceiro,
    e os netos dos netos nao apareciam na EAP, nem no Dicionario, nem no cronograma.

    O QUE DEFINE UM PACOTE DE TRABALHO NAO E' O NIVEL, E' SER FOLHA. E' a definicao do
    PMBOK: o pacote de trabalho e' o nivel mais baixo da EAP, aquele em que se estima
    custo e duracao. Entao aqui `tipo == "pacote"` quer dizer "nao tem filhos" — e todo o
    resto do curso (datas, custo, criterio de aceitacao, tarefas do Passo 7) continua
    valendo sem saber a profundidade em que aquilo caiu.

    `com_tarefas=True` acrescenta um nivel que NAO e' da EAP: as ATIVIDADES (as tarefas de
    execucao do Passo 7, filhas dos pacotes). A distincao e' de metodo, e e' do Eduardo
    (2026-07-30): a EAP decompoe ENTREGAS e para no pacote de trabalho; o CRONOGRAMA
    detalha as ATIVIDADES de cada pacote. Sao vistas diferentes da mesma arvore — por
    isso o Dicionario da EAP, a auditoria e o Status Report chamam sem tarefas, e o
    Cronograma (.xlsx e MS Project) chama com. Nesta visao o pacote continua sendo
    "pacote" (nao vira entrega por ter atividades dentro) e as atividades saem com
    tipo "tarefa".

    A hierarquia mora em `cartao_pai_id`, que o GET de LISTA nao devolve — por isso
    lemos cada cartao (GET /cartoes/{id}) para achar o pai. (Ver
    reference_projectadm_card_hierarchy.)
    """
    lid = ids.get("ESCOPO")
    if not lid:
        return [], False
    cards = [c for c in list_cards(lid)
             if (c.get("titulo") or "").strip()
             and not e_guia((c.get("titulo") or ""))
             and (com_tarefas or MARCA_EXEC_P7 not in (c.get("descricao") or ""))]
    if not cards:
        return [], False

    from concurrent.futures import ThreadPoolExecutor

    def _int(v):
        try:
            return int(str(v).strip())
        except (TypeError, ValueError):
            return 0

    # ATENCAO: o GET de LISTA devolve id/ordem como STRING; o GET de cartao devolve
    # cartao_pai_id como INT. Sem normalizar, 'pai in nossos' nunca casava (str x int)
    # e a EAP saia toda plana; e ordenar por ordem-string embaralhava (>= 10).
    def _detalhe(c):
        d = get_card(c["id"])
        pai = d.get("cartao_pai_id")
        membros = [str(m.get("id") or m.get("usuario_id"))
                   for m in (d.get("membros") or []) if (m.get("id") or m.get("usuario_id"))]
        return str(c["id"]), {"pai": (str(pai) if pai is not None else None),
                              "ordem": _int(c.get("ordem")),
                              "titulo": (c.get("titulo") or "").strip(),
                              "desc": d.get("descricao") or "",
                              "ini": (d.get("data_inicio") or "")[:10],
                              "fim": (d.get("data_vencimento") or "")[:10],
                              "membros": membros}
    with ThreadPoolExecutor(max_workers=8) as ex:
        det = dict(ex.map(_detalhe, cards))

    nossos = set(det)
    filhos = {}
    raizes = []
    for cid, d in det.items():
        if d["pai"] in nossos:
            filhos.setdefault(d["pai"], []).append(cid)
        else:
            raizes.append(cid)

    def _ordenar(lst):
        return sorted(lst, key=lambda i: det[i]["ordem"])

    raizes = _ordenar(raizes)

    def _e_tarefa(cid):
        return MARCA_EXEC_P7 in (det[cid]["desc"] or "")

    def _filhos_da_eap(cid):
        """Os filhos que fazem parte da DECOMPOSICAO — atividade de execucao nao conta.
        E' o que impede um pacote de virar 'entrega' so' porque o Passo 7 pendurou
        tarefas nele (e o que mantem `tem_fases` medindo a EAP, nao o cronograma)."""
        return [f for f in filhos.get(cid, []) if not _e_tarefa(f)]

    # tem_fases = alguma raiz tem NETO na EAP (raiz -> entrega -> pacote)
    tem_fases = any(any(_filhos_da_eap(f) for f in _filhos_da_eap(r)) for r in raizes)

    # PROTECAO CONTRA CICLO: um `cartao_pai_id` que aponte para um descendente (arrastar
    # cartoes no ProjectAdm ja produziu hierarquias estranhas) faria a descida recursiva
    # abaixo girar para sempre. Um `visitados` custa nada e transforma um travamento do
    # curso num galho ignorado.
    visitados = set()
    nos = []

    def _no(cid, codigo, nivel, tipo):
        d = det[cid]
        txt = entradas.texto_do_cartao(d["desc"]) or ""
        nos.append({"id": cid, "codigo": codigo, "nivel": nivel, "tipo": tipo,
                    "titulo": d["titulo"],
                    "aceitacao": _aceitacao_do_pacote(d["desc"]) if tipo == "pacote" else "",
                    "ini": d.get("ini", ""), "fim": d.get("fim", ""),
                    "membros": d.get("membros", []),
                    "novo_p4": MARCA_NOVO_P4 in (d["desc"] or ""),
                    # OS ROTULOS QUE O PASSO 4 ESCREVE, NAO OS QUE IMAGINAMOS. A linha
                    # gravada e' "duração estimada: 5 dias · responsável: Ana" — em
                    # ingles, "estimated duration: 5 working days · owner: Ana". Ate aqui
                    # a leitura procurava "duração:" seco, que ninguem escreve: a DURACAO
                    # voltava vazia em TODO pacote, nos dois idiomas (some da coluna F do
                    # Cronograma.xlsx e do Dicionario da EAP). E o dono so' era lido em
                    # portugues — o quadro em ingles ja' grava "owner:" (quadro 324,
                    # 06/08/2026). Ler os dois idiomas e as duas formas resolve os dois.
                    "responsavel": _val_desc(txt, r"respons[aá]vel|owner|responsible"),
                    "duracao": _val_desc(txt, r"dura[cç][aã]o(?:\s+estimada)?|"
                                              r"(?:estimated\s+)?duration"),
                    "depende": _val_desc(txt, r"depende de|depends on")})

    def _tipo(cid, nivel):
        """FOLHA da EAP = pacote de trabalho (a definicao do PMBOK, e nao "nivel 3").
        Nivel 1 com filhos = fase quando a EAP tem fases; o meio todo = entrega.
        Atividade de execucao (Passo 7) = 'tarefa' — ela nao e' um elemento da EAP."""
        if _e_tarefa(cid):
            return "tarefa"
        if not _filhos_da_eap(cid):
            return "pacote"
        return "fase" if (nivel == 1 and tem_fases) else "entrega"

    def _descer(cid, codigo, nivel):
        if cid in visitados:
            return
        visitados.add(cid)
        _no(cid, codigo, nivel, _tipo(cid, nivel))
        for i, f in enumerate(_ordenar(filhos.get(cid, [])), 1):
            _descer(f, f"{codigo}.{i}", nivel + 1)

    for i, r in enumerate(raizes, 1):
        _descer(r, str(i), 1)
    return nos, tem_fases


def rollup_datas(ids, dry=False):
    """As datas do cronograma vivem nos PACOTES (as folhas). Aqui elas SOBEM: cada
    entrega/fase (cartão-pai) passa a abranger os filhos — data_inicio = o menor início,
    data_vencimento = o maior término. É o que faz o Gantt do ProjectAdm desenhar as
    barras-resumo, e o que mantém o Cronograma.xlsx honesto.

    Devolve [(id_do_pai, ini, fim)] do que MUDOU (com `dry=True` só calcula, não grava).

    Mora aqui — e não mais dentro do 04_cronograma_orcamento.py — porque quem ajusta uma
    data a mão no quadro precisa do mesmo cálculo sem re-rodar o Passo 4 inteiro
    (recalcular.py). O script de um passo não dá para importar: ele executa ao importar.
    """
    from concurrent.futures import ThreadPoolExecutor
    # AS ATIVIDADES DO PASSO 7 FICAM DE FORA DESTE ROLLUP, e é de propósito. Desde que
    # elas passaram a carregar a data de término no cartão (2026-07-30), deixá-las subir
    # faria a data que o aluno combinou numa Daily EMPURRAR a data do pacote — ou seja, o
    # plano aprovado se reescreveria sozinho, sem ninguém decidir nada, e o Passo 8
    # passaria a comparar o realizado contra um planejado que muda junto. O atraso de uma
    # atividade tem de APARECER no Status Report, não ser absorvido pela linha de base.
    cards = [c for c in list_cards(ids["ESCOPO"])
             if (c.get("titulo") or "").strip()
             and not e_guia((c.get("titulo") or ""))
             and MARCA_EXEC_P7 not in (c.get("descricao") or "")]
    if not cards:
        return []

    def _det(c):
        d = get_card(c["id"])
        pai = d.get("cartao_pai_id")
        return str(c["id"]), {
            "pai": (str(pai) if pai is not None else None),
            "ini": (d.get("data_inicio") or "")[:10],
            "fim": (d.get("data_vencimento") or "")[:10]}

    with ThreadPoolExecutor(max_workers=6) as ex:
        info = dict(ex.map(_det, cards))
    filhos = {}
    for cid, d in info.items():
        if d["pai"] and d["pai"] in info:
            filhos.setdefault(d["pai"], []).append(cid)
    if not filhos:
        return []
    antes = {pid: (info[pid]["ini"], info[pid]["fim"]) for pid in filhos}
    # bottom-up em passes: uma passada por nível da EAP, e ela não tem mais teto fixo —
    # a profundidade real basta (+1 de folga), então uma EAP de 6 níveis também converge.
    for _ in range(max(4, len(filhos) and _profundidade(filhos) + 1)):
        for pid, kids in filhos.items():
            inis = [info[k]["ini"] for k in kids if info[k]["ini"]]
            fims = [info[k]["fim"] for k in kids if info[k]["fim"]]
            if inis:
                info[pid]["ini"] = min(inis)
            if fims:
                info[pid]["fim"] = max(fims)
    mudou = []
    for pid in filhos:
        ini, fim = info[pid]["ini"], info[pid]["fim"]
        if (ini, fim) == antes[pid] or not (ini or fim):
            continue
        mudou.append((pid, ini, fim))
        if dry:
            continue
        campos = {}
        if ini:
            campos["data_inicio"] = ini
        if fim:
            campos["data_vencimento"] = fim
        try:
            update_card(pid, **campos)
        except Exception:
            pass
    return mudou


def _profundidade(filhos):
    """Quantos níveis tem a árvore {pai: [filhos]} — sem recursão e à prova de ciclo."""
    tem_pai = {f for kids in filhos.values() for f in kids}
    raizes = [p for p in filhos if p not in tem_pai] or list(filhos)
    nivel, fronteira, vistos = 0, set(raizes), set()
    while fronteira and nivel < 50:
        nivel += 1
        vistos |= fronteira
        fronteira = {f for p in fronteira for f in filhos.get(p, [])} - vistos
    return nivel


def cronograma_do_quadro(ids, com_tarefas=True):
    """A EAP com o cronograma para o 'Cronograma do Projeto.xlsx': para cada nó da WBS,
    o código, o nível, o nome, início/término (das datas do cartão — pacote datado no
    Passo 4, entrega/fase com rollup), duração, responsável e o predecessor (o código
    EAP da tarefa de que ela depende, resolvido pelo nome). Devolve [] sem EAP.

    `com_tarefas=True` (o padrão AQUI, e só aqui) desce até as ATIVIDADES de cada pacote —
    as tarefas do Passo 7. Um cronograma que mostra o pacote e esconde as atividades dele
    não é um cronograma, é um resumo: quem executa precisa ver a linha que vai marcar como
    feita. A EAP (Dicionário, auditoria, Status Report) continua parando no pacote."""
    nos, _tem_fases = arvore_da_eap(ids, com_tarefas=com_tarefas)
    por_titulo = {entradas._sem_acento(n["titulo"]): n["codigo"] for n in nos}
    linhas = []
    for n in nos:
        dep = (n.get("depende") or "").strip()
        pred = por_titulo.get(entradas._sem_acento(dep), dep) if dep else ""
        linhas.append({
            "codigo": n["codigo"], "nivel": n["nivel"], "tipo": n["tipo"],
            "titulo": n["titulo"], "ini": n.get("ini", ""), "fim": n.get("fim", ""),
            "duracao": n.get("duracao", ""), "predecessor": pred,
            "responsavel": n.get("responsavel", ""),
        })
    return linhas


_ROTULO_SNAPSHOT_PT = [
    ("ESCOPO", "EAP → CRONOGRAMA (escopo + status das tarefas) / Product Backlog"),
    ("EXECUCAO", "SPRINT BACKLOG"),
    ("PRONTO", "INCREMENTO / ENTREGUE"),
    ("RISCOS", "RISCOS E PREMISSAS"),
    ("ORCAMENTO", "ORÇAMENTO"),
    ("PARTES", "PARTES INTERESSADAS"),
    ("MUDANCAS", "MUDANÇAS"),
]


def snapshot(ids):
    """A FOTO VIVA do quadro em texto — e' o que vai dentro de [INPUT QUADRO].

    Os passos 5, 7, 8, 9 e 10 raciocinam sobre o ESTADO REAL do projeto (o que ja foi
    feito, o que esta em execucao, o que atrasou), nao sobre a memoria estatica. Aqui a
    gente le as listas de trabalho e marca o que ja esta CONCLUIDO — a diferenca entre
    planejado e realizado que o Status Report (passo 8) precisa.
    """
    from datetime import date
    partes_txt = [f"SITUAÇÃO DO QUADRO em {date.today().isoformat()}:", ""]
    for chave, rotulo in _ROTULO_SNAPSHOT:
        lid = ids.get(chave)
        if not lid:
            continue
        linhas = []
        for c in list_cards(lid):
            titulo = (c.get("titulo") or "").strip()
            if not titulo or e_guia(titulo):
                continue
            feito = "✓" if card_concluido(c) else "○"
            linhas.append(f"  {feito} {titulo}")
        if linhas:
            partes_txt.append(f"{rotulo} ({len(linhas)} cartão(ões)):")
            partes_txt += linhas
            partes_txt.append("")
    return "\n".join(partes_txt).strip()


# ─────────────────────────────────────────────────────────────────────────────
# O CICLO DA SPRINT (so' na abordagem AGIL)
#
# Na adaptativa os passos 5 a 10 sao CICLICOS: sao a Sprint. O que o quadro precisa
# guardar entre eles e' pouco — a Meta da Sprint (um cartao no topo do Sprint Backlog)
# e a Definicao de Pronto (um cartao na lista Concluido, com o checklist). O NUMERO da
# Sprint vive no .env (SPRINT_ATUAL): o Passo 5 le, o Passo 10 avanca. Nao criamos lista
# nova no quadro por causa disso — o Agile Together tinha listas "Daily Scrum" e
# "Eventos Scrum"; aqui o evento mora no passo, e o registro dele na trilha do curso.
# ─────────────────────────────────────────────────────────────────────────────
_META_PREFIXO_PT = "🎯 META DA SPRINT"
_DOD_PREFIXO_PT = "✅ Definição de Pronto"
# O TERCEIRO compromisso do Guia Scrum — o do Product Backlog. Emoji DIFERENTE do 🎯 de
# proposito: as duas metas vivem no topo de uma lista cada e sao lidas de relance; se
# usassem o mesmo icone, a bandeira do projeto e a da Sprint se confundiriam no quadro.
_META_PRODUTO_PREFIXO_PT = "🏁 META DO PRODUTO"
_COD_BACKLOG = _re.compile(r"^\s*(B\d+)\s*[:\-—–·|)]?\s", _re.I)


def codigo_do_item(titulo):
    """'B10: Plano...' ou 'B10 — Plano...' -> 'b10'. Sem codigo -> '' (nunca casa).

    O codigo e' a alca ESTAVEL do item de backlog — e' o que o proprio Passo 2 promete ao
    aluno ("e por ele que os proximos passos vao se referir a este item"). Casar por ele
    sobrevive a separador e acento diferentes entre o quadro e a resposta da IA.
    """
    m = _COD_BACKLOG.match(titulo or "")
    return m.group(1).lower() if m else ""


def cartoes_por_codigo(cards):
    """{codigo: cartao} dos cartoes que tem codigo (B1, B2...). Codigo REPETIDO fica de
    fora: com dois candidatos nao da para saber qual e' o certo, e mexer no cartao errado
    e' pior do que avisar que nao achei."""
    por_cod, repetidos = {}, set()
    for cd in cards:
        cod = codigo_do_item(cd.get("titulo", ""))
        if not cod:
            continue
        if cod in por_cod:
            repetidos.add(cod)
        por_cod[cod] = cd
    for cod in repetidos:
        por_cod.pop(cod, None)
    return por_cod


def itens_de_trabalho(lid):
    """Os cartoes de trabalho de uma lista — sem o cartao-guia, sem a Meta da Sprint."""
    out = []
    for c in list_cards(lid or 0) if lid else []:
        t = (c.get("titulo") or "").strip()
        # A Meta do Produto e a Meta da Sprint moram no TOPO das suas listas, mas nao
        # sao trabalho: sem esta guarda a Meta do Produto seria oferecida como item
        # selecionavel na Sprint Planning (passo 5) e contada como entrega no relatorio.
        if not t or e_guia(t) or t.startswith((META_PREFIXO, META_PRODUTO_PREFIXO, "0.")):
            continue
        out.append(c)
    return out


def cartao_da_meta(ids, sprint=None):
    """O cartao da Meta da Sprint no Sprint Backlog (o da Sprint `sprint`, ou o ultimo)."""
    alvo = f"{META_PREFIXO} {sprint}:" if sprint else META_PREFIXO
    metas = [c for c in list_cards(ids.get("EXECUCAO") or 0)
             if (c.get("titulo") or "").startswith(alvo)] if ids.get("EXECUCAO") else []
    return metas[-1] if metas else None


def cartao_da_meta_do_produto(ids):
    """O cartao da Meta do Produto (topo do Product Backlog), ou None.

    Ela e' UMA por projeto e nao leva numero — ao contrario da Meta da Sprint, que
    nasce a cada Sprint. Por isso a busca aqui e' pelo prefixo puro.
    """
    lid = ids.get("ESCOPO")
    return next((c for c in list_cards(lid or 0)
                 if (c.get("titulo") or "").startswith(META_PRODUTO_PREFIXO)),
                None) if lid else None


def texto_da_meta_do_produto(ids):
    """A Meta do Produto em texto — e' o que vai dentro de [INPUT META_PRODUTO].

    Ela e' o compromisso do Product Backlog, entao entra como CONTEXTO em todo passo
    que decide ORDEM ou julga PROGRESSO: a Planning (esta Meta da Sprint aproxima da
    Meta do Produto?), a Review (o Guia Scrum manda inspecionar o progresso RUMO A
    ELA), a repriorizacao (o que sai ainda serve a meta?) e o encerramento (chegamos?).

    Devolve "" quando ela ainda nao existe — o preencher_inputs diz isso com todas as
    letras em vez de fingir que tem o dado.
    """
    cd = cartao_da_meta_do_produto(ids)
    if not cd:
        return ""
    titulo = (cd.get("titulo") or "")
    # O titulo carrega a meta inteira ("🏁 META DO PRODUTO: <frase>"): o que interessa
    # e' a frase, sem o rotulo.
    frase = titulo[len(META_PRODUTO_PREFIXO):].lstrip(" :·—-").strip()
    return frase or entradas.texto_do_cartao(cd.get("descricao", "")).strip()


def cartao_da_dod(ids):
    """O cartao da Definicao de Pronto (lista Concluido), ou None."""
    return next((c for c in list_cards(ids.get("CONCLUIDO") or 0)
                 if (c.get("titulo") or "").startswith(DOD_PREFIXO)),
                None) if ids.get("CONCLUIDO") else None


def texto_da_dod(ids):
    """A Definicao de Pronto em texto — e' o que vai dentro de [INPUT DOD].

    A DoD e' do PROJETO INTEIRO e nao muda a cada Sprint (Eduardo, 2026-07-27: "ela deve
    ser generica para todas as entregas"), entao ela entra como CONTEXTO nos passos que
    julgam "pronto": a Planning (o item cabe na Sprint e atinge a DoD?), a Daily (o que
    esta mais perto de ficar pronto?) e a Review (isto e incremento ou volta ao backlog?).
    Devolve "" quando ela ainda nao existe — o preencher_inputs diz isso com todas as
    letras em vez de fingir que tem o dado.
    """
    cd = cartao_da_dod(ids)
    if not cd:
        return ""
    linhas = []
    try:
        from pa_client import list_checklists
        for chk in list_checklists(cd["id"]):
            for it in (chk.get("itens") or chk.get("checklist_itens") or []):
                txt = (it.get("descricao") or it.get("titulo") or it.get("texto") or "").strip()
                if txt:
                    linhas.append(f"• {txt}")
    except Exception:
        pass
    if not linhas:                     # sem checklist legivel: cai no texto do cartao
        corpo = entradas.texto_do_cartao(cd.get("descricao", "")) or ""
        linhas = [l for l in corpo.splitlines() if l.strip().startswith(("•", "-", "*"))]
    return "\n".join(linhas).strip()


def resolver_inputs(texto, ids, extras=None, respostas=None):
    """O prompt PRONTO + a lista do que ainda falta responder no quadro.

    `extras` = o que nao mora num cartao: [INPUT QUADRO] (a foto viva), [INPUT
    DIAGNOSTICO] (as notas do 01a), [INPUT MUDANCA] (o pedido). Quem os monta e' o
    script do passo — ele sabe.
    """
    from prompts import inputs_do_prompt, preencher_inputs
    extras = extras or {}
    valores, faltando = {}, []
    for tk in inputs_do_prompt(texto):
        if extras.get(tk):
            valores[tk] = extras[tk]
            continue
        if tk == "EQUIPE":
            v = texto_da_equipe(ids)
        elif tk in _LISTAS_FONTE:
            v = texto_da_lista(ids, tk)
        else:
            if respostas is None:
                respostas = respostas_do_termo(ids)
            v = respostas.get(tk)
        if v:
            valores[tk] = v
        else:
            faltando.append(tk)
    return preencher_inputs(texto, valores), faltando


_MARCA_ITEM = _re.compile(r"^\s*[•\-\*]\s+", _re.M)


def _pedacos_de_item(texto):
    """O texto quebrado em UM pedaco por item ("• nome" mais as linhas de detalhe dele)."""
    pos = [m.start() for m in _MARCA_ITEM.finditer(texto or "")]
    if not pos:
        return [texto.strip()] if (texto or "").strip() else []
    return [(texto[a:b]).rstrip() for a, b in zip(pos, pos[1:] + [len(texto)])]


def _nome_do_pedaco(pedaco):
    """O NOME do item — a primeira linha, sem o marcador e sem a etiqueta [grupo]."""
    primeira = (pedaco or "").strip().splitlines()[0] if (pedaco or "").strip() else ""
    primeira = _MARCA_ITEM.sub("", primeira, count=1)
    return _re.sub(r"\s*\[[^\]]*\]", "", primeira).strip()


def acrescentar_ao_texto(atual, novo):
    """`novo` somado ao que ja' existe, SEM repetir item que ja' esta la'.

    E' o que sustenta o bloco aditivo [N+] (ver distribuir_caixa_entrada). O casamento
    e' o mesmo do quadro (palavras_do_titulo): a IA reescreve a mesma pessoa com as
    palavras noutra ordem, e "Henry — infra e ProjectAdm" nao pode voltar como um gemeo
    de "Henry — ProjectAdm e infra".
    """
    atual = (atual or "").strip()
    if not atual:
        return (novo or "").strip()
    ja = {frozenset(palavras_do_titulo(_nome_do_pedaco(p)))
          for p in _pedacos_de_item(atual)}
    ja.discard(frozenset())
    novos = [p for p in _pedacos_de_item(novo)
             if frozenset(palavras_do_titulo(_nome_do_pedaco(p))) not in ja]
    return atual if not novos else (atual + "\n" + "\n".join(novos)).strip()


def distribuir_caixa_entrada(ids, log=print):
    """Le a caixa de entrada, distribui cada bloco [x.y] no cartao certo e limpa.

    Formato aceito (e o que os prompts pedem a IA):
        [1.2] OBJETIVO: ...
        [2.1] • entrega ...
    Um bloco vai ate o proximo rotulo. Blocos sem rotulo conhecido sao ignorados
    (e avisados) — nada e perdido: a caixa so e limpa se algo foi distribuido.

    O BLOCO ADITIVO [N+] (Eduardo, 14/08/2026)
        [3]  SUBSTITUI o cartao 3.       [3+] ACRESCENTA ao cartao 3.
        Nasceu do Processo 2 do PMBOK 8 Together ("Identificar as partes interessadas"),
        que nao repete a pergunta do Termo de Abertura: ele pede a' IA so' QUEM FALTA.
        Colar esse delta como [3] apagaria as partes que o Termo ja' tinha — a secao QUEM
        do proprio Termo encolheria. Com [N+] o cartao CRESCE, que e' o que elaboracao
        progressiva quer dizer, e o item repetido nao entra duas vezes.
    """
    _avisar_rotulos_repetidos(ids, log=log)
    blocos = blocos_da_caixa(ids)
    if not blocos:
        # A UNICA resposta do aluno que NAO vem de uma IA — logo, a unica sem rotulo
        # [x.y] — e a JUSTIFICATIVA: e o texto dele, escrito por ele. Quando ele a cola
        # na Caixa ("COLE AQUI...") sem rotulo, blocos_da_caixa devolve {} e, sem isto, o
        # texto some: o cartao 1.1 fica no guia e o Passo 1 volta a pedir a justificativa
        # — o loop que o aluno vive como "eu ja escrevi e ele nao reconhece" (Eduardo,
        # 2026-07-15). Aqui: texto solto na Caixa + cartao 1.1 ainda no guia => 1.1.
        return _caixa_sem_rotulo_para_justificativa(ids, log=log)
    cards = _cards_inputs(ids)
    consumidos = []
    for rotulo, corpo in blocos.items():
        acrescenta = rotulo.endswith("+")
        chave = rotulo[:-1] if acrescenta else rotulo
        if not _re.match(r"^\d+(\.\d+)?$", chave):
            continue                      # nao e' cartao do termo (ex.: [P2]) — deixa la'
        pref = chave if chave.count(".") else chave + "."
        alvo = next((c for t, c in cards.items() if t.startswith(pref)), None)
        if not alvo or not corpo:
            log(Tf("  [aviso] bloco [{0}] não tem cartão correspondente — ignorado.", rotulo))
            continue
        if acrescenta:
            atual = entradas.texto_do_cartao(alvo.get("descricao"))
            if atual in textos_de_fabrica(alvo.get("titulo") or ""):
                atual = ""                # o cartao ainda esta no texto-guia: nao e' resposta
            corpo = acrescentar_ao_texto(atual, corpo)
        update_card(alvo["id"], descricao=corpo)
        log(Tf("  [caixa -> cartão] [{0}] {1}", rotulo, alvo.get('titulo', '')[:44]))
        consumidos.append(rotulo)
    if consumidos:
        consumir_blocos(ids, consumidos, log=log)
    return len(consumidos)


def _caixa_sem_rotulo_para_justificativa(ids, log=print):
    """Texto SOLTO (sem rotulo [x.y]) na Caixa -> cartao 1.1, se ele ainda for o guia.

    Devolve 1 se distribuiu a justificativa, 0 se nao havia o que fazer. Nunca
    sobrescreve uma resposta que o aluno ja deu no 1.1: nesse caso avisa e deixa o
    texto solto na Caixa (pode ser uma resposta que ele esqueceu de rotular)."""
    cards = _cards_inputs(ids)
    caixa = cards.get(CAIXA)
    if not caixa:
        return 0
    texto = entradas.texto_do_cartao(caixa.get("descricao"))
    if caixa_no_guia(texto):
        return 0                          # Caixa vazia / no texto-guia: nada a fazer
    alvo = next((c for t, c in cards.items() if t.startswith("1.1")), None)
    if not alvo:
        return 0
    atual = entradas.texto_do_cartao(alvo.get("descricao"))
    if atual and atual not in textos_de_fabrica(alvo.get("titulo") or ""):
        log(T("  [aviso] há texto sem rótulo na Caixa de entrada, mas o cartão 1.1 já está "
            "respondido — deixei o texto na Caixa. Se era outra resposta, cole-a com o "
            "rótulo que o prompt pede ([1.2], [2.1]...)."))
        return 0
    update_card(alvo["id"], descricao=texto)
    log(Tf("  [caixa -> cartão] texto sem rótulo reconhecido como a justificativa → {0}", (alvo.get('titulo') or '')[:40]))
    update_card(caixa["id"], descricao=_CAIXA_DESC)   # Caixa consumida: volta ao guia
    return 1


def caixa_no_guia(texto):
    """A Caixa ainda esta como veio de fabrica — vazia ou so' com o NOSSO texto-guia?

    Ate 2026-08-04 os tres lugares que faziam esta pergunta escreviam
    `texto.startswith("COLE AQUI")` — o prefixo do texto-guia EM PORTUGUES. No pacote EN
    a Caixa nasce com "PASTE HERE your AI's answer...", o teste dava False e o proprio
    texto-guia da Caixa passava por resposta do aluno: sem rotulo [x.y] para distribuir,
    o Passo 1 caia no `_caixa_sem_rotulo_para_justificativa` e gravava o guia da Caixa
    dentro do cartao 1.1 como "a justificativa". Dai em diante o painel dizia
    "[OK] 1.1 Justificativa (POR QUE)" para quem nunca escreveu uma (Eduardo, 2026-08-04,
    quadro FIRE em ingles).

    A pergunta certa nao e' "comeca com esta frase em portugues?" e sim "isto e' texto
    NOSSO?" — e a resposta vem dos DOIS idiomas, porque um quadro montado pelo pacote PT
    pode acabar sendo lido pelo pacote EN (e vice-versa).
    """
    t = (texto or "").strip().upper()
    if not t:
        return True
    return any(t.startswith(a) for a in _CAIXA_ABERTURAS)


def _avisar_rotulos_repetidos(ids, log=print):
    """DOIS blocos com o MESMO rotulo? Avisa — antes que um suma sem ninguem ver.

    POR QUE ISTO EXISTE (Eduardo, 20/08/2026, quadro 341)
        O curso convida a ouvir DUAS IAs (e a opcao C existe para consolidar as duas).
        Mas quem colava as duas respostas na Caixa e rodava o passo direto perdia uma:
        `blocos_da_caixa` devolve um DICT {rotulo: texto}, entao dois blocos [1.2]
        colapsam e **so o ULTIMO sobrevive** — sem erro, sem aviso, sem rastro. O aluno
        ve o passo dizer "[caixa -> cartao] [1.2]" e conclui que as duas entraram.

        Foi o que aconteceu na validacao do quadro 341: DeepSeek colada primeiro, Gemini
        depois; a do DeepSeek seria descartada em silencio.

    NAO BLOQUEIA NADA. Colapsar pode ser exatamente o que a pessoa quis (colar de novo
    uma resposta corrigida, por cima da anterior). Quem decide e ela; o que nao pode e'
    decidir sem saber. Por isso: avisa, diz qual sobrevive e aponta a opcao C.
    """
    try:
        caixa = _cards_inputs(ids).get(CAIXA)
        if not caixa:
            return
        texto = entradas.texto_do_cartao(caixa.get("descricao"))
        if caixa_no_guia(texto):
            return
        vistos = _re.findall(r"^\s*\[([A-Za-z0-9_.+]+)\]", texto, flags=_re.M)
        repetidos = sorted({r.upper() for r in vistos if vistos.count(r) > 1})
        for rot in repetidos:
            n = sum(1 for r in vistos if r.upper() == rot)
            log(Tf("  [aviso] a Caixa tem {0} blocos [{1}] — só o ÚLTIMO vai para o cartão.",
                   n, rot))
            log(T("          Quer juntar as respostas em vez de escolher uma? "
                  "Feche e use a opção C (consolidar) antes de rodar o passo."))
    except Exception:
        return                            # aviso nunca pode derrubar o passo


# As duas formas do aviso de corte que pa_client._capar_descricao escreve no
# fim de uma descricao que nao coube. Ficam aqui porque e' aqui que a Caixa e'
# LIDA de volta; quem as escreve e' o pa_client.
_CORTE_MARCAS = ("texto muito longo", "text too long")

def _avisar_caixa_cortada(texto, log=print):
    """A Caixa chegou CORTADA? Diz — antes que meio plano vire "passo concluído".

    POR QUE ISTO EXISTE (Eduardo, quadro 341, 21/08/2026)
        O cartao do ProjectAdm tem teto de descricao (~10 000 bytes; capamos em 8 000 —
        ver pa_client._capar_descricao). Um [P4] de 32 pacotes de trabalho NAO cabe. O
        que aconteceu: o CRONOGRAMA foi cortado no meio de um item — nasceu um pacote
        novo chamado "Series reachab" — e a secao ORCAMENTO inteira se perdeu. O Passo 4
        rodou ate o fim e imprimiu "Passo 4 concluido"; so' a auditoria, mais abaixo,
        murmurou que o orcamento "ainda nao existe".

    NAO BLOQUEIA NADA (mesma regra do _avisar_rotulos_repetidos): o que ja chegou pode
    ser exatamente o que a pessoa queria gravar. O que nao pode e' ela nao saber.
    """
    try:
        if not texto or not any(m in texto for m in _CORTE_MARCAS):
            return False
        log(T("  [aviso] a Caixa de entrada chegou CORTADA — a resposta não coube no "
              "cartão."))
        log(T("          O que veio depois do corte NÃO existe para este passo (é assim "
              "que um orçamento inteiro some sem erro nenhum)."))
        log(T("          Cole a resposta em DUAS vezes: a primeira seção, rode o passo, "
              "depois a seção seguinte e rode o passo de novo."))
        return True
    except Exception:
        return False                      # aviso nunca pode derrubar o passo


def blocos_da_caixa(ids):
    """{rotulo: texto} do que o aluno colou na Caixa de entrada.

    A CAIXA E' A UNICA PORTA DE ENTRADA do aluno CHAT: toda resposta da IA chega por
    aqui, rotulada, e o script a leva para o lugar certo do quadro.
        [1.2] -> o cartao 1.2 do termo de abertura (passo 1)
        [P2]  -> a lista de escopo: EAP ou Product Backlog (passo 2)
    Cada passo consome os SEUS blocos e deixa os outros na caixa — por isso o passo 1
    nao pode limpar um [P2] que ainda nao foi usado.
    """
    caixa = _cards_inputs(ids).get(CAIXA)
    if not caixa:
        return {}
    # O editor do ProjectAdm devolve "<p>[1.2]</p><p>OBJETIVO 1: ...</p>": sem limpar o
    # HTML, o rotulo [1.2] nao fica no inicio da linha, o split nao acha NADA e a caixa
    # nunca e' distribuida — o aluno fica preso repetindo o mesmo passo.
    texto = entradas.texto_do_cartao(caixa.get("descricao"))
    _avisar_caixa_cortada(texto)
    if caixa_no_guia(texto):
        return {}
    # O "+" no conjunto de caracteres e' o bloco ADITIVO ([3+] acrescenta ao cartao 3,
    # [3] substitui) — ver distribuir_caixa_entrada.
    partes = _re.split(r"^\s*\[([A-Za-z0-9_.+]+)\]\s*", texto, flags=_re.M)
    if len(partes) < 3:
        return {}
    return {partes[i].strip().upper(): partes[i + 1].strip()
            for i in range(1, len(partes) - 1, 2) if partes[i + 1].strip()}


def blocos_multiplos_da_caixa(ids, rotulo):
    """TODOS os blocos com o mesmo rotulo — na ordem em que aparecem na caixa.

    blocos_da_caixa() devolve um dict {rotulo: texto}: se o aluno cola varios blocos com
    o MESMO rotulo (o caso do Passo 9, quando a IA analisa 4 mudancas de uma vez), o
    dict os colapsa e so' o ULTIMO sobrevive — foi por isso que o Passo 9 registrava
    "so' uma" mudanca (Eduardo, 2026-07-15). Aqui a gente re-divide o texto cru e junta
    TODAS as ocorrencias daquele rotulo. Separadores soltos entre blocos (uma linha so'
    de tracos, '---') sao aparados."""
    caixa = _cards_inputs(ids).get(CAIXA)
    if not caixa:
        return []
    texto = entradas.texto_do_cartao(caixa.get("descricao"))
    if caixa_no_guia(texto):
        return []
    partes = _re.split(r"^\s*\[([A-Za-z0-9_.]+)\]\s*", texto, flags=_re.M)
    alvo = (rotulo or "").upper()
    out = []
    for i in range(1, len(partes) - 1, 2):
        if partes[i].strip().upper() != alvo:
            continue
        corpo = _re.sub(r"\n[ \t]*[-–—]{2,}[ \t]*$", "", partes[i + 1].rstrip()).strip()
        if corpo:
            out.append(corpo)
    return out


def consumir_blocos(ids, rotulos, log=print):
    """Tira da caixa os blocos ja usados — e SO' eles. O que sobrou continua la',
    esperando o passo dono dele."""
    caixa = _cards_inputs(ids).get(CAIXA)
    if not caixa or not rotulos:
        return
    restantes = {r: t for r, t in blocos_da_caixa(ids).items()
                 if r.upper() not in {x.upper() for x in rotulos}}
    if restantes:
        texto = "\n\n".join(f"[{r}]\n{t}" for r, t in restantes.items())
        update_card(caixa["id"], descricao=texto)
        log(Tf("  Caixa de entrada: {0} bloco(s) usado(s); {1} guardado(s) para o próximo "
               "passo.", len(rotulos), len(restantes)))
    else:
        update_card(caixa["id"], descricao=_CAIXA_DESC)
        log(Tf("  Caixa de entrada distribuída ({0} bloco(s)) e limpa.", len(rotulos)))


def ler_inputs(ids):
    """Le os cartoes da lista INPUTS -> dict pronto para os scripts.

    Devolve so o que o aluno JA preencheu (o texto-guia de fabrica e ignorado).
    """
    dados = {}
    for titulo, card in _cards_inputs(ids).items():
        # HTML do editor -> texto. Sem isto, a resposta do aluno chega ao prompt (e ao
        # documento que o passo gera) embrulhada em <p>...</p>.
        desc = entradas.texto_do_cartao(card.get("descricao"))
        if not desc or titulo == CAIXA:
            continue
        for pref, (chave, parser) in MAPA_INPUTS.items():
            if not titulo.startswith(pref):
                continue
            # texto-guia de fabrica (o cartao ainda nao foi respondido) -> ignora.
            # Conferimos contra TODOS os textos que ja escrevemos naquele cartao
            # (neutro da etapa 1, trancado, preditivo, agil) — senao o guia de uma
            # etapa anterior seria lido como se fosse resposta do aluno.
            if desc in textos_de_fabrica(titulo):
                break
            valor = parser(desc) if parser else desc
            if valor:
                dados[chave] = valor
            break
    return dados


# ─────────────────────────────────────────────────────────────────────────────
# O IDIOMA DO CURSO — as tabelas acima sao a versao PORTUGUESA
#
# Aqui nao ha frase de console: ha o QUADRO do aluno — o nome de cada lista, os 14
# cartoes do termo de abertura, os textos-guia. Sao o produto, e mudam juntos: um
# titulo de lista traduzido sem o texto-guia correspondente entrega um quadro pela
# metade. Por isso vao em TABELA PARALELA (quadro_en.py), revisavel lado a lado, e
# nao em catalogo de frases.
#
# `mesclar` junta CHAVE A CHAVE: o que ainda nao foi traduzido sai em portugues e o
# quadro continua sendo montado inteiro.
#
# CUIDADO QUE NAO E' OBVIO — o nome da lista e' TAMBEM a chave de busca no quadro
# (find_list_id_by_name) e o cartao do termo e' casado pelo TITULO. Traduzir os dois
# e' coerente: o pacote EN cria em ingles e procura em ingles. O que NAO pode e' um
# aluno trocar IDIOMA no meio do curso — o quadro dele ficaria com metade dos nomes
# em cada lingua. Os TITULOS_LEGADOS/LEGADO cobrem renomeacao entre VERSOES, nao
# entre idiomas.
# ─────────────────────────────────────────────────────────────────────────────
from idioma import mesclar                                          # noqa: E402

try:
    import quadro_en as _QEN                                        # noqa: E402
except ImportError:                       # pacote PT: o arquivo nem viaja
    _QEN = None


def _qen(nome):
    return getattr(_QEN, nome, None) if _QEN else None


COMUNS_ANTES = mesclar(_COMUNS_ANTES_PT, _qen('COMUNS_ANTES'))
ESCOPO = mesclar(_ESCOPO_PT, _qen('ESCOPO'))
COMUNS_MEIO = mesclar(_COMUNS_MEIO_PT, _qen('COMUNS_MEIO'))
FLUXO_AGIL = mesclar(_FLUXO_AGIL_PT, _qen('FLUXO_AGIL'))
COMUNS_DEPOIS = mesclar(_COMUNS_DEPOIS_PT, _qen('COMUNS_DEPOIS'))
MARCA_EXEC_P7 = mesclar(_MARCA_EXEC_P7_PT, _qen('MARCA_EXEC_P7'))
MARCA_NOVO_P4 = mesclar(_MARCA_NOVO_P4_PT, _qen('MARCA_NOVO_P4'))
CAIXA = mesclar(_CAIXA_PT, _qen('CAIXA'))
_CAIXA_DESC = mesclar(_CAIXA_DESC_PT, _qen('CAIXA_DESC'))

# O texto-guia da Caixa em TODOS os idiomas do pacote — nao so' no idioma corrente.
# Reconhecer "isto e' o nosso guia" nao pode depender da lingua em que o quadro nasceu:
# quem monta em PT e abre em EN (ou o contrario) tem o mesmo quadro, e o texto continua
# sendo NOSSO. Derivado da tabela, e nao escrito a mao, para nao voltar a envelhecer
# quando o guia mudar de redacao ou entrar um terceiro idioma.
_CAIXA_FABRICA = tuple(d.strip() for d in (_CAIXA_DESC_PT, _qen('CAIXA_DESC'))
                       if (d or "").strip())
# As duas primeiras palavras de cada guia ("COLE AQUI", "PASTE HERE"): a marca que o
# caixa_no_guia procura no comeco do cartao. Duas palavras, e nao a linha inteira, para
# manter exatamente a tolerancia que o codigo tinha em portugues — o aluno pode ter
# apagado o resto do guia e deixado a primeira linha.
_CAIXA_ABERTURAS = tuple({" ".join(d.split()[:2]).upper() for d in _CAIXA_FABRICA})

# O texto-guia da JUSTIFICATIVA (a "zona de valores iniciais" do 01_iniciar_projeto.py),
# nas duas linguas — mesmo criterio bilingue do _PLACEHOLDERS_PROJETO. Ele nao devia
# nunca chegar a um cartao, mas a versao EN de ate 04/08/2026 o gravava no 1.1 como se
# fosse a justificativa do aluno (o guarda era o prefixo PORTUGUES "escreva aqui"). Aqui
# ele volta a contar como texto NOSSO: o quadro de quem rodou aquela versao se conserta
# sozinho no Passo 1 seguinte, em vez de carregar para sempre um 1.1 "respondido".
_PLACEHOLDERS_JUSTIFICATIVA = (
    "Escreva aqui, com as suas palavras, por que este projeto existe.",   # PT
    "Write here, in your own words, why this project exists.",            # EN
)
_TERMO_PREDITIVA = mesclar(_TERMO_PREDITIVA_PT, _qen('TERMO_PREDITIVA'))
_TERMO_AGIL = mesclar(_TERMO_AGIL_PT, _qen('TERMO_AGIL'))
_TERMO_BASE = mesclar(_TERMO_BASE_PT, _qen('TERMO_BASE'))
GUIA_TITULO = mesclar(_GUIA_TITULO_PT, _qen('GUIA_TITULO'))
# O titulo do cartao-guia em TODOS os idiomas do pacote — a base do e_guia(). Ver la' o
# porque de a pergunta viver num lugar so'.
_GUIA_TITULOS = tuple({t.strip() for t in (_GUIA_TITULO_PT, _qen('GUIA_TITULO'))
                       if (t or "").strip()})

# Cartoes da lista PARTES que NAO sao uma parte interessada: o Guia e os tres cartoes
# de GRUPO (cada parte entra como subtarefa de um deles). Quatro arquivos precisavam
# reconhece-los — 01 (para nao recriar), 06 (para nao contar), artefatos (para nao
# listar) e reset (para NAO APAGAR) — e os quatro traziam a tupla escrita a mao, em
# portugues. Num quadro em ingles o Guia se chama "Guide to this list": o reset nao o
# reconhecia como protegido e o APAGAVA. Agora a tupla nasce uma vez so', ja' no
# idioma do aluno, e os quatro leem daqui.
#
# O Guia entra em TODOS os idiomas (nao so' no corrente): um quadro montado pelo pacote
# PT e aberto pelo pacote EN tem o cartao "Guia desta lista", e proteger so' o titulo do
# idioma corrente devolveria o reset ao mesmo apagao. Os tres cartoes de GRUPO seguem no
# idioma do pacote porque quem os cria e' o proprio 01 — e ele nao muda de lingua no
# meio do curso.
PROTEGIDOS_PARTES = _GUIA_TITULOS + (T("CLIENTES"), T("EQUIPE"), T("DEMAIS"))
_GUIAS_ESCOPO = mesclar(_GUIAS_ESCOPO_PT, _qen('GUIAS_ESCOPO'))
_GUIAS = mesclar(_GUIAS_PT, _qen('GUIAS'))
_ROT_PROB = mesclar(_ROT_PROB_PT, _qen('ROT_PROB'))
_ROT_IMP = mesclar(_ROT_IMP_PT, _qen('ROT_IMP'))
META_PREFIXO = mesclar(_META_PREFIXO_PT, _qen('META_PREFIXO'))
META_PRODUTO_PREFIXO = mesclar(_META_PRODUTO_PREFIXO_PT, _qen('META_PRODUTO_PREFIXO'))
DOD_PREFIXO = mesclar(_DOD_PREFIXO_PT, _qen('DOD_PREFIXO'))
_ROTULO_SNAPSHOT = mesclar(_ROTULO_SNAPSHOT_PT, _qen('ROTULO_SNAPSHOT'))

# Os titulos de lista de OUTRO idioma entram como legado: um quadro montado pelo
# pacote PT que passe a ser aberto pelo pacote EN e' RENOMEADO no lugar, em vez de
# ganhar uma segunda lista com o mesmo papel (os cartoes do aluno nunca se perdem).
if _QEN is not None:
    for _ch, _nome in (_COMUNS_ANTES_PT + _COMUNS_MEIO_PT + _FLUXO_AGIL_PT
                       + _COMUNS_DEPOIS_PT + list(_ESCOPO_PT.values())):
        TITULOS_LEGADOS.setdefault(_ch, [])
        if _nome not in TITULOS_LEGADOS[_ch]:
            TITULOS_LEGADOS[_ch].append(_nome)
