"""
Project Together — powered by AI: RECALCULAR — ajustar o plano sem re-rodar tudo.

PARA QUE SERVE (o pedido do Eduardo, 28/07: "ajustar de forma mais fácil o escopo,
o cronograma e o orçamento")

  Você mudou alguma coisa no quadro: adiou um pacote, trocou o valor de um custo, tirou
  ou acrescentou uma entrega. Antes, para o plano voltar a fechar, era preciso re-rodar o
  Passo 4 INTEIRO — uma rodada de IA cara, que ainda reescrevia o que estava bom.

  Aqui não há IA. Este script refaz só o que é CONTA — e conta é o que o computador faz
  melhor que qualquer modelo de linguagem:

    1. EMPURRA quem depende de quem. Se um pacote atrasou, o sucessor anda junto,
       respeitando o TIPO da dependência (TI · II · TT · IT) e contando só dias úteis.
       Nunca antecipa nada: só empurra o que ficou impossível.
    2. SOBE as datas para as entregas e fases (as barras-resumo do Gantt).
    3. REFAZ as contas do orçamento e a coluna Custo do Dicionário da EAP.
    4. REGERA os seus documentos, já com tudo isso dentro.

  O plano é o QUADRO; o documento é o retrato dele. Você ajusta o quadro — que é onde
  você já está — e o Python devolve o plano coerente.

SEGURANÇA: ele SEMPRE simula primeiro e mostra o que vai mudar. Nada é gravado antes de
você confirmar. Rodar de novo sem ter mudado nada não faz nada (é idempotente).

Uso:
    py recalcular.py             # simula, mostra e pergunta se aplica
    py recalcular.py --dry-run   # só simula (nunca grava)
"""
import datetime
import sys

import quadro
from env import cfg, require
from pa_client import limpar_cache, list_dependencies, update_card
from runner import done, header, ok, step, warn
from idioma import T, Tf

# tipo da dependencia -> a regra que a data do SUCESSOR tem de respeitar.
#   TI (finish_to_start)  : comeca DEPOIS que a predecessora termina
#   II (start_to_start)   : nao comeca antes de a predecessora comecar
#   TT (finish_to_finish) : nao termina antes de a predecessora terminar
#   IT (start_to_finish)  : nao termina antes de a predecessora comecar (raro)
_REGRA = {"finish_to_start": ("fim", "ini", 1),
          "start_to_start": ("ini", "ini", 0),
          "finish_to_finish": ("fim", "fim", 0),
          "start_to_finish": ("ini", "fim", 0)}
_NOME_TIPO = {"finish_to_start": "TI", "start_to_start": "II",
              "finish_to_finish": "TT", "start_to_finish": "IT"}


def _data(iso):
    try:
        return datetime.date.fromisoformat((iso or "")[:10])
    except ValueError:
        return None


def _proximo_util(d):
    while d.weekday() >= 5:            # sabado/domingo -> segunda
        d += datetime.timedelta(days=1)
    return d


def _mais_dias_uteis(d, n):
    """A data `n` dias ÚTEIS depois de `d` (n=0 devolve o próprio dia útil)."""
    d = _proximo_util(d)
    for _ in range(max(0, n)):
        d = _proximo_util(d + datetime.timedelta(days=1))
    return d


def _dias_uteis(ini, fim):
    """Dias úteis entre duas datas, inclusive — a DURAÇÃO que a tarefa tem hoje."""
    if not ini or not fim or fim < ini:
        return 1
    n, d = 0, ini
    while d <= fim:
        if d.weekday() < 5:
            n += 1
        d += datetime.timedelta(days=1)
    return max(1, n)


def _ler_dependencias(nos):
    """[(id_predecessor, id_sucessor, tipo)] — os links REAIS do quadro (as setas do
    Gantt), lidos em paralelo. Só entre cartões da EAP."""
    from concurrent.futures import ThreadPoolExecutor
    nossos = {str(n["id"]) for n in nos}
    links = []

    def _do(n):
        try:
            return [(str(p.get("cartao_origem_id")), str(n["id"]),
                     p.get("tipo") or "finish_to_start")
                    for p in (list_dependencies(n["id"]).get("predecessores") or [])]
        except Exception:
            return []

    with ThreadPoolExecutor(max_workers=8) as ex:
        for pares in ex.map(_do, nos):
            links += [l for l in pares if l[0] in nossos and l[0] != l[1]]
    return links


def _empurrar(nos, links):
    """A conta do cronograma: enquanto houver sucessor começando cedo demais, empurra.

    Devolve {id: (ini_novo, fim_novo)} só do que MUDA. Preserva a DURAÇÃO de cada tarefa
    (em dias úteis) — empurrar não é encurtar. Só as FOLHAS (pacotes/histórias) entram:
    entrega e fase recebem as datas pelo rollup, dos filhos."""
    # FOLHA = nó sem filhos na EAP (nenhum código começa com o dele + "."). Vale nas duas
    # abordagens: na preditiva é o pacote de trabalho; no ágil, o item do backlog.
    codigos = [n["codigo"] for n in nos]
    folhas = {str(n["id"]): n for n in nos
              if not any(c.startswith(n["codigo"] + ".") for c in codigos)}
    datas, dur = {}, {}
    for cid, n in folhas.items():
        ini, fim = _data(n.get("ini")), _data(n.get("fim"))
        if not ini and not fim:
            continue
        ini = ini or fim
        fim = fim or ini
        datas[cid] = [ini, max(fim, ini)]
        dur[cid] = _dias_uteis(ini, max(fim, ini))

    ativos = [(p, s, t) for p, s, t in links if p in datas and s in datas]
    for _volta in range(len(ativos) * 2 + 4):        # converge (ou para, se houver ciclo)
        mudou = False
        for pred, suc, tipo in ativos:
            campo_p, campo_s, folga = _REGRA.get(tipo, _REGRA["finish_to_start"])
            base = datas[pred][0 if campo_p == "ini" else 1]
            minimo = _mais_dias_uteis(base, folga)
            atual = datas[suc][0 if campo_s == "ini" else 1]
            if atual >= minimo:
                continue
            if campo_s == "ini":                      # empurra o início e mantém a duração
                datas[suc][0] = minimo
                datas[suc][1] = _mais_dias_uteis(minimo, dur[suc] - 1)
            else:                                     # empurra o término (TT/IT)
                datas[suc][1] = minimo
                if datas[suc][0] > datas[suc][1]:
                    datas[suc][0] = datas[suc][1]
            mudou = True
        if not mudou:
            break

    saida = {}
    for cid, n in folhas.items():
        if cid not in datas:
            continue
        ini, fim = datas[cid][0].isoformat(), datas[cid][1].isoformat()
        if (ini, fim) != ((n.get("ini") or "")[:10], (n.get("fim") or "")[:10]):
            saida[cid] = (ini, fim)
    return saida


def _resumo_orcamento(ids, nos):
    """As contas do dinheiro (não escreve nada no quadro): total, reserva e quanto do
    orçamento já está ligado a um pacote da EAP (o que vai para a coluna Custo)."""
    import artefatos
    import entradas
    itens = quadro.orcamento_do_quadro(ids)
    if not itens:
        return None
    total = sum(it["valor_num"] for it in itens)
    reservas = sum(it["valor_num"] for it in itens
                   if "reserv" in entradas._sem_acento(it.get("tipo", ""))
                   or "conting" in entradas._sem_acento(it.get("nome", "")))
    custos, fora = artefatos._custo_por_codigo(ids, nos)
    return {"total": total, "reservas": reservas, "base": total - reservas,
            "fora": fora, "na_eap": total - sum(it["valor_num"] for it in fora)}


def main():
    dry = "--dry-run" in sys.argv[1:]
    limpar_cache()
    c = cfg()
    require(c, "PROJECTADM_TOKEN", "BOARD_ID")
    abordagem = quadro.abordagem_do_env()
    if not abordagem:
        raise SystemExit(T("Escolha a abordagem primeiro (rode 01a_escolher_abordagem.py)."))
    header(T("Recalcular o plano — o que é conta, o Python refaz")
           + (T("   [SIMULAÇÃO]") if dry else ""))
    print(T("  Você ajustou o quadro (uma data, um custo, uma entrega). Eu refaço as contas:"))
    print(T("  empurro quem depende de quem, subo as datas para as entregas e fases, refaço"))
    print(T("  o orçamento e regero os seus documentos. Sem IA — e sem tocar no que você"))
    print("  escreveu.\n")

    ids = quadro.garantir_listas(abordagem, log=lambda _m: None)
    nos, _tem_fases = quadro.arvore_da_eap(ids)
    if not nos:
        raise SystemExit(T("Ainda não há escopo no seu quadro — rode o Passo 2 primeiro."))
    por_id = {str(n["id"]): n for n in nos}

    # ── 1) dependências: empurrar o que ficou impossível ──
    links = _ler_dependencias(nos)
    mudancas = _empurrar(nos, links) if links else {}
    if not links:
        step(T("Nenhuma dependência no cronograma — nada a empurrar. (As setas nascem no "
             "Passo 4, no campo \"depende de\".)"))
    elif not mudancas:
        ok(Tf("Cronograma coerente: {0} dependência(s) conferida(s), nenhuma data precisou "
              "andar.", len(links)))
    else:
        print(Tf('  📅 {0} tarefa(s) precisam andar para respeitar as dependências:\n', len(mudancas)))
        for cid, (ini, fim) in list(mudancas.items())[:12]:
            n = por_id.get(cid, {})
            print(f"     {n.get('codigo','?'):<8} {(n.get('titulo') or '')[:42]:<44}")
            print(f"     {'':<8} {(n.get('ini') or '—')} → {ini}   ·   "
                  f"{(n.get('fim') or '—')} → {fim}")
        if len(mudancas) > 12:
            print(Tf("     ... e mais {0}.", len(mudancas) - 12))
        print()

    # ── 2) rollup (simulado) ──
    # Só faz sentido prever o rollup quando NENHUMA data vai andar: se as folhas ainda
    # vão se mover, a foto de agora mentiria. Com mudanças, o rollup roda depois delas,
    # na aplicação — e aí sobre as datas já corrigidas.
    roll = quadro.rollup_datas(ids, dry=True) if not mudancas else []
    if roll:
        print(Tf('  📐 {0} entrega(s)/fase(s) com a barra-resumo desatualizada (vou fazê-la '
                 'abranger os filhos).\n', len(roll)))

    # ── 3) dinheiro ──
    orc = _resumo_orcamento(ids, nos)
    if orc:
        from artefatos import _reais
        print(Tf("  💰 Orçamento: base {0} + reserva {1} = {2}.", _reais(orc['base']), _reais(orc['reservas']), _reais(orc['total'])))
        if orc["fora"]:
            print(Tf("     {0} item(ns) sem pacote da EAP — ficam fora da coluna Custo do "
                     "Dicionário.", len(orc['fora'])))
        print()

    if dry:
        done(T("Simulação — NADA foi alterado. Rode sem --dry-run para aplicar."))
        return

    if not mudancas and not roll:
        step(T("Nada a recalcular no cronograma. Vou regerar os seus documentos com o que "
             "está no quadro agora."))
    else:
        try:
            resp = input(T("  Aplicar isso no seu quadro? [s/N] ")).strip().lower()
        except (EOFError, KeyboardInterrupt):
            resp = ""
        if resp not in ("s", "sim", "y", "yes"):
            print(T("\n  Ok — não mexi em nada. (Os documentos você regera pela tecla D "
                  "quando quiser.)"))
            return
        n_ok = 0
        for cid, (ini, fim) in mudancas.items():
            try:
                update_card(cid, data_inicio=ini, data_vencimento=fim)
                n_ok += 1
            except Exception as e:
                warn(Tf('não consegui mover "{0}": {1}', (por_id.get(cid, {}).get('titulo') or '')[:34], e))
        if n_ok:
            ok(Tf("{0} tarefa(s) reagendada(s) — veja as barras novas no Gantt.", n_ok))
        limpar_cache()
        n_roll = len(quadro.rollup_datas(ids))
        if n_roll:
            ok(Tf("{0} entrega(s)/fase(s) passaram a abranger as datas dos seus pacotes.", n_roll))

    # ── 3b) o código da EAP no cartão ──
    # Reordenar cartões no quadro MUDA o código (ele é posicional): quem arrastou uma
    # entrega para outro lugar tem de ver o número novo no cartão, não só no documento.
    limpar_cache()
    try:
        import campos
        nos_atuais, _tf = quadro.arvore_da_eap(ids)
        n_cod = campos.gravar_codigos(nos_atuais, log=step, ids=ids)
        if n_cod:
            ok(Tf("Código EAP atualizado em {0} cartão(ões) (o código acompanha a posição na "
                  "EAP).", n_cod))
    except Exception as e:
        warn(Tf("não consegui atualizar o campo Código EAP agora ({0}).", e))

    # ── 4) os documentos, sempre ──
    limpar_cache()
    try:
        import artefatos
        gerados = artefatos.gerar_disponiveis(ids, abordagem=abordagem, log=warn)
        for rotulo, caminho in gerados:
            import os as _os
            print(Tf("  📄 _saida/documentos/{0}", _os.path.basename(caminho)))
        if gerados:
            ok(Tf("{0} documento(s) regerado(s) com o plano ajustado.", len(gerados)))
    except Exception as e:
        warn(Tf("não consegui regerar os documentos agora ({0}) — use a tecla D do menu.", e))

    done(T("Plano recalculado. O quadro continua sendo o seu plano — e agora os números "
         "voltaram a fechar."))


if __name__ == "__main__":
    main()
