# -*- coding: utf-8 -*-
"""catalogo_pmbok8.py — o acesso de leitura ao catalogo unico dos artefatos do PMBOK 8.

(O nome casa com o do .json de proposito: a pasta de dados ao lado chama-se `catalogo/`,
e um modulo `catalogo.py` ali seria uma ambiguidade gratuita para quem le' o import.)

POR QUE ESTE MODULO EXISTE
--------------------------
`catalogo/catalogo_pmbok8.json` e' gerado por `catalogo/build_catalogo.py` a partir da
planilha de referencia. Ele e' a fonte unica que os DOIS cursos compartilham:

    Project Together  = 10 PASSOS    (livro Introducao ao Gerenciamento de Projetos)
    PMBOK 8 Together  = 40 PROCESSOS (livro PMBOK 8 na Pratica)

O artefato e' indexado pela SAIDA do PMBOK, nunca pelo numero do passo — e' isso que faz
um conserto no gerador do Termo de Abertura chegar aos dois caminhos no mesmo commit.
Um curriculo e' so' uma tabela de curadoria sobre o mesmo catalogo.

Este modulo e' a porta de leitura: quem consome nao abre o .json na mao e nao adivinha
caminho. So' le'.

PASSO NAO E' PROCESSO — E ESTE MODULO E' QUEM GARANTE ISSO (Eduardo, 14/08/2026)
-------------------------------------------------------------------------------
    Project Together  fala em PASSO.    Um passo e' um passo DO METODO: ele agrupa
                                        varios processos do Guia (o passo 6 cobre 4).
    PMBOK 8 Together  fala em PROCESSO. Cada um E' um processo do Guia PMBOK 8, com o
                                        codigo dele (2.5.2.1) e as saidas que o Guia
                                        lhe atribui.

Chamar os dois de "etapa" apagava exatamente a diferenca que separa os produtos — e ela
ja' tinha vazado para dentro do documento do aluno: o Registro das Partes Interessadas
saia carimbado "Passo 6" mesmo quando quem o gerou foi o PROCESSO 2 do PMBOK 8 Together.

A palavra mora no DADO (`unidade`, no catalogo), nao escrita a mao em cada tela. Quem
imprime um rotulo chama `rotulo_unidade()` e acerta nos dois cursos e nos dois idiomas.

QUEM ESTA RODANDO AGORA
-----------------------
`curriculo_corrente()` responde, nesta ordem:
    1. TOGETHER_CURRICULO no ambiente  — quem roda diz (o `processo.py` define, e o
       subprocesso herda: e' assim que o `01_iniciar_projeto.py` sabe que foi chamado
       pelo curso de 40 e nao pelo de 10);
    2. o arquivo `.together` do pacote  — o PACOTE declara de que curso ele e';
    3. project-together                 — o curso que sempre existiu.

ONDE O .json MORA (as duas realidades)
    repo    courses/_together_kit/catalogo/catalogo_pmbok8.json
    pacote  <raiz do pacote do aluno>/catalogo/catalogo_pmbok8.json
A subpasta `catalogo/` e' preservada nos dois lados de proposito: um caminho so' para
lembrar, e ninguem confunde o dado com um modulo.
"""
import json
import os
import re

_AQUI = os.path.dirname(os.path.abspath(__file__))
_NOME = "catalogo_pmbok8.json"
_cache = None

# Os dois curriculos, pelo nome que eles tem DENTRO do catalogo.
CURRICULO_10 = "project-together"      # 10 passos
CURRICULO_40 = "pmbok8-together"       # 40 processos
CURRICULO_PADRAO = CURRICULO_10

# slug do pacote (`.together`) -> curriculo. O slug e' o nome do PRODUTO; o curriculo e'
# o nome da CURADORIA. Sao coisas diferentes e so' por acaso um deles coincide.
CURRICULO_DO_PACOTE = {
    "project-together-ai": CURRICULO_10,
    "pmbok8-together": CURRICULO_40,
}

# As variaveis de ambiente que um runner usa para dizer o que esta rodando. Ambiente (e
# nao argv) porque o runner chama os scripts de passo em SUBPROCESSO: o ambiente atravessa
# a fronteira sozinho, sem obrigar 10 scripts a aprender a ler um parametro novo.
ENV_CURRICULO = "TOGETHER_CURRICULO"
ENV_UNIDADE = "TOGETHER_UNIDADE"


def _candidatos():
    """Os lugares onde o catalogo pode estar, do mais provavel ao menos."""
    return [
        os.path.join(_AQUI, "catalogo", _NOME),                        # pacote do aluno e repo (kit)
        os.path.join(_AQUI, _NOME),                                    # flat, se algum dia for copiado assim
        os.path.abspath(os.path.join(_AQUI, "..", "_together_kit", "catalogo", _NOME)),
    ]


def caminho():
    for c in _candidatos():
        if os.path.exists(c):
            return c
    return None


def carregar():
    """O catalogo inteiro (em cache). Levanta se nao achar — nunca devolve vazio.

    Falha alto de proposito: um catalogo ausente que devolvesse {} faria o menu perder
    artefatos SEM erro nenhum, que e' a falha silenciosa que ja nos custou caro."""
    global _cache
    if _cache is None:
        c = caminho()
        if not c:
            raise FileNotFoundError(
                "catalogo_pmbok8.json nao encontrado. Procurei em:\n  "
                + "\n  ".join(_candidatos())
                + "\nNo repo: py courses/_together_kit/catalogo/build_catalogo.py")
        with open(c, encoding="utf-8") as fh:
            _cache = json.load(fh)
    return _cache


# ─────────────────────────────────────────────────────────────────────────────
# QUEM ESTA RODANDO — o curso e, dentro dele, o passo/processo da vez
# ─────────────────────────────────────────────────────────────────────────────
def _slug_do_pacote():
    """O slug declarado no `.together` do pacote do aluno (None fora de um pacote)."""
    for base in (_AQUI, os.path.abspath(os.path.join(_AQUI, ".."))):
        arq = os.path.join(base, ".together")
        if os.path.exists(arq):
            try:
                with open(arq, encoding="utf-8") as fh:
                    return (json.load(fh).get("slug") or "").strip()
            except Exception:
                return None
    return None


def curriculo_corrente():
    """Qual dos dois cursos esta rodando AGORA: ambiente > pacote > o de 10 passos.

    Nunca levanta e nunca devolve algo que o catalogo nao conheca: um valor estranho no
    ambiente cai no padrao, porque um rotulo em portugues errado e' um arranhao e um
    KeyError no meio do passo do aluno e' o passo perdido.
    """
    v = (os.environ.get(ENV_CURRICULO) or "").strip()
    if v in (CURRICULO_10, CURRICULO_40):
        return v
    do_pacote = CURRICULO_DO_PACOTE.get(_slug_do_pacote() or "")
    return do_pacote or CURRICULO_PADRAO


def definir_curriculo(nome):
    """Declara o curso desta execucao — e das que ela chamar (o subprocesso herda)."""
    if nome in (CURRICULO_10, CURRICULO_40):
        os.environ[ENV_CURRICULO] = nome
    return curriculo_corrente()


def definir_unidade(n):
    """Declara o passo/processo que esta rodando agora (o subprocesso herda)."""
    os.environ[ENV_UNIDADE] = str(int(n))
    return int(n)


def unidade_corrente():
    """O numero do passo/processo em execucao, ou None quando ninguem declarou."""
    v = (os.environ.get(ENV_UNIDADE) or "").strip()
    return int(v) if v.isdigit() else None


def _idioma():
    """'pt' ou 'en' — o idioma do curso. Sem o idioma.py em maos, portugues."""
    try:
        from idioma import lang
        return "en" if lang() == "EN" else "pt"
    except Exception:
        return "pt"


# ─────────────────────────────────────────────────────────────────────────────
# O VOCABULARIO — passo (10) x processo (40), nos dois idiomas
# ─────────────────────────────────────────────────────────────────────────────
def curso(curriculo=None):
    """O bloco inteiro do curriculo (rotulo, livro, unidade, total, unidades)."""
    return carregar()["curriculos"][curriculo or curriculo_corrente()]


def nome_curso(curriculo=None, idioma=None):
    """"Project Together" ou "PMBOK 8 Together"."""
    return curso(curriculo)["rotulo"].get(idioma or _idioma()) or ""


def unidade(curriculo=None, idioma=None, plural=False):
    """A palavra: passo/passos · processo/processos · step/steps · process/processes."""
    u = curso(curriculo).get("unidade")
    if isinstance(u, str):          # catalogo antigo (unidade era uma palavra so')
        return u + ("s" if plural else "")
    formas = u.get(idioma or _idioma()) or u.get("pt") or ["", ""]
    return formas[1 if plural else 0]


def rotulo_unidade(n, curriculo=None, idioma=None):
    """"Passo 6" · "Processo 2" · "Step 6" · "Process 2" — com a inicial maiuscula."""
    return f"{unidade(curriculo, idioma).capitalize()} {n}"


def nome_unidade(n, curriculo=None, idioma=None):
    """O NOME do passo/processo `n`: "Identificar as partes interessadas".

    No curriculo de 40 o nome E' o do processo do Guia, e o catalogo o guarda nos dois
    idiomas (nome_pt/nome_en) — por isso o pacote EN nao mostra um titulo em portugues.
    No de 10, e' o rotulo curado pelo Eduardo.
    """
    cur = curriculo or curriculo_corrente()
    spec = unidade_do(n, cur)
    if (idioma or _idioma()) == "en":
        for cod in (spec.get("processos") or []):
            p = carregar()["processos"].get(cod) or {}
            if p.get("nome_en"):
                return p["nome_en"]
    return spec.get("rotulo") or ""


def total(curriculo=None):
    """10 ou 40."""
    return curso(curriculo).get("total") or len(unidades(curriculo))


def unidades(curriculo=None):
    """{numero: spec} — os 10 passos ou os 40 processos, conforme o curriculo."""
    c = curso(curriculo)
    return c.get("unidades") or c.get("etapas") or {}      # `etapas`: catalogo antigo


def unidade_do(n, curriculo=None):
    """O spec do passo/processo `n` ({} quando nao existe)."""
    return unidades(curriculo).get(str(n)) or {}


def numero_da_saida(nome, curriculo=None):
    """O numero do passo/processo que ENTREGA esta saida neste curriculo (None se nenhum).

    O primeiro, quando mais de um a entrega: a "Solicitação de mudança" e' saida de meia
    duzia de processos, e quem pergunta assim quer saber onde ela NASCE.
    """
    for n in sorted(unidades(curriculo), key=int):
        if nome in (unidade_do(n, curriculo).get("artefatos") or []):
            return int(n)
    return None


def rotulo_de_origem(n_project_together, saida=None):
    """O carimbo "quem gerou este documento", no vocabulario do curso que esta rodando.

    O gerador conhece o numero dele no Project Together (o curso onde ele nasceu) e passa
    esse numero aqui. Rodando o PMBOK 8 Together, o numero e' OUTRO — e a palavra tambem:
    o mesmo `gerar_registro_partes` e' o Passo 6 la' e o Processo 2 aqui. Sem isto, o
    documento do aluno de 40 saia assinado por um passo que o curso dele nao tem.
    """
    cur = curriculo_corrente()
    if cur == CURRICULO_10:
        return rotulo_unidade(n_project_together, cur)
    n = unidade_corrente() or (numero_da_saida(saida, cur) if saida else None)
    return rotulo_unidade(n, cur) if n else nome_curso(cur)


def numero_do_script(script, curriculo=None):
    """O numero do passo/processo que roda ESTE script neste curriculo (None se nenhum).

    Existe porque nem toda referencia cruzada tem uma SAIDA para apontar. "Onde eu defino
    a estrategia de engajamento?" e' o `06_engajar_partes.py` — e a saida dele, no Guia,
    e' uma "Solicitação de mudança", que nao identifica nada. Quem identifica e' o script.
    """
    for n in sorted(unidades(curriculo), key=int):
        if unidade_do(n, curriculo).get("script") == script:
            return int(n)
    return None


def rotulo_do_script(n_project_together, script):
    """"Passo 6" ou "Processo 24" — o lugar onde `script` roda, para citar num texto."""
    cur = curriculo_corrente()
    if cur == CURRICULO_10:
        return rotulo_unidade(n_project_together, cur)
    n = numero_do_script(script, cur)
    return rotulo_unidade(n, cur) if n else nome_curso(cur)


# ─────────────────────────────────────────────────────────────────────────────
# O NUMERO E O ROTULO DE UM PASSO, NO CURSO QUE ESTA RODANDO
#
# Os scripts do curso nasceram no Project Together e conhecem o numero DELES ("o
# Passo 5 captura a linha de base"). Rodando o PMBOK 8 Together esse mesmo script e'
# o Processo 21 — e la' nao existe passo nenhum. Estas quatro funcoes sao o unico
# lugar onde essa traducao acontece: nenhum script escreve "Passo" a mao.
#
# A ponte e' o SCRIPT, a unica chave que os dois curriculos compartilham (o
# `unidades[n].script` do catalogo, nos dois lados).
# ─────────────────────────────────────────────────────────────────────────────
def _script_do_passo(n):
    """O script que roda o passo `n` do Project Together ("" se nao houver)."""
    return unidade_do(n, CURRICULO_10).get("script") or ""


def numero_do_passo(n):
    """O NUMERO de `n` no curso corrente: 2 no Project Together, 5 no PMBOK 8.

    None quando o curso corrente nao executa este passo — quem chama decide o que
    dizer no lugar. Nunca devolve o numero do OUTRO curso: era esse o defeito."""
    cur = curriculo_corrente()
    if cur == CURRICULO_10:
        return int(n)
    return numero_do_script(_script_do_passo(n), cur)


def numeros_dos_passos(ns):
    """TODOS os numeros que estes passos cobrem no curso corrente, ordenados.

    No PMBOK 8 um script fecha mais de um processo — o `01_iniciar_projeto.py` fecha
    o 1 E o 2 — entao "concluidos" nao e' uma conta de scripts: e' de processos."""
    cur = curriculo_corrente()
    if cur == CURRICULO_10:
        return sorted(int(n) for n in ns)
    scripts = {_script_do_passo(n) for n in ns} - {""}
    return sorted(int(k) for k, u in unidades(cur).items() if u.get("script") in scripts)


def rotulo_do_passo(n):
    """"Passo 2" · "Processo 5" — o rotulo de `n` no vocabulario do curso corrente."""
    return rotulo_do_script(int(n), _script_do_passo(n))


def rotulos_dos_passos(ns, e=" e "):
    """"Passos 1, 2, 3 e 4" · "Processos 1, 5, 9 e 17" — a lista, no plural certo.

    Nao e' um INTERVALO ("Passos 5 a 10"): no curriculo de 40 os numeros nao sao
    contiguos, e "Processos 21 a 40" incluiria 18 processos que o curso nao executa
    nesse ciclo. O ` e ` entra por parametro porque quem chama e' que tem o T()."""
    numeros = [x for x in (numero_do_passo(n) for n in ns) if x]
    if not numeros:
        return nome_curso()
    palavra = unidade(plural=len(numeros) != 1).capitalize()
    corpo = (", ".join(str(x) for x in numeros[:-1]) + e + str(numeros[-1])
             if len(numeros) > 1 else str(numeros[0]))
    return f"{palavra} {corpo}"


def executa_tudo():
    """O curso corrente executa TODAS as unidades dele?

    O Project Together executa os 10 passos dele (True). O PMBOK 8 Together tem 40
    processos e os scripts entregam a saida de 17 (False) — por isso "voce percorreu
    os 40 processos" nunca pode ser dito la'."""
    return len(numeros_dos_passos(range(1, 11))) >= total()


# ─────────────────────────────────────────────────────────────────────────────
# O FILTRO DE SAIDA — "Passo 5" vira "Processo 21" na hora de imprimir
#
# Os ~2.000 textos do curso foram escritos no Project Together e dizem "Passo N".
# O PMBOK 8 Together roda os MESMOS arquivos. Em vez de reescrever 2.000 frases (e
# 2.000 traducoes), o numero e a palavra sao trocados no fim, uma vez, aqui.
#
# TRES RECUSAS, e sao elas que tornam isto seguro:
#   1. numero que este curso NAO executa fica como esta' (o "Passo 0" e' a
#      configuracao do ambiente, nao um processo do Guia — reescreve-lo inventaria
#      um "Processo 0" que nao existe);
#   2. so' casa a palavra colada a um numero ou sozinha entre limites de palavra —
#      `prompt-passo-5.txt` e `_saida/passo3.json` NAO sao tocados, senao o texto
#      passaria a citar um arquivo que nao existe no disco;
#   3. em portugues, no curso de 10, tudo isto e' NO-OP por construcao: o rotulo de
#      5 nos 10 passos E' "Passo 5".
# ─────────────────────────────────────────────────────────────────────────────
# `(?<![\w-])` e `(?![\w-])`: o hifen entra no limite de proposito — e' o que salva
# os nomes de arquivo (`prompt-passo-5.txt`) e as chaves (`passo-2`).
# `(?P<mais>(?:/\d{1,2})*)`: a forma COMPOSTA "Passo 4/5" (= passos 4 e 5) tem de ser
# traduzida INTEIRA. Traduzir so' o primeiro numero produzia "Processo 9/5" — um par que
# nao existe em curso nenhum, e que o aluno leria como um processo chamado "9/5".
_UNIDADE_RE = {
    "pt": re.compile(r"(?<![\w-])(?P<p>[Pp]asso|PASSO)(?P<s>s|S)?"
                     r"(?P<sep>\s+)(?P<n>\d{1,2})(?P<mais>(?:/\d{1,2})*)"
                     r"(?!\s+a\s+\d)(?![\w-])"),
    "en": re.compile(r"(?<![\w-])(?P<p>[Ss]tep|STEP)(?P<s>s|S)?"
                     r"(?P<sep>\s+)(?P<n>\d{1,2})(?P<mais>(?:/\d{1,2})*)"
                     r"(?!\s+to\s+\d)(?![\w-])"),
}
# a palavra SOZINHA ("este passo e' rotina", "os passos do ciclo").
# `(?!\s+\d)` e' o que faz a recusa 1 valer ate' o fim: quando o passe do numero
# RECUSOU (o curso nao executa aquele numero), a palavra tem de ficar como esta'.
# Sem isto, "Passo 0" — a configuracao do ambiente — virava "Processo 0", um
# processo que o Guia PMBOK 8 nao tem.
_PALAVRA_RE = {
    "pt": re.compile(r"(?<![\w-])(?P<p>[Pp]asso|PASSO)(?P<s>s|S)?(?!\s+\d)(?![\w-])"),
    "en": re.compile(r"(?<![\w-])(?P<p>[Ss]tep|STEP)(?P<s>s|S)?(?!\s+\d)(?![\w-])"),
}


def _mesma_caixa(molde, palavra):
    """"PASSO"->"PROCESSO" · "Passo"->"Processo" · "passo"->"processo"."""
    if molde.isupper():
        return palavra.upper()
    if molde[:1].isupper():
        return palavra.capitalize()
    return palavra


def reescrever_unidade(texto):
    """Troca "Passo N"/"passo" pelo vocabulario do curso corrente. NO-OP nos 10."""
    cur = curriculo_corrente()
    if cur == CURRICULO_10 or not texto or ("asso" not in texto and "tep" not in texto
                                            and "ASSO" not in texto and "TEP" not in texto):
        return texto
    idi = _idioma()

    def _com_numero(m):
        crus = [int(m.group("n"))] + [int(x) for x in m.group("mais").lstrip("/").split("/")
                                      if x]
        novos = [numero_do_passo(x) for x in crus]
        if not all(novos):              # recusa 1: o curso nao executa algum destes
            return m.group(0)           #            numeros -> a frase inteira fica
        # o plural acompanha o ORIGINAL, nunca a quantidade de numeros: "do Passo 4/5"
        # tem de virar "do Processo 9/21", e nao "do Processos 9/21".
        return (_mesma_caixa(m.group("p"), unidade(cur, idi, plural=bool(m.group("s"))))
                + m.group("sep") + "/".join(str(x) for x in novos))

    def _so_palavra(m):
        return _mesma_caixa(m.group("p"), unidade(cur, idi, plural=bool(m.group("s"))))

    texto = _UNIDADE_RE[idi].sub(_com_numero, texto)
    return _PALAVRA_RE[idi].sub(_so_palavra, texto)


def ativar_vocabulario():
    """Liga o filtro no idioma. Chamado por quem declara o curriculo (menu/processo)."""
    try:
        from idioma import registrar_filtro
        registrar_filtro(reescrever_unidade)
    except Exception:
        pass                            # sem idioma em maos, o curso segue em portugues


def rotulo_da_saida(n_project_together, saida):
    """Como o rotulo_de_origem(), mas para uma REFERENCIA CRUZADA dentro do texto.

    "os custos do Passo 4" (Project Together) e' "os custos do Processo 12" (PMBOK 8
    Together). A diferenca para o rotulo_de_origem() e' de propósito: aqui quem manda e'
    SEMPRE a saida citada, nunca o passo que esta rodando — senao o Dicionario da EAP
    diria que os custos vieram dele mesmo.
    """
    cur = curriculo_corrente()
    if cur == CURRICULO_10:
        return rotulo_unidade(n_project_together, cur)
    n = numero_da_saida(saida, cur)
    return rotulo_unidade(n, cur) if n else nome_curso(cur)


# ─────────────────────────────────────────────────────────────────────────────
# A CURADORIA DOS 10 PASSOS
#
# `curriculo=CURRICULO_10` fixo, e nao "o corrente", de proposito: quem chama isto e' a
# tabela ARTEFATOS do artefatos.py, que E' a dos 10 passos. Deixar o corrente decidir
# faria o pacote do PMBOK 8 procurar o passo 7 numa lista de 40 processos e quebrar na
# importacao — cara feia por um ganho nenhum.
# ─────────────────────────────────────────────────────────────────────────────
def artefato(nome):
    """O spec de UMA saida do PMBOK (ou de um artefato do metodo, como o Kick-Off)."""
    return carregar()["artefatos"].get(nome)


def etapas(curriculo=CURRICULO_10):
    """Apelido historico de unidades(). Mantido para nao quebrar chamador antigo."""
    return unidades(curriculo)


def etapa(n, curriculo=CURRICULO_10):
    """Apelido historico de unidade_do()."""
    return unidade_do(n, curriculo)


def gerador_do_passo(n, curriculo=CURRICULO_10):
    """O NOME da funcao que o passo `n` chama para gerar o(s) documento(s).

    Nome, nao funcao: quem resolve o nome e' o modulo que tem as funcoes (artefatos.py).
    Assim o catalogo continua sendo dado, e o kit nao passa a depender do curso."""
    return unidade_do(n, curriculo).get("gerador")


def artefatos_do_passo(n, curriculo=CURRICULO_10):
    """Os nomes das saidas que o passo `n` entrega (curadoria, nao derivacao)."""
    return list(unidade_do(n, curriculo).get("artefatos") or [])


def modelo_do_artefato(nome, idioma="pt"):
    """O arquivo de modelo daquela saida, no idioma pedido (cai no PT se nao houver EN)."""
    a = artefato(nome) or {}
    m = a.get("modelo") or {}
    return m.get(idioma) or m.get("pt")


# O filtro liga NO IMPORT, nao numa chamada que alguem precisa lembrar de fazer.
#
# Quem declara o curriculo sao varios (menu, processo, etapa, e o proprio `.together`
# quando ninguem declara nada). Deixar a ativacao a cargo de cada um deles seria uma
# lista de chamadas para esquecer — e o sintoma de esquecer e' silencioso: o curso de
# 40 volta a dizer "Passo 5" e nada acusa. Ligar aqui e' seguro porque a reescrita e'
# NO-OP no curriculo de 10 (o rotulo de 5 nos 10 passos E' "Passo 5") e porque ela le
# `curriculo_corrente()` a cada chamada — quem trocar de curriculo depois disto ja'
# pega o vocabulario novo.
ativar_vocabulario()
