"""
Project Together — powered by AI: DIRIGIR A EXECUCAO (o motor do Passo 7).

POR QUE ESTE MODULO EXISTE (Eduardo, 2026-08-07)
    O Passo 7 nasceu como "monta um prompt e pergunta a IA o que fazer agora". Mas o
    projeto JA SABE o que tem de acontecer: o cronograma do Passo 4 tem toda atividade,
    com dono e com data. Perguntar isso a uma IA e' pedir que ela adivinhe o que esta
    escrito no quadro. Dirigir a execucao e' outra coisa — e' PEGAR o cronograma e
    transformar em acao: quem esta atrasado, quem entrega esta semana, e um recado para
    cada dono com a lista dele em anexo.

    A referencia e' a macro que o Eduardo rodava no MS Project por anos
    (`PendenciasPorEquipe`): para cada recurso, as tarefas nao concluidas com termino
    dentro de um horizonte de N dias, uma planilha por pessoa, e um e-mail com a planilha
    anexada. Isto aqui e' a mesma ideia, lendo o quadro do ProjectAdm em vez do .mpp.

O QUE ELE PRODUZ
    _saida/execucao/
        <Projeto> - <Nome da pessoa>.xlsx    uma planilha por dono  (o "Individual")
        <Projeto> - execucao.xlsx            a consolidada, todo mundo  (o "Todos")
        emails/<Nome da pessoa>.eml          o rascunho, com a planilha ja anexada
    e, no quadro: as etiquetas "Atrasada" e "Vence em N dias" nos cartoes.

O QUE ELE NAO FAZ: ENVIAR. O rascunho abre no programa de e-mail do aluno e QUEM APERTA
    ENVIAR E' ELE. Mandar e-mail para a equipe de um projeto real, sozinho, sem ninguem
    ler antes, e' o tipo de automacao que so' se descobre errada depois que saiu. A macro
    do Eduardo fazia `mail.Display` antes do `mail.Send` pelo mesmo motivo — a diferenca
    e' que aqui o Send e' do aluno.

A PLANILHA REUSA O TEMPLATE DO CRONOGRAMA. As colunas que a macro gerava (WBS, Atividade,
    Inicio, Termino, Duracao, Recurso, % Conclusao) sao EXATAMENTE as do
    'Cronograma do Projeto.xlsx' que o curso ja tem — e o aluno ja conhece esse visual.
    Um template novo so' para isto seria um arquivo a mais para manter nos dois idiomas.
"""
import datetime
import os
import re

import entradas
import quadro
from idioma import T, Tf

HERE = os.path.dirname(os.path.abspath(__file__))

# O HORIZONTE: quantos dias para a frente a gente olha. A macro do Eduardo usava 60 no
# primeiro envio ("veja tudo o que vem por ai") e depois rodava semanalmente. 15 dias e'
# o meio-termo que serve para as duas cadencias sem inundar ninguem; o aluno muda no .env
# (EXECUTION_HORIZON_DAYS) quando o projeto dele pede outro ritmo.
HORIZONTE_PADRAO = 15

# As situacoes de uma atividade diante da data de hoje. A chave e' interna (o codigo le);
# o rotulo e' o que o aluno ve, e passa por T().
ATRASADA = "atrasada"
NO_HORIZONTE = "no_horizonte"
FUTURA = "futura"
CONCLUIDA = "concluida"
SEM_DATA = "sem_data"


def rotulo_situacao(sit):
    return {
        ATRASADA: T("Atrasada"),
        NO_HORIZONTE: T("Vence em breve"),
        FUTURA: T("Adiante no cronograma"),
        CONCLUIDA: T("Concluída"),
        SEM_DATA: T("Sem data no cronograma"),
    }.get(sit, sit)


def horizonte(c=None):
    """Quantos dias para a frente o Passo 7 olha (do .env, ou o padrão)."""
    from env import cfg, get_int
    try:
        return max(1, get_int(c if c is not None else cfg(),
                              "EXECUTION_HORIZON_DAYS", HORIZONTE_PADRAO))
    except Exception:
        return HORIZONTE_PADRAO


def _data(valor):
    """'2026-08-14' ou '2026-08-14 00:00:00' -> date. None quando não dá para ler."""
    m = re.search(r"(\d{4})-(\d{2})-(\d{2})", str(valor or ""))
    if not m:
        return None
    try:
        return datetime.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
    except ValueError:
        return None


def _pct(card):
    """% de conclusão de um cartão: 100 quando concluído, senão o progresso (ou vazio)."""
    if quadro.card_concluido(card):
        return 100
    p = card.get("progresso")
    try:
        return int(float(p))
    except (TypeError, ValueError):
        return ""


def atividades(ids):
    """O CRONOGRAMA INTEIRO como uma lista de linhas executáveis.

    Cada linha é um nó da EAP (fase, entrega, pacote de trabalho) ou uma ATIVIDADE do
    Passo 7, com o que a execução precisa: código da EAP, título, datas, duração,
    predecessor, responsável — e, do cartão, se está concluída e o quanto andou.

    Só as FOLHAS são trabalho de alguém: uma fase ou uma entrega é um agrupamento, e a
    data dela é o rollup dos filhos. Cobrar de um dono a entrega inteira quando ele
    responde por um pacote dela é o jeito mais rápido de o e-mail perder a credibilidade.
    Por isso `trabalho()` filtra por tipo — mas a lista completa fica disponível para o
    relatório, que precisa mostrar a estrutura.
    """
    from pa_client import list_cards
    linhas = quadro.cronograma_do_quadro(ids)          # já desce até as atividades
    nos, _tem_fases = quadro.arvore_da_eap(ids, com_tarefas=True)
    por_codigo = {n["codigo"]: n for n in nos}
    # o estado de execução (concluído / progresso) vive no CARTÃO, e o GET de lista já
    # traz os dois — uma chamada para a lista toda, em vez de uma por atividade.
    cards = {}
    for chave in ("ESCOPO", "EXECUCAO"):
        lid = ids.get(chave)
        if lid:
            for cd in list_cards(lid):
                cards[str(cd.get("id"))] = cd
    out = []
    for l in linhas:
        no = por_codigo.get(l["codigo"]) or {}
        cid = str(no.get("id") or "")
        cd = cards.get(cid, {})
        out.append({
            "id": cid,
            "codigo": l["codigo"],
            "nivel": l["nivel"],
            "tipo": l["tipo"],
            "titulo": l["titulo"],
            "ini": l.get("ini", ""),
            "fim": l.get("fim", ""),
            "duracao": l.get("duracao", ""),
            "predecessor": l.get("predecessor", ""),
            "responsavel": (l.get("responsavel") or "").strip(),
            "concluido": quadro.card_concluido(cd) if cd else False,
            "pct": _pct(cd) if cd else "",
        })
    return out


def trabalho(itens):
    """Só as linhas que são trabalho de alguém: pacote de trabalho (a folha da EAP) e
    atividade do Passo 7. Fase e entrega são agrupamento — ver `atividades()`."""
    return [i for i in itens if i["tipo"] in ("pacote", "tarefa")]


def classificar(itens, hoje=None, dias=None):
    """Escreve a `situacao` de cada item (atrasada / vence em breve / adiante / …).

    ATRASADA é o que a macro chamava de vencida: não concluída e com término ANTES de
    hoje. NO_HORIZONTE é `t.Finish < DataProximoSR` — não concluída e vencendo dentro dos
    próximos N dias. É esse par que vira o e-mail; o resto é contexto.
    """
    hoje = hoje or datetime.date.today()
    dias = horizonte() if dias is None else dias
    limite = hoje + datetime.timedelta(days=dias)
    for i in itens:
        fim = _data(i.get("fim"))
        i["fim_data"] = fim
        i["ini_data"] = _data(i.get("ini"))
        if i.get("concluido"):
            i["situacao"] = CONCLUIDA
        elif not fim:
            i["situacao"] = SEM_DATA
        elif fim < hoje:
            i["situacao"] = ATRASADA
        elif fim <= limite:
            i["situacao"] = NO_HORIZONTE
        else:
            i["situacao"] = FUTURA
    return itens


def na_janela(itens):
    """O que entra no e-mail e nas planilhas: atrasado + vencendo no horizonte.

    Ordenado como a pessoa precisa ler: o atrasado primeiro, depois por data de término.
    """
    sel = [i for i in itens if i.get("situacao") in (ATRASADA, NO_HORIZONTE)]
    return sorted(sel, key=lambda i: (0 if i["situacao"] == ATRASADA else 1,
                                      i.get("fim_data") or datetime.date.max,
                                      i.get("codigo") or ""))


def por_responsavel(itens):
    """{nome do dono: [atividades dele]} — na ordem em que aparecem no cronograma.

    Quem não tem dono NÃO vira uma pessoa chamada "?": vai para `sem_dono()`, porque
    atividade sem responsável é um problema de plano, não um destinatário.
    """
    carteiras = {}
    for i in itens:
        nome = (i.get("responsavel") or "").strip()
        if not nome or team_pendente(nome):
            continue
        carteiras.setdefault(nome, []).append(i)
    return carteiras


def juntar_por_pessoa(ids, carteiras):
    """Junta as carteiras que são da MESMA PESSOA, mesmo escritas de formas diferentes.

    No quadro 324 o cronograma tinha dois donos — "Eduardo" (12 atividades) e "Investor &
    Portfolio Manager (Eduardo)" (15) — que são a MESMA conta do ProjectAdm. Sem juntar,
    a pessoa recebia dois e-mails e duas planilhas do mesmo projeto, no mesmo minuto, e
    tinha de descobrir sozinha que as duas eram dela. Quem decide se é a mesma pessoa é a
    CONTA (`team_check.conta_do_responsavel`) — nunca a semelhança dos nomes.

    O nome que sobrevive é o do cartão da equipe (o nome próprio da pessoa no quadro),
    porque é ele que o aluno reconhece e é dele que sai o "Ana," do começo do e-mail.
    """
    import team_check
    contas = team_check.contas_da_equipe(ids)
    if not contas:
        return carteiras
    # {id da conta: título do cartão da equipe} — o nome canônico de cada pessoa
    canonico = {c["id"]: titulo for titulo, c in contas.items()}
    juntas, destino = {}, {}
    for nome in carteiras:
        conta = team_check.conta_do_responsavel(nome, contas)
        destino[nome] = canonico.get((conta or {}).get("id"), nome)
    for nome, itens in carteiras.items():
        juntas.setdefault(destino[nome], []).extend(itens)
    for chave in juntas:
        juntas[chave] = sorted(juntas[chave],
                               key=lambda i: (0 if i["situacao"] == ATRASADA else 1,
                                              i.get("fim_data") or datetime.date.max,
                                              i.get("codigo") or ""))
    return juntas


def sem_dono(itens):
    """As atividades da janela que não têm responsável — o furo que o Passo 7 tem de
    mostrar ANTES de mandar qualquer e-mail. Ninguém executa o que não é de ninguém."""
    return [i for i in itens
            if not (i.get("responsavel") or "").strip() or team_pendente(i["responsavel"])]


def team_pendente(nome):
    """'a definir' / 'to be defined' / '?' — o dono que ainda não foi decidido."""
    try:
        import team_check
        return team_check.e_pendente(entradas._sem_acento(nome or ""))
    except Exception:
        return (nome or "").strip() in ("", "?", "-", "—")


# ── e-mail: onde mora o endereço de cada pessoa ──────────────────────────────
def emails_da_equipe(ids):
    """{nome da parte: e-mail} — TODAS as partes do quadro, com "" para quem não tem.

    DUAS FONTES, NESTA ORDEM (Eduardo, 07/08/2026):
      1. a CONTA do ProjectAdm ligada ao cartão da pessoa no Passo 1. É a melhor fonte:
         quem mantém esse endereço é o dono da conta, ninguém digita nada, e é a mesma
         pessoa a quem a tarefa foi atribuída — o e-mail e a atribuição não podem
         divergir;
      2. a linha "e-mail:" escrita no cartão — o plano B, para quem não é usuário do
         ProjectAdm.

    Quem NÃO tem endereço entra assim mesmo, com "". Filtrar aqui seria cômodo e errado:
    é a lista completa que permite ao `casar_pessoa` enxergar a ambiguidade entre duas
    partes de nome parecido. Quem só quer os endereços conhecidos usa `endereco_de`.
    """
    import artefatos
    import team_check
    saida = {}
    for p in artefatos._partes_do_quadro(ids):
        nome = (p.get("nome") or "").strip()
        if nome:
            saida[nome] = (p.get("email") or "").strip()
    # a conta do ProjectAdm GANHA da linha digitada: ela é a fonte viva
    try:
        for c, full in team_check.cartoes_da_equipe(ids):
            u = team_check.usuario_do_cartao(full)
            titulo = (c.get("titulo") or "").strip()
            if titulo and u and u.get("email"):
                saida[titulo] = u["email"]
    except Exception:
        pass
    return saida


# QUEM É QUEM mora no `team_check` — ele é o dono da pergunta "quem é esta pessoa?", e a
# mesma resposta serve ao vínculo com a conta (Passo 1), à atribuição da tarefa (Passos 4
# e 7) e ao endereço do e-mail (aqui). Três implementações seriam três bugs diferentes.
from team_check import casar_pessoa  # noqa: E402,F401  (reexportado de propósito)


def _cartao_da_parte(ids, nome):
    """O cartão da pessoa na lista Partes Interessadas. None quando não existe — ou
    quando o nome casa com mais de uma parte (ver `casar_pessoa`)."""
    from pa_client import list_cards
    lid = ids.get("PARTES")
    if not lid:
        return None
    cartoes = {(cd.get("titulo") or ""): cd for cd in list_cards(lid)
               if not (cd.get("titulo") or "").startswith(quadro.PROTEGIDOS_PARTES)}
    titulo = casar_pessoa(nome, list(cartoes))
    return cartoes.get(titulo) if titulo else None


def gravar_email(ids, nome, endereco):
    """Escreve o e-mail da pessoa NO CARTÃO dela — o quadro continua sendo a fonte.

    Atualiza a linha "e-mail:" que já existe, em vez de acrescentar uma segunda: duas
    linhas para o mesmo dado é o pior fim possível, porque a pessoa edita uma e o
    sistema lê a outra (`DEV_STANDARDS`, migrar sem quebrar). Cartões criados antes de
    2026-08-07 não têm a linha — nesses, ela é acrescentada. Devolve True se gravou.
    """
    from pa_client import update_card
    import vocabulario
    cd = _cartao_da_parte(ids, nome)
    if not cd or not (endereco or "").strip():
        return False
    base = entradas.texto_do_cartao(cd.get("descricao", "")) or ""
    linha = Tf("e-mail: {0}", endereco.strip())
    padrao = re.compile(rf"^[ \t]*(?:{vocabulario.ROTULOS_PARTE['email']})[ \t]*:.*$",
                        re.I | re.M)
    novo, trocas = padrao.subn(linha, base, count=1)
    if not trocas:
        # sem a linha (cartão antigo): entra logo depois do bloco de identificação, e
        # nunca depois do "── Plano de engajamento" do Passo 6, que é rodapé.
        corte = len(novo)
        for marca in ("── Plano de engajamento", "── Engagement plan"):
            i = novo.find(marca)
            if i >= 0:
                corte = min(corte, i)
        novo = (novo[:corte].rstrip() + "\n" + linha + "\n\n" + novo[corte:].strip()).strip()
    try:
        update_card(cd["id"], descricao=novo)
        return True
    except Exception:
        return False


def endereco_de(nome, agenda):
    """O e-mail de `nome` na agenda — ou "" quando não dá para ter certeza de quem é.

    Passa por `casar_pessoa`, que é estrito: melhor não mandar do que mandar a lista de
    uma pessoa para o endereço de outra. O passo avisa por quem ficou sem endereço.
    """
    pessoa = casar_pessoa(nome, list(agenda))
    return agenda.get(pessoa, "") if pessoa else ""


def enderecos_dos_donos(ids, nomes):
    """{nome do dono, como está no cronograma: e-mail} — a resolução completa.

    DUAS PASSADAS, e a ordem importa:
      1. a CONTA do ProjectAdm de quem é da EQUIPE. É a mesma resolução que decide a
         ATRIBUIÇÃO da tarefa (`team_check.conta_do_responsavel`), então quem recebe a
         tarefa é exatamente quem recebe o e-mail — os dois nunca divergem. E é ela que
         desempata o caso real do quadro 324: "Eduardo" cai em dois cartões de parte
         ("Portfolio Owner" e "Investor & Portfolio Manager"), mas os dois são a MESMA
         conta, então não há dúvida sobre para onde mandar.
      2. o endereço escrito no cartão da parte, para quem não tem conta (o plano B).
    """
    import team_check
    contas = team_check.contas_da_equipe(ids)
    agenda = emails_da_equipe(ids)
    saida = {}
    for nome in nomes:
        conta = team_check.conta_do_responsavel(nome, contas) if contas else None
        saida[nome] = (conta or {}).get("email") or endereco_de(nome, agenda)
    return saida


# ── as planilhas ─────────────────────────────────────────────────────────────
def _saida_dir():
    d = os.path.join(HERE, "_saida", "execucao")
    os.makedirs(d, exist_ok=True)
    return d


def _slug(nome):
    return (re.sub(r'[<>:"/\\|?*]', "", (nome or "").strip()) or "projeto")[:60]


def planilha(itens, saida_path, projeto, hoje=None):
    """Grava `itens` numa planilha no visual do Cronograma do Projeto.

    Colunas: as mesmas da macro do MS Project — WBS · Atividade · Início · Término ·
    Duração · Predecessor · Responsável · % Conclusão · Linha de base. A atividade
    atrasada leva o aviso no PRÓPRIO nome da atividade, e não numa coluna nova: é a
    célula que a pessoa lê primeiro, e uma coluna a mais não caberia no template dos
    dois idiomas sem retrabalho.
    """
    import artefatos
    from documentos import preencher_xlsx
    hoje = hoje or datetime.date.today()
    linhas = []
    for i in itens:
        nome = i["titulo"]
        if i.get("situacao") == ATRASADA:
            dias = (hoje - i["fim_data"]).days if i.get("fim_data") else 0
            nome = Tf("[ATRASADA há {0} dia(s)] {1}", dias, nome)
        linhas.append({
            "B": i.get("codigo", ""),
            "C": nome,
            "D": i.get("ini", ""),
            "E": i.get("fim", ""),
            "F": i.get("duracao", ""),
            "G": i.get("predecessor", ""),
            "H": i.get("responsavel", ""),
            "I": i.get("pct", ""),
            "J": i.get("ini", ""),
            "K": i.get("fim", ""),
        })
    versao = ("1.0", hoje.strftime("%d/%m/%Y"), "",
              Tf("Gerado a partir do quadro no ProjectAdm (Passo {0} · {1})",
                 7, T("Dirigir a execução")))
    return preencher_xlsx(
        artefatos._template(T("Cronograma do Projeto.xlsx")), saida_path,
        aba=artefatos.ABAS["cronograma"], primeira_linha=9, linhas=linhas,
        trocas=artefatos._trocas_capa(projeto), versao=versao,
        aba_capa=artefatos.ABAS["cronograma"])


def planilhas_por_pessoa(carteiras, projeto, log=lambda _m: None, hoje=None):
    """Uma planilha por dono (o modo "Individual" da macro). Devolve {nome: caminho}."""
    saida = {}
    for nome, itens in sorted(carteiras.items()):
        if not itens:
            continue
        caminho = os.path.join(_saida_dir(), f"{_slug(projeto)} - {_slug(nome)}.xlsx")
        try:
            planilha(itens, caminho, projeto, hoje=hoje)
            saida[nome] = caminho
            log(Tf("  {0}: {1} atividade(s) → {2}", nome, len(itens),
                   os.path.basename(caminho)))
        except Exception as e:
            log(Tf("  [aviso] não consegui gerar a planilha de {0}: {1}", nome, e))
    return saida


def planilha_consolidada(itens, projeto, log=lambda _m: None, hoje=None):
    """A planilha com TODO MUNDO (o modo "Grupo"/"Todos" da macro) — a visão do gerente:
    a mesma janela, agrupada por responsável. É o relatório que guia a reunião de
    acompanhamento."""
    ordenados = sorted(itens, key=lambda i: ((i.get("responsavel") or "~").lower(),
                                             0 if i["situacao"] == ATRASADA else 1,
                                             i.get("fim_data") or datetime.date.max))
    caminho = os.path.join(_saida_dir(), Tf("{0} - execução.xlsx", _slug(projeto)))
    try:
        planilha(ordenados, caminho, projeto, hoje=hoje)
        log(Tf("  {0} atividade(s) de {1} pessoa(s) → {2}", len(ordenados),
               len({(i.get('responsavel') or '') for i in ordenados}),
               os.path.basename(caminho)))
        return caminho
    except Exception as e:
        log(Tf("  [aviso] não consegui gerar a planilha consolidada: {0}", e))
        return None


# ── os rascunhos de e-mail ───────────────────────────────────────────────────
def corpo_do_email(nome, itens, projeto, gerente, dias, hoje=None):
    """O texto do recado. Curto, com o número na frente e um pedido claro de resposta —
    é o que a macro mandava, e é o que faz alguém responder."""
    hoje = hoje or datetime.date.today()
    atrasadas = [i for i in itens if i.get("situacao") == ATRASADA]
    linhas = [Tf("{0},", nome), ""]
    linhas.append(Tf("Em anexo, as suas atividades do projeto {0} com término previsto "
                     "para os próximos {1} dias.", projeto, dias))
    if atrasadas:
        linhas.append("")
        linhas.append(Tf("{0} dela(s) já passou(aram) da data combinada:", len(atrasadas)))
        for i in atrasadas[:10]:
            linhas.append(Tf("  • {0} {1} — era para {2}", i.get("codigo", ""),
                             i["titulo"], i.get("fim", "")))
        if len(atrasadas) > 10:
            linhas.append(Tf("  … e mais {0}.", len(atrasadas) - 10))
    linhas += [
        "",
        T("Peço que avalie as suas atividades e me diga se alguma coisa está travando a "
          "entrega — quanto antes eu souber, mais fácil é resolver junto."),
        "",
        T("Conte comigo no que precisar."),
        "",
        gerente or "",
    ]
    return "\n".join(l for l in linhas if l is not None)


def rascunho_email(destinatario, assunto, corpo, anexo, saida_path):
    """Grava um .eml — a mensagem pronta, com a planilha anexada, que o programa de
    e-mail do aluno abre para ele conferir e ENVIAR.

    .eml e não uma chamada de API: funciona no Windows e no Mac, não pede senha nenhuma
    e não põe uma segunda credencial de verdade no computador do aluno. É o
    `mail.Display` da macro, sem o `mail.Send`.

    `X-Unsent: 1` é o cabeçalho que diz "isto é um RASCUNHO, não uma mensagem recebida".
    O Outlook o respeita e abre o arquivo já na janela de composição, com o botão Enviar.
    O Thunderbird NÃO o respeita — por isso quem abre para enviar é o `abrir_para_enviar`,
    que chama a janela de composição do programa direto. O arquivo continua sendo o
    registro do que foi escrito.
    """
    from email.message import EmailMessage
    msg = EmailMessage()
    msg["To"] = destinatario
    msg["Subject"] = assunto
    msg["X-Unsent"] = "1"
    msg.set_content(corpo)
    if anexo and os.path.exists(anexo):
        with open(anexo, "rb") as f:
            msg.add_attachment(
                f.read(), maintype="application",
                subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                filename=os.path.basename(anexo))
    os.makedirs(os.path.dirname(os.path.abspath(saida_path)), exist_ok=True)
    with open(saida_path, "wb") as f:
        f.write(bytes(msg))
    return saida_path


def rascunhos(carteiras, planilhas, agenda, projeto, gerente, dias,
              log=lambda _m: None, hoje=None):
    """Um rascunho por dono que TEM endereço.

    Devolve ({nome: {para, assunto, corpo, anexo, eml}}, [sem endereço]). A mensagem
    inteira volta, e não só o caminho do arquivo, porque quem abre a janela de envio
    (`abrir_para_enviar`) precisa do texto — não do .eml, que o Thunderbird abriria
    somente para leitura.
    """
    pasta = os.path.join(_saida_dir(), "emails")
    feitos, sem_endereco = {}, []
    for nome, itens in sorted(carteiras.items()):
        endereco = endereco_de(nome, agenda)
        if not endereco:
            sem_endereco.append(nome)
            continue
        assunto = Tf("{0} — as suas atividades dos próximos {1} dias", projeto, dias)
        corpo = corpo_do_email(nome, itens, projeto, gerente, dias, hoje=hoje)
        caminho = os.path.join(pasta, f"{_slug(nome)}.eml")
        anexo = planilhas.get(nome)
        try:
            rascunho_email(endereco, assunto, corpo, anexo, caminho)
            feitos[nome] = {"para": endereco, "assunto": assunto, "corpo": corpo,
                            "anexo": anexo, "eml": caminho}
            log(Tf("  {0} <{1}> → {2}", nome, endereco, os.path.basename(caminho)))
        except Exception as e:
            log(Tf("  [aviso] não consegui preparar o e-mail de {0}: {1}", nome, e))
    return feitos, sem_endereco


def abrir(caminho):
    """Abre o arquivo no programa padrão do sistema.
    Falha em silêncio: não conseguir abrir não pode custar o passo — o arquivo está
    gravado e o aluno abre pela pasta."""
    import subprocess
    import sys
    try:
        if sys.platform.startswith("win"):
            os.startfile(caminho)                                   # noqa: S606
        elif sys.platform == "darwin":
            subprocess.Popen(["open", caminho])
        else:
            subprocess.Popen(["xdg-open", caminho])
        return True
    except Exception:
        return False


# ── ABRIR PARA ENVIAR (e não para ler) ───────────────────────────────────────
# O DEFEITO QUE ISTO CONSERTA (Eduardo, 07/08/2026, com print): o .eml aberto no
# Thunderbird vem como MENSAGEM RECEBIDA — a barra mostra Responder / Encaminhar /
# Arquivar / Spam / Excluir, e NÃO existe botão Enviar. O arquivo estava certo; o que
# estava errado era o VERBO. "Abrir um arquivo de mensagem" e "compor uma mensagem" são
# duas coisas diferentes para o programa de e-mail, e nós pedíamos a primeira.
#
# O conserto é chamar a JANELA DE COMPOSIÇÃO do programa, que é a única que tem Enviar —
# e que aceita anexo (um `mailto:` puro abriria a janela certa, mas mailto NÃO anexa
# arquivo, e o anexo é o ponto deste passo).
def _cliente_de_email():
    """(caminho do executável, tipo) do programa que trata mailto: neste computador.

    tipo = 'thunderbird' | 'outlook' | 'mail.app' | '' (desconhecido).
    No Windows a resposta está no registro, no mesmo lugar de onde o próprio sistema a
    lê quando alguém clica num link mailto:.
    """
    import sys
    if sys.platform == "darwin":
        return ("", "mail.app")
    if not sys.platform.startswith("win"):
        return ("", "")
    try:
        import winreg
    except ImportError:
        return ("", "")
    for raiz, chave in ((winreg.HKEY_CURRENT_USER,
                         r"SOFTWARE\Classes\mailto\shell\open\command"),
                        (winreg.HKEY_CLASSES_ROOT, r"mailto\shell\open\command")):
        try:
            with winreg.OpenKey(raiz, chave) as k:
                comando = winreg.QueryValueEx(k, "")[0] or ""
        except OSError:
            continue
        m = re.match(r'\s*"([^"]+)"', comando) or re.match(r"\s*(\S+)", comando)
        if not m:
            continue
        exe = m.group(1)
        base = os.path.basename(exe).lower()
        if "thunderbird" in base:
            return (exe, "thunderbird")
        if "outlook" in base:
            return (exe, "outlook")
        return (exe, "")
    return ("", "")


def _sem_aspa_simples(texto):
    """A aspa simples é o que quebra o parâmetro -compose do Thunderbird: os valores vão
    entre aspas simples, e uma no meio fecha o campo cedo (o corpo chegaria cortado).
    Trocamos pela tipográfica ’ — muda um caractere no rascunho, e nunca o sentido.
    O .eml gravado em disco continua com o texto exato."""
    return (texto or "").replace("'", "’")


def abrir_para_enviar(destinatario, assunto, corpo, anexo, eml=None):
    """Abre a mensagem numa JANELA DE ENVIO, com o anexo. Devolve True se conseguiu.

    Cai para o .eml quando não reconhece o programa — lá o aluno ainda envia, só que com
    um passo a mais (no Thunderbird, Mensagem ▸ Editar como nova; no Outlook o
    `X-Unsent` já abre direto na composição).
    """
    import subprocess
    exe, tipo = _cliente_de_email()
    anexo_uri = "file:///" + os.path.abspath(anexo).replace("\\", "/") if anexo else ""
    try:
        if tipo == "thunderbird" and exe:
            campos = [f"to='{_sem_aspa_simples(destinatario)}'",
                      f"subject='{_sem_aspa_simples(assunto)}'",
                      f"body='{_sem_aspa_simples(corpo)}'"]
            if anexo_uri:
                campos.append(f"attachment='{anexo_uri}'")
            subprocess.Popen([exe, "-compose", ",".join(campos)])
            return True
        if tipo == "outlook" and exe:
            from urllib.parse import quote
            alvo = (f"{destinatario}?subject={quote(assunto)}&body={quote(corpo)}")
            cmd = [exe, "/c", "ipm.note", "/m", alvo]
            if anexo:
                cmd += ["/a", os.path.abspath(anexo)]
            subprocess.Popen(cmd)
            return True
        if tipo == "mail.app":
            script = _applescript_mail(destinatario, assunto, corpo,
                                       os.path.abspath(anexo) if anexo else "")
            subprocess.Popen(["osascript", "-e", script])
            return True
    except Exception:
        pass
    return abrir(eml) if eml else False


def _applescript_mail(destinatario, assunto, corpo, anexo):
    """O AppleScript que abre o Mail.app já com destinatário, texto e anexo, SEM enviar."""
    def esc(t):
        return (t or "").replace("\\", "\\\\").replace('"', '\\"')
    linhas = [
        'tell application "Mail"',
        f'  set m to make new outgoing message with properties '
        f'{{subject:"{esc(assunto)}", content:"{esc(corpo)}", visible:true}}',
        '  tell m',
        f'    make new to recipient at end of to recipients with properties '
        f'{{address:"{esc(destinatario)}"}}',
    ]
    if anexo:
        linhas.append('    tell content')
        linhas.append(f'      make new attachment with properties '
                      f'{{file name:(POSIX file "{esc(anexo)}")}} at after last paragraph')
        linhas.append('    end tell')
    linhas += ['  end tell', '  activate', 'end tell']
    return "\n".join(linhas)


# ── o quadro: as etiquetas que fazem o filtro ────────────────────────────────
# O ProjectAdm nao tem API de FILTRO SALVO — o que ele tem, e o que a interface dele
# filtra, e' ETIQUETA. Entao "criar um filtro" aqui e' marcar o cartao com a etiqueta
# certa: o aluno abre o quadro, clica na etiqueta "Atrasada" e ve exatamente a lista que
# o e-mail mandou. As duas visoes passam a contar a mesma historia.
ETIQUETA_ATRASADA = T("Atrasada")
ETIQUETA_HORIZONTE = T("Vence em breve")

_CORES = {
    ETIQUETA_ATRASADA: ["#E53935", "vermelho", "red"],
    ETIQUETA_HORIZONTE: ["#FB8C00", "laranja", "orange"],
}

# A LIMPEZA ACEITA OS DOIS IDIOMAS (`DEV_STANDARDS`, migrar sem quebrar). Quem marca usa
# o nome do idioma do pacote; quem DESMARCA tem de reconhecer tambem o nome do outro,
# senao um quadro montado em portugues e aberto pelo pacote ingles fica com "Atrasada"
# grudada para sempre ao lado de "Late" — duas etiquetas para o mesmo fato, e a que o
# aluno filtra e' a errada.
_NOMES_DAS_NOSSAS = {"Atrasada", "Vence em breve", ETIQUETA_ATRASADA, ETIQUETA_HORIZONTE}


def marcar_no_quadro(itens, log=lambda _m: None):
    """Põe "Atrasada" / "Vence em breve" nos cartões, e TIRA de quem saiu da situação.

    Tirar é metade do trabalho: uma etiqueta que só entra vira ruído em duas semanas —
    o cartão entregue continuaria vermelho, e o aluno pararia de olhar para a cor.
    Devolve (marcados, limpos). Nunca levanta: sem etiqueta o passo apenas não marca.
    """
    from pa_client import (anexar_etiqueta, desanexar_etiqueta, etiquetas_do_cartao,
                           garantir_etiqueta)
    ids_etq = {}
    for nome, cores in _CORES.items():
        try:
            eid = garantir_etiqueta(nome, cores)
        except Exception:
            eid = None
        if eid:
            ids_etq[nome] = str(eid)
    if not ids_etq:
        log(T("  [aviso] não consegui criar as etiquetas no quadro — sigo sem marcar."))
        return 0, 0

    desejada = {ATRASADA: ids_etq.get(ETIQUETA_ATRASADA),
                NO_HORIZONTE: ids_etq.get(ETIQUETA_HORIZONTE)}
    # tudo o que ESTE módulo já pôs no quadro, nos dois idiomas — é o que ele pode tirar.
    # Etiqueta de outra origem (a "🔎 Revisar" da auditoria, as do próprio aluno) nunca
    # entra nesse conjunto: mexer nelas seria apagar o trabalho de outra pessoa.
    nossas = {v for v in ids_etq.values() if v}
    try:
        from pa_client import list_etiquetas
        nossas |= {str(e.get("id")) for e in (list_etiquetas() or [])
                   if (e.get("nome") or "").strip() in _NOMES_DAS_NOSSAS}
    except Exception:
        pass
    marcados = limpos = 0
    for i in itens:
        cid = i.get("id")
        if not cid:
            continue
        quer = desejada.get(i.get("situacao"))
        try:
            atuais = {str(e.get("id")) for e in (etiquetas_do_cartao(cid) or [])}
        except Exception:
            continue
        for eid in (atuais & nossas) - ({quer} if quer else set()):
            try:
                desanexar_etiqueta(cid, eid)
                limpos += 1
            except Exception:
                pass
        if quer and quer not in atuais:
            try:
                anexar_etiqueta(cid, quer)
                marcados += 1
            except Exception:
                pass
    return marcados, limpos


# ── a foto da execucao, em texto ─────────────────────────────────────────────
def panorama(itens, carteiras, orfas, dias, hoje=None):
    """O texto que o Passo 7 imprime E que entra no prompt da IA.

    É a MESMA foto nos dois lugares, de propósito: a IA passa a raciocinar sobre os
    números reais do quadro em vez de adivinhar o que está nele — que era o problema do
    Passo 7 antigo (Eduardo, 2026-08-07).
    """
    hoje = hoje or datetime.date.today()
    atrasadas = [i for i in itens if i.get("situacao") == ATRASADA]
    proximas = [i for i in itens if i.get("situacao") == NO_HORIZONTE]
    linhas = [Tf("SITUAÇÃO DA EXECUÇÃO em {0} (horizonte: {1} dias):",
                 hoje.isoformat(), dias), ""]
    linhas.append(Tf("  Atrasadas: {0} · Vencendo no horizonte: {1} · "
                     "Sem responsável: {2}", len(atrasadas), len(proximas), len(orfas)))
    linhas.append("")
    if atrasadas:
        linhas.append(T("ATRASADAS (término já passou):"))
        for i in atrasadas:
            linhas.append(Tf("  • {0} {1} — {2} · era para {3} ({4} dia(s))",
                             i.get("codigo", ""), i["titulo"],
                             i.get("responsavel") or T("sem responsável"),
                             i.get("fim", ""),
                             (hoje - i["fim_data"]).days if i.get("fim_data") else "?"))
        linhas.append("")
    if proximas:
        linhas.append(Tf("VENCENDO NOS PRÓXIMOS {0} DIAS:", dias))
        for i in proximas:
            linhas.append(Tf("  • {0} {1} — {2} · até {3}", i.get("codigo", ""),
                             i["titulo"], i.get("responsavel") or T("sem responsável"),
                             i.get("fim", "")))
        linhas.append("")
    if carteiras:
        linhas.append(T("CARGA POR PESSOA (na janela):"))
        for nome, seus in sorted(carteiras.items(), key=lambda kv: -len(kv[1])):
            atr = len([i for i in seus if i["situacao"] == ATRASADA])
            linhas.append(Tf("  • {0}: {1} atividade(s), {2} atrasada(s)",
                             nome, len(seus), atr))
        linhas.append("")
    if orfas:
        linhas.append(T("SEM RESPONSÁVEL (ninguém executa o que não é de ninguém):"))
        for i in orfas:
            linhas.append(Tf("  • {0} {1} · até {2}", i.get("codigo", ""), i["titulo"],
                             i.get("fim", "")))
        linhas.append("")
    return "\n".join(linhas).rstrip()


# ── a orquestracao: o que o Passo 7 roda ANTES de falar com qualquer IA ──────
def dirigir(c, ids, perguntar=input, log=None, hoje=None):
    """Lê o cronograma e DIRIGE: planilhas por dono, consolidada, etiquetas no quadro e
    um rascunho de e-mail por pessoa. Devolve a foto da execução (o texto do panorama),
    que o prompt do Passo 7 recebe como [INPUT SITUACAO_EXECUCAO].

    A ordem é a da conversa que um gerente tem de verdade: primeiro OLHA (o panorama),
    depois cobra o que não tem dono, depois escreve para cada um. Nada aqui depende de
    IA — e é esse o ponto: o quadro já sabe.
    """
    from runner import ok, step, warn
    log = log or step
    dias = horizonte(c)
    hoje = hoje or datetime.date.today()
    projeto = (c.get("PROJECT") or c.get("PROJETO") or "").strip() or T("Meu Projeto")
    gerente = (c.get("MANAGER") or c.get("GERENTE") or "").strip()

    todas = classificar(trabalho(atividades(ids)), hoje=hoje, dias=dias)
    janela = na_janela(todas)
    # duas grafias do mesmo dono = uma pessoa só: um e-mail, uma planilha (ver
    # `juntar_por_pessoa`). Quem decide é a conta do ProjectAdm, não a semelhança.
    carteiras = juntar_por_pessoa(ids, por_responsavel(janela))
    orfas = sem_dono(janela)
    foto = panorama(janela, carteiras, orfas, dias, hoje=hoje)
    print("\n" + foto + "\n")

    if not janela:
        ok(Tf("Nada atrasado e nada vencendo nos próximos {0} dias — o cronograma está "
              "em dia. Se isso te surpreendeu, o mais provável é que as datas do Passo 4 "
              "ainda não estejam no quadro.", dias))
        return foto

    # 1) AS PLANILHAS — a de cada dono e a do gerente.
    log(T("Planilhas de execução (_saida/execucao/):"))
    planilhas = planilhas_por_pessoa(carteiras, projeto, log=log, hoje=hoje)
    planilha_consolidada(janela, projeto, log=log, hoje=hoje)

    # 2) O QUADRO — a etiqueta É o filtro (o ProjectAdm não tem filtro salvo na API).
    marcados, limpos = marcar_no_quadro(todas, log=log)
    if marcados or limpos:
        ok(Tf('{0} cartão(ões) marcado(s) e {1} liberado(s) no quadro. No ProjectAdm, '
              'filtre pela etiqueta "{2}" para ver exatamente esta lista.',
              marcados, limpos, ETIQUETA_ATRASADA))

    # 3) OS ENDEREÇOS — do quadro; o que faltar, pergunta e GRAVA no cartão da pessoa,
    #    para o quadro seguir sendo a fonte e ninguém digitar isto duas vezes.
    # resolvido UMA vez, e daqui para a frente a chave é o nome do DONO como ele aparece
    # no cronograma — assim o `endereco_de` de baixo casa exato e não volta a adivinhar.
    enderecos = enderecos_dos_donos(ids, sorted(carteiras))
    faltando = [n for n, v in enderecos.items() if not v]
    if faltando:
        print("")
        step(Tf("Não tenho o e-mail de {0} pessoa(s). Escreva agora (ou Enter para pular "
                "— eu gravo no cartão da pessoa, no quadro).", len(faltando)))
        for nome in faltando:
            try:
                resp = (perguntar(Tf("   e-mail de {0}: ", nome)) or "").strip()
            except (EOFError, KeyboardInterrupt):
                print("")
                break
            if not resp:
                continue
            # O endereço vale para ESTA rodada mesmo que a gravação falhe: o aluno acabou
            # de dizer para quem é. Não gravar no quadro é um problema de persistência —
            # não é motivo para não escrever o e-mail que ele veio escrever.
            enderecos[nome] = resp
            if gravar_email(ids, nome, resp):
                ok(Tf("   gravado no cartão de {0}.", nome))
            else:
                warn(Tf("   não consegui gravar no cartão de {0} — escreva a linha "
                        '"e-mail: {1}" no cartão dela, na lista Partes Interessadas.',
                        nome, resp))

    # 4) OS RASCUNHOS — prontos, com a planilha anexada. QUEM ENVIA É O ALUNO.
    print("")
    log(T("Rascunhos de e-mail (_saida/execucao/emails/):"))
    feitos, sem_endereco = rascunhos(carteiras, planilhas, enderecos, projeto, gerente,
                                     dias, log=log, hoje=hoje)
    if feitos:
        ok(Tf("{0} rascunho(s) prontos, com a planilha de cada pessoa anexada. "
              "Eles NÃO foram enviados: abra, leia e mande você — o recado é seu.",
              len(feitos)))
    if sem_endereco:
        warn(Tf("sem e-mail para: {0}. Ligue essas pessoas à conta delas no ProjectAdm "
                "(rode o Passo 1) ou escreva a linha \"e-mail: …\" no cartão delas, na "
                "lista Partes Interessadas.", ", ".join(sem_endereco)))
    if feitos:
        try:
            resp = (perguntar(Tf("\n   Abro as {0} mensagem(ns) no seu programa de e-mail, "
                                 "prontas para enviar? (s/N) ",
                                 len(feitos))) or "").strip().lower()
        except (EOFError, KeyboardInterrupt):
            resp = ""
        if resp[:1] in ("s", "y"):
            for msg in feitos.values():
                abrir_para_enviar(msg["para"], msg["assunto"], msg["corpo"],
                                  msg["anexo"], msg["eml"])
            ok(T("Cada mensagem abriu numa JANELA DE ENVIO, com a planilha anexada. "
                 "Confira o texto e aperte Enviar — quem envia é você."))
    if orfas:
        warn(Tf("{0} atividade(s) da janela não têm responsável — ninguém vai receber "
                "e-mail sobre elas. Defina o dono no cartão (Passo 4) antes de cobrar "
                "o prazo.", len(orfas)))
    return foto
