"""
Project Together — powered by AI: PASSO 4 — Cronograma e orçamento.

QUANDO e QUANTO, agora com número. O cronograma NÃO é um documento à parte: ele
vive nos cartões do escopo (cada pacote/história ganha data de início e de fim). O
orçamento vira cartões na lista "Orçamento [QUANTO]", um por item — com uma reserva
de contingência proporcional aos riscos do Passo 3.

PREDITIVA: datas e dependências nos pacotes da EAP.
ÁGIL:      cada história recebe a Sprint em que entra e as suas datas (a data é fixa;
           o escopo é que flutua).

O RITMO (modo CHAT): o prompt do Passo 4 já foi para a sua área de transferência no
fim do Passo 3. Cole na IA → cole a resposta ([P4]) na "0. Caixa de entrada" → rode
este passo.
"""
import re

import entradas
import passos
import prompts
import quadro
import team_check
from concurrent.futures import ThreadPoolExecutor as _Pool
from pa_client import (list_cards, update_card, get_card, garantir_dependencia,
                       assign_member, unassign_member)
from runner import ok, step, warn
from idioma import T, Tf


def _mapa_escopo(ids):
    """{nome_sem_acento: cartao} da lista ESCOPO (fora o cartão-guia)."""
    return {entradas._sem_acento(cd.get("titulo", "")): cd
            for cd in list_cards(ids["ESCOPO"])
            if not quadro.e_guia((cd.get("titulo") or ""))}


# O CÓDIGO do item de backlog (B1, B2…) é a alça ESTÁVEL do item — é assim que o próprio
# prompt do Passo 2 o apresenta ao aluno: "é por ele que os próximos passos (riscos,
# Sprints, execução) vão se referir a este item". Então o Passo 4 tem de casar por ele.
#
# Por que isto existe (Eduardo, 2026-07-27: "rodei o passo 4 e o B10 foi duplicado"): ele
# criou o B10 À MÃO no quadro, com travessão — "B10 — Plano alimentar individualizado em
# prática" — e o [P4] veio com dois-pontos, "B10: Plano alimentar ... em pratica". O match
# era pelo TÍTULO INTEIRO sem acento: o acento some, o SEPARADOR não. Não casou, e o
# `else` lá embaixo criou um cartão novo em silêncio. Casar pelo código resolve os dois
# desencontros de uma vez (separador e acento) — e é o que o curso já promete ao aluno.
_COD_ITEM = re.compile(r"^\s*(B\d+)\s*[:\-—–·|)]?\s", re.I)


def _codigo_do_item(nome):
    """'B10: Plano...' ou 'B10 — Plano...' -> 'b10'. Sem código -> '' (nunca casa)."""
    m = _COD_ITEM.match(nome or "")
    return m.group(1).lower() if m else ""


def _mapa_codigos(escopo):
    """{codigo: cartao} dos itens do escopo que têm código (B1, B2…). Código repetido
    fica de fora: com dois candidatos não dá para saber qual é o certo, e datar o cartão
    errado é pior do que avisar que não achei."""
    por_cod, repetidos = {}, set()
    for cd in escopo.values():
        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


# As funções que viviam aqui — _formas_do_nome, _equipe_usuarios, _achar_usuario e
# _atribuir_unico — foram para o `team_check` em 07/08/2026, como contas_da_equipe,
# conta_do_responsavel e atribuir_responsavel. Motivo: o Passo 7 passou a precisar da
# MESMA decisão ("de quem é este trabalho?"), e duas implementações da mesma decisão
# são dois bugs diferentes. O casamento também ficou estrito no caminho — o daqui era
# por substring crua. Guarda: tests/execution_pessoa_certa.py.


# Tipo de dependência (o curso ensina TI/II/TT/IT; ver o deck "MS Project Avançado",
# slide 8). Código PT/EN/por-extenso -> tipo da API do ProjectAdm. TI (Término->Início)
# é o padrão — o mais comum. II/TT/IT servem para tarefas que correm em paralelo.
_TIPO_DEP = {
    "": "finish_to_start", "ti": "finish_to_start", "fs": "finish_to_start",
    "termino-inicio": "finish_to_start", "termino a inicio": "finish_to_start",
    "ii": "start_to_start", "ss": "start_to_start",
    "inicio-inicio": "start_to_start", "inicio a inicio": "start_to_start",
    "tt": "finish_to_finish", "ff": "finish_to_finish",
    "termino-termino": "finish_to_finish", "termino a termino": "finish_to_finish",
    "it": "start_to_finish", "sf": "start_to_finish",
    "inicio-termino": "start_to_finish", "inicio a termino": "start_to_finish",
}


def _tipo_dependencia(cod):
    """Código do tipo (TI/II/TT/IT, FS/SS/FF/SF ou por extenso) -> tipo da API.
    Desconhecido/vazio -> Término-a-Início (o padrão)."""
    chave = entradas._sem_acento(cod or "").lower().strip()
    chave = re.sub(r"\s*(->|→|-)\s*", "-", chave)
    return _TIPO_DEP.get(chave, "finish_to_start")


def _predecessores(dep):
    """Um campo "depende de" pode listar VÁRIOS predecessores, cada um com um TIPO
    opcional entre parênteses: 'A (II), B; C (TT)' -> [('A','start_to_start'),
    ('B','finish_to_start'), ('C','finish_to_finish')]. Sem tipo => Término-a-Início
    (o padrão). NÃO quebramos em ' e ' de propósito — um nome de pacote pode conter 'e'
    ('Testes e Validação'). Antes toda a string virava uma chave única e nunca casava."""
    out = []
    for tok in re.split(r"\s*[,;]\s*", dep or ""):
        tok = tok.strip()
        if not tok or tok == "-":
            continue
        m = re.search(r"\(([^)]*)\)\s*$", tok)          # tipo opcional no fim: "(II)"
        tipo = _tipo_dependencia(m.group(1)) if m else "finish_to_start"
        nome = re.sub(r"\s*\([^)]*\)\s*$", "", tok).strip()
        if nome:
            out.append((nome, tipo))
    return out


def _achar_pacote(chave, escopo):
    """O cartão do escopo que casa com 'chave' (um nome de pacote), tolerante.

    O prompt pede o nome EXATO e o match exato (sem acento) resolve quase tudo. Mas em
    "depende de" a IA CITA outro pacote e às vezes o encurta ('Scripts Python Validados'
    por 'Scripts Python Validados e Testados'); aí caímos para um match por conteúdo, e
    só quando ele é ÚNICO — uma seta errada no Gantt é pior que uma seta faltando."""
    alvo = entradas._sem_acento(chave)
    if not alvo:
        return None
    if alvo in escopo:
        return escopo[alvo]
    if len(alvo) >= 4:
        cands = [cd for k, cd in escopo.items() if alvo in k or k in alvo]
        if len(cands) == 1:
            return cands[0]
    return None


# ── O pacote que NASCE no Passo 4 (elaboração progressiva) ───────────────────
# Estimar duração revela trabalho que ninguém tinha listado — e isso é BOM: é o detalhe
# aparecendo quando a gente chega perto. O defeito era outro (Eduardo, 2026-07-29: "quando
# altero algum elemento que esqueci no meu cronograma, pode gerar um novo elemento da
# EAP"): o cartão nascia SEM PAI, e um cartão sem pai é uma RAIZ — virava uma FASE no fim
# da EAP, com código de primeiro nível, fora de toda auditoria de pacote (data, dono,
# critério de aceitação só são cobrados de quem é 'pacote'). Agora ele nasce no lugar
# certo: pacote de trabalho DENTRO da entrega que a IA indicar (campo "entrega:") — ou da
# que você escolher aqui.
def _entregas_da_eap(ids):
    """As ENTREGAS da EAP (os cartões que podem receber um pacote novo), na ordem da
    árvore. [] quando ainda não há EAP de dois níveis (ou na abordagem ágil)."""
    try:
        nos, _tem_fases = quadro.arvore_da_eap(ids)
        return [n for n in nos if n["tipo"] == "entrega"]
    except Exception:
        return []


def _escolher_entrega(nome, entregas, sugestao=""):
    """A entrega que vai receber o pacote novo `nome`. 1º o que a IA disse (`sugestao`,
    casada de forma tolerante); se não bater, PERGUNTA — em vez de pendurar no lugar
    errado em silêncio. Enter (ou sem terminal) devolve None: o cartão nasce solto, como
    antes, e a auditoria vai cobrar isso depois."""
    por_titulo = {entradas._sem_acento(e["titulo"]): e for e in entregas}
    if sugestao:
        alvo = entradas._sem_acento(sugestao)
        e = por_titulo.get(alvo)
        if not e and len(alvo) >= 5:
            cands = [x for k, x in por_titulo.items() if alvo in k or k in alvo]
            e = cands[0] if len(cands) == 1 else None
        if e:
            return e
    print(Tf('\n  🆕 "{0}" não existe na sua EAP — é trabalho que apareceu agora,', nome[:60]))
    print(T("     ao estimar o cronograma. Em qual ENTREGA ele entra?"))
    for i, e in enumerate(entregas[:20], 1):
        print(f"       {i:>2}. {e['codigo']}  {e['titulo'][:52]}")
    try:
        resp = input(T("     Número da entrega (Enter = deixar solto na EAP): ")).strip()
    except (EOFError, KeyboardInterrupt):
        resp = ""
    if resp.isdigit() and 1 <= int(resp) <= len(entregas[:20]):
        return entregas[int(resp) - 1]
    return None


def _criar_pacote(lista_id, titulo, desc, pai_id=None):
    """Cria o cartão do pacote (como filho de `pai_id`, quando há). Devolve o id ou None."""
    from pa_client import api
    corpo = {"lista_id": lista_id, "titulo": titulo[:255], "descricao": desc}
    if pai_id:
        corpo["cartao_pai_id"] = pai_id
    try:
        return api("POST", "/cartoes", corpo)["dados"]["id"]
    except Exception as e:
        warn(Tf('não consegui criar "{0}" na EAP: {1}', titulo[:34], e))
        return None


def _e_linha_de_total(nome):
    """A IA às vezes emite uma linha de TOTAL/SUBTOTAL como se fosse um item de custo
    (o Eduardo viu 'Total Estimado (desembolso direto)' virar cartão de R$ 9.100 no
    board 270). Isso infla o orçamento — o total quem calcula é o script."""
    n = entradas._sem_acento(nome or "").strip().lower()
    return n.startswith(("total", "subtotal", "soma ", "somatorio", "valor total",
                         "custo total", "orcamento total"))


def _reconciliar_orcamento(ids, titulos_atuais):
    """O orçamento é REGENERADO pela IA a cada rodada. Remove da lista Orçamento os itens
    de uma rodada ANTERIOR que não estão mais no orçamento atual — senão eles ACUMULAM
    (o Eduardo re-rodou com IAs diferentes e o board 270 ficou com 17 itens de 2 rodadas,
    total inflado). Só apaga cartão com a NOSSA forma (uma linha 'Valor:' na descrição),
    nunca o cartão-guia nem um bilhete escrito à mão pelo aluno. Devolve quantos removeu."""
    from pa_client import list_cards, delete_card
    lid = ids.get("ORCAMENTO")
    if not lid:
        return 0
    n = 0
    for cd in list_cards(lid):
        titulo = (cd.get("titulo") or "").strip()
        if not titulo or titulo in titulos_atuais or quadro.e_guia(titulo):
            continue
        corpo = entradas.texto_do_cartao(cd.get("descricao", "")) or ""
        if re.search(r"(?im)^\s*valor\s*:", corpo) and delete_card(cd["id"]):
            n += 1
    return n


N = 4
c, ids, abordagem = passos.abrir(N)
bloco = passos.ler_bloco(ids, N, T("nenhum cronograma nem orçamento"))

secs = entradas.secoes(bloco, ["CRONOGRAMA", "ORCAMENTO", "ORÇAMENTO"])
crono = entradas.itens(secs.get("cronograma", ""))
orcamento = entradas.itens(secs.get("orcamento", ""))
if not crono and not orcamento:
    raise SystemExit(T("Não achei CRONOGRAMA nem ORÇAMENTO no bloco [P4]. Confira o "
                     "formato (as duas seções, cada item num •) e cole de novo."))

# ── 0) A GUARDA DA EQUIPE: a IA não inventa gente ──
# ANTES de escrever qualquer coisa. Um responsável inventado gravado no cartão vira um
# plano com dono falso — e só o Passo 5 o encontraria, um dia depois (quadro 324:
# 13 pacotes com "responsável: Ana", uma pessoa que não existe). Aqui o aluno decide na
# hora, uma pergunta por nome, e o quadro nasce certo.
team_check.conferir(crono, ids, o_que=T("pacote(s) de trabalho"))

# ── 1) CRONOGRAMA: as datas entram nos cartões do escopo (o quadro É o cronograma) ──
escopo = _mapa_escopo(ids)
por_codigo = _mapa_codigos(escopo)      # B1, B2… — a alça estável do item de backlog
# Quem é quem: cada membro da EQUIPE que o aluno associou a um usuário do ProjectAdm. É
# com isto que o responsável de cada pacote vira uma atribuição REAL no cartão da tarefa.
# Desde 07/08/2026 o mapa e o casamento vêm do `team_check`: ele é o dono da pergunta
# "quem é esta pessoa?", e a mesma resposta serve ao Passo 1 (ligar a conta), a este
# passo e ao Passo 7. O casamento de antes daqui era por substring crua (`k in r`), da
# mesma família do "ana" dentro de "m-ANA-ger" — e este caminho só passou a rodar de
# verdade quando o Passo 1 começou a ligar as contas.
equipe = team_check.contas_da_equipe(ids)
equipe_uids = {c["id"] for c in equipe.values()}
datados = atribuidos = 0
novos_crono = []      # [(nome, codigo_da_entrega, titulo_da_entrega)] — trabalho que NASCEU aqui
entregas_eap = None   # lido só quando (e se) aparecer um item novo (economiza uma varredura)
sem_usuario = set()   # responsáveis do cronograma que não batem com nenhum membro do time
predecessores = []   # (nome_do_pacote, nome_do_predecessor) -> vira dependência REAL
for it in crono:
    nome = it.get("_", "")
    # 1º o título inteiro (o caminho normal); 2º o CÓDIGO do item de backlog, que
    # sobrevive a separador e acento diferentes entre o quadro e a resposta da IA.
    alvo = escopo.get(entradas._sem_acento(nome)) or por_codigo.get(_codigo_do_item(nome))
    ini, fim = it.get("inicio", ""), it.get("fim", "")
    dep = (it.get("depende de") or "").strip()
    if dep and dep not in ("-", ""):
        predecessores.append((nome, dep))
    extra = []
    if it.get("duracao"):
        extra.append(Tf("duração estimada: {0}", it['duracao']))
    if it.get("sprint"):
        extra.append(f"Sprint {it['sprint']}")
    if dep and dep not in ("-", ""):
        extra.append(Tf("depende de: {0}", dep))
    if it.get("responsavel"):
        extra.append(Tf("responsável: {0}", it['responsavel']))
    linha_extra = ("\n" + " · ".join(extra)) if extra else ""
    if alvo:
        campos = {}
        if ini:
            campos["data_inicio"] = ini
        if fim:
            campos["data_vencimento"] = fim
        if linha_extra:
            base = entradas.texto_do_cartao(alvo.get("descricao", ""))
            if linha_extra.strip() not in base:
                campos["descricao"] = (base + linha_extra).strip()
        if campos:
            try:
                update_card(alvo["id"], **campos)
                datados += 1
            except Exception as e:
                warn(Tf('não consegui datar "{0}": {1}', nome[:34], e))
        # RESPONSÁVEL -> atribuição REAL: o usuário do membro da equipe vira membro do
        # cartão da tarefa (aparece no Gantt), além do texto na descrição.
        resp = (it.get("responsavel") or "").strip()
        if resp and equipe:
            conta = team_check.conta_do_responsavel(resp, equipe)
            if conta:
                team_check.atribuir_responsavel(alvo["id"], conta, equipe_uids)
                atribuidos += 1
            elif not team_check.e_pendente(entradas._sem_acento(resp)):
                sem_usuario.add(resp)
    else:
        # Item que não bate com nenhum cartão do escopo: é trabalho NOVO, descoberto ao
        # estimar (elaboração progressiva). Nasce como PACOTE DENTRO de uma entrega — e a
        # entrega vem do campo "entrega:" do [P4] ou da sua escolha (nunca de um chute).
        if entregas_eap is None:
            entregas_eap = _entregas_da_eap(ids)
        pai = _escolher_entrega(nome, entregas_eap,
                                (it.get("entrega") or "").strip()) if entregas_eap else None
        # ATENÇÃO ao texto: NÃO escreva aqui a expressão "critério de aceitação" nem como
        # placeholder. O leitor da EAP (quadro._aceitacao_do_pacote) casa essa expressão e
        # capturaria o texto de aviso COMO SE fosse o critério — e aí a auditoria pararia
        # de cobrar o que está justamente faltando.
        desc = (quadro.MARCA_NOVO_P4 + "\n"
                + Tf("Pacote de trabalho que não estava na EAP do Passo 2 — apareceu ao "
                     "estimar o cronograma.{0}\n"
                     "Ainda falta dizer o que prova que ele ficou pronto: volte ao Passo 2.",
                     linha_extra))
        cid = _criar_pacote(ids["ESCOPO"], nome, desc, pai["id"] if pai else None)
        if cid:
            campos = {}
            if ini:
                campos["data_inicio"] = ini
            if fim:
                campos["data_vencimento"] = fim
            if campos:
                try:
                    update_card(cid, **campos)
                except Exception:
                    pass
            novos_crono.append((nome, pai["codigo"] if pai else "", pai["titulo"] if pai else ""))
_novos = Tf(" · {0} novo(s)", len(novos_crono)) if novos_crono else ""
ok(Tf("Cronograma: {0} cartão(ões) do escopo com datas{1}.", datados, _novos))
if novos_crono:
    # O ESCOPO CRESCEU — e isso tem de ser dito em voz alta, com o nome certo. Antes da
    # linha de base (Passo 5) é ELABORAÇÃO PROGRESSIVA: o detalhe aparece quando a gente
    # chega perto, e o lugar dele é o Passo 2. Depois da linha de base aprovada é uma
    # MUDANÇA — e mudança tem processo (Passo 9), senão a baseline vira ficção.
    import progresso as _prog
    _feitos = _prog.feitos()
    print()
    warn(Tf("O SEU ESCOPO CRESCEU: {0} pacote(s) de trabalho não estavam na EAP do Passo 2 e "
            "nasceram agora, ao estimar o cronograma:", len(novos_crono)))
    for nome_novo, cod_pai, tit_pai in novos_crono[:8]:
        onde = (Tf("dentro de {0} {1}", cod_pai, tit_pai[:34]) if cod_pai
                else T("SOLTO na EAP (sem entrega-pai)"))
        print(f"       • {nome_novo[:52]}  →  {onde}")
    if len(novos_crono) > 8:
        print(Tf("       … e mais {0}.", len(novos_crono) - 8))
    print(T("     Se algum destes JÁ existia com outro nome, você tem o mesmo trabalho em"))
    print(T("     dois cartões: apague o novo e ajuste o nome no [P4] para ficar igual ao do"))
    print(T("     quadro (ou deixe o código B1, B2… na frente, que é por onde eu caso)."))
    if 5 in _feitos:
        print(T("\n     ⚠️  ATENÇÃO: você já aprovou o plano no Passo 5 — a sua linha de base"))
        print(T("        está congelada. Isto NÃO é elaboração progressiva, é uma MUDANÇA DE"))
        print(T("        ESCOPO, e mudança tem processo: rode o PASSO 9 para medir o impacto"))
        print(T("        (prazo, custo, riscos) e registrar a decisão do patrocinador. Sem"))
        print(T("        isso, o planejado × realizado do Passo 8 compara com uma linha de"))
        print(T("        base que já não é verdade."))
    else:
        print(T("\n     Isto é ELABORAÇÃO PROGRESSIVA (o normal antes da linha de base): o"))
        print(T("     detalhe aparece quando você chega perto. Volte ao PASSO 2 e revise o"))
        print(T("     que apareceu — cada pacote novo precisa de critério de aceitação, e a"))
        print(T("     sua EAP tem de continuar sendo 100% do escopo. O Dicionário da EAP já"))
        print(T("     foi regerado com eles."))
if atribuidos:
    ok(Tf("Responsáveis: {0} tarefa(s) atribuída(s) ao usuário do membro do time (aparece no "
          "cartão e no Gantt do ProjectAdm).", atribuidos))
if sem_usuario:
    warn(Tf("estes responsáveis não são um membro da equipe com usuário do ProjectAdm "
            "(atribuí só o texto, não o usuário): {0}. Cada tarefa deve ter UM membro do "
            "time — ajuste o nome no [P4] para bater com uma parte do grupo \"equipe\", ou "
            "associe o usuário no cartão dessa parte.", "; ".join(sorted(sem_usuario)[:8])))

# ── 1b) DEPENDÊNCIAS REAIS: "depende de X" vira um link Finish-to-Start no quadro,
#        que o ProjectAdm desenha como seta no Gantt (não é mais só texto na descrição). ──
if predecessores:
    escopo = _mapa_escopo(ids)   # re-lê (inclui marcos recém-criados)
    n_dep = n_tipo = 0
    avisados = set()             # não repetir o mesmo aviso p/ predecessor citado N vezes
    for nome, dep in predecessores:
        destino = _achar_pacote(nome, escopo)
        if not destino:
            continue
        for tok_nome, tok_tipo in _predecessores(dep):   # cada predecessor traz o seu tipo
            pred = _achar_pacote(tok_nome, escopo)
            if not pred:
                chave = entradas._sem_acento(tok_nome)
                if chave not in avisados:
                    avisados.add(chave)
                    step(Tf('depende de "{0}": não achei esse pacote no escopo — link ignorado.', tok_nome[:40]))
                continue
            if str(pred["id"]) != str(destino["id"]):
                res = garantir_dependencia(pred["id"], destino["id"], tok_tipo)
                if res:                      # 'criada' | 'existe' | 'tipo_divergente'
                    n_dep += 1               # conta link novo E o que ja existia
                if res == "tipo_divergente":
                    n_tipo += 1
    if n_dep:
        ok(Tf("Dependências: {0} link(s) no cronograma (TI/II/TT/IT conforme o cronograma) — "
              "veja as setas no Gantt do ProjectAdm.", n_dep))
    if n_tipo:
        warn(Tf("{0} dependência(s) continuaram como Término-a-Início: não consegui trocar o "
                "tipo (ex.: II/TT/IT) porque a remoção da dependência antiga falhou no "
                "ProjectAdm. Re-rode o Passo 4 quando o ProjectAdm estiver atualizado.", n_tipo))

# ── 1c) ROLLUP: as datas sobem para entregas/fases (barras-resumo no Gantt) ──
# (o cálculo mora em quadro.rollup_datas — o mesmo que a tecla "Recalcular" usa quando
#  você ajusta uma data a mão no quadro, sem re-rodar este passo inteiro.)
n_roll = len(quadro.rollup_datas(ids))
if n_roll:
    step(Tf("Rollup: {0} entrega(s)/fase(s) passaram a abranger as datas dos seus pacotes.", n_roll))

# O Passo 4 pode ter feito a EAP CRESCER (pacote descoberto ao estimar): os códigos
# mudam, então o campo "Código EAP" é reescrito aqui — é ele que o aluno lê no cartão.
try:
    import campos as _campos
    _nos_cod, _grav_cod = _campos.sincronizar_codigos(ids, log=step)
    if _grav_cod:
        step(Tf("Código EAP atualizado em {0} cartão(ões).", _grav_cod))
except Exception as e:
    warn(Tf("não consegui atualizar os códigos da EAP agora ({0}).", e))

# ── 2) ORÇAMENTO: um cartão por item de custo, na lista Orçamento ──
cartoes_orc = []
total = 0.0
for it in orcamento:
    nome = it.get("_", "")
    if _e_linha_de_total(nome):
        step(Tf('ignorando linha de total "{0}" — o total quem calcula sou eu.', nome[:40]))
        continue
    valor = it.get("valor", "")
    # Preserva o sinal: um item pode ser NEGATIVO (ex.: "Ajuste de ordem de grandeza:
    # -5000"). Sem isto o '-' era comido e o abatimento virava soma (total inflado).
    try:
        bruto = str(valor).strip()
        num = re.sub(r"[^\d.,]", "", bruto).replace(".", "").replace(",", ".")
        v = float(num or 0)
        total += -v if bruto.startswith("-") else v
    except Exception:
        pass
    # PACOTE: a que elemento da EAP este custo pertence. É por ele que o dinheiro chega
    # à coluna Custo do Dicionário da EAP (artefatos._custo_por_codigo) — o orçamento
    # deixa de ser uma lista paralela e passa a pousar na entrega que o consome.
    # "-" (ou vazio) = custo do projeto inteiro, como a reserva de contingência.
    pacote = (it.get("pacote") or "").strip()
    # OS ROTULOS VAO NO IDIOMA DO ALUNO (06/08/2026). Eram f-string cru, entao o quadro
    # em ingles recebia "Tipo:/Valor:/Quando:/Pacote:" — quatro palavras em portugues em
    # TODO cartao de custo. quadro.orcamento_do_quadro le' os dois idiomas, entao o
    # quadro de quem ja' rodou o Passo 4 continua sendo lido.
    desc = (Tf("Tipo: {0}", it.get('tipo', '?')) + "\n"
            + Tf("Valor: {0}", valor) + "\n"
            + Tf("Quando: {0}", it.get('quando', '?'))
            + ("\n" + Tf("Pacote: {0}", pacote) if pacote and pacote != "-" else ""))
    cartoes_orc.append((nome[:255], desc, None))
n_orc = passos.escrever_cartoes(ids, "ORCAMENTO", cartoes_orc, log=step)
n_rm = _reconciliar_orcamento(ids, {t for t, _d, _p in cartoes_orc})
_total = Tf(" · total ≈ R$ {0:,.0f}", total).replace(",", ".") if total else ""
_rm = Tf(" · {0} item(ns) de rodada anterior removido(s)", n_rm) if n_rm else ""
ok(Tf("Orçamento: {0} item(ns) na lista Orçamento{1}{2}.", n_orc, _total, _rm))

passos.finalizar(
    N, ids,
    resumo=Tf("Cronograma ({0} datado) e orçamento ({1} item) na abordagem {2}.",
              datados, n_orc, prompts.nome_abordagem(abordagem)),
    mensagem_conclusao=T("Passo 4 concluído — abra a visão Gantt do ProjectAdm: o QUANDO "
                       "está nos cartões (com as setas de dependência), o QUANTO está na "
                       "lista Orçamento. A linha de base (plano congelado) você marca no Passo 5."))

# Subproduto (como nos passos anteriores): gera os documentos do Passo 4 lendo o quadro.
try:
    import os as _os
    import artefatos
    saida, _av = artefatos.gerar_p4_documentos(ids, abordagem=abordagem)
    for cam in ([saida] if isinstance(saida, str) else (saida or [])):
        print(Tf("  📄 _saida/documentos/{0}", _os.path.basename(cam)))
    if saida:
        print(T("     (regenere a qualquer momento pela tecla D do menu.)"))
except Exception as e:
    warn(Tf("não consegui gerar os documentos do Passo 4 agora ({0}); use a tecla D do menu "
            "quando quiser.", e))
