"""
Together Kit — atualizar os scripts do curso SEM perder o seu .env.

Por que existe: ao baixar uma versao nova do curso, extrair o .zip por cima da
pasta a mao e arriscado — e facil sobrescrever o .env e perder o seu token, o
BOARD_ID e os IDs de listas. Este script faz isso com seguranca.

O que faz:
  1. BAIXA a versao mais nova do curso direto do site (link no marcador .together).
     Sem internet ou link? Cai para o .zip mais recente da sua pasta Downloads.
  2. Faz backup dos .py atuais em _backup_scripts/ (rolante; nunca acumula).
  3. Substitui os .py/prompts/README pela versao nova.
  4. PRESERVA o seu .env e apenas ADICIONA, ao final, qualquer variavel NOVA
     que o curso passou a usar (com o valor padrao) — sem tocar no que voce ja
     preencheu.

Como usar:
  - Voce nao precisa rodar isto a mao. Ao abrir o curso pelo Iniciar, ele confere
    uma vez por dia se saiu versao nova e pergunta se voce quer atualizar; o menu
    tambem tem a opcao. Ele baixa e atualiza sozinho.
"""
import glob
import json
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.request
import zipfile
from idioma import T, Tf

try:
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except Exception:
    pass

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


def _course():
    p = os.path.join(HERE, '.together')
    if os.path.exists(p):
        try:
            return json.load(open(p, encoding='utf-8'))
        except Exception:
            return {}
    return {}


def _is_course_zip(path, slug=None):
    """E o pacote DESTE curso?

    Nao basta "parecer um pacote Together": o aluno pode ter o zip do BPM Together e
    o deste curso na mesma pasta Downloads. Se o zip traz o marcador .together, o
    slug tem de ser o mesmo — senao atualizariamos o curso com os scripts de OUTRO.
    """
    try:
        with zipfile.ZipFile(path) as z:
            names = z.namelist()
            if 'env.py' not in names or 'pa_client.py' not in names:
                return False
            if slug and '.together' in names:
                try:
                    meta = json.loads(z.read('.together').decode('utf-8'))
                except Exception:
                    return False
                return (meta.get('slug') or '') == slug
        return True
    except Exception:
        return False


def _download_zip(course):
    """Baixa o .zip do curso direto do site (download_url do .together) para um
    arquivo temporario. Devolve o caminho, ou None se nao houver link / falhar
    (ai o main cai para a pasta Downloads)."""
    url = (course.get('download_url') or '').strip()
    if not url:
        return None
    tmp = os.path.join(tempfile.gettempdir(), 'together_update_pkg.zip')
    try:
        print(Tf("  baixando a versao mais nova de {0} ...", url))
        req = urllib.request.Request(url, headers={'User-Agent': 'together-kit-update'})
        with urllib.request.urlopen(req, timeout=60) as r, open(tmp, 'wb') as f:
            shutil.copyfileobj(r, f)
    except Exception as e:
        print(Tf("  nao consegui baixar automaticamente ({0}); vou procurar o .zip na pasta "
                 "Downloads.", e.__class__.__name__))
        return None
    if _is_course_zip(tmp, course.get('slug')):
        print(Tf("  baixado: {0} KB", os.path.getsize(tmp) // 1024))
        return tmp
    print(T("  o arquivo baixado nao parece o pacote do curso — usando a pasta Downloads."))
    try:
        os.remove(tmp)
    except OSError:
        pass
    return None


def _find_zip(course):
    dl = os.path.join(os.path.expanduser('~'), 'Downloads')
    slug = course.get('slug')
    cands = []
    pkg = course.get('package_name')
    if pkg:
        cands += glob.glob(os.path.join(dl, f"{pkg}*.zip"))
    cands += glob.glob(os.path.join(dl, "*together*.zip"))
    seen, uniq = set(), []
    for c in cands:
        if c not in seen and _is_course_zip(c, slug):
            seen.add(c)
            uniq.append(c)
    return max(uniq, key=os.path.getmtime) if uniq else None


def _merge_env(zip_env_text):
    """Mantem o .env local; acrescenta apenas chaves NOVAS do template.

    "Nova" e' pelo DADO, nao pelo nome da linha (07/08/2026). As chaves passaram a ser
    em ingles em 05/08 e o .env de quem ja' estava no curso traz as antigas: comparando
    so' o nome cru, toda atualizacao acrescentava PROJECT ao lado de PROJETO, LANGUAGE
    ao lado de IDIOMA — duas linhas para o mesmo dado, com o texto-guia do template na
    linha nova. O aluno editava uma e o curso lia a outra; e um LANGUAGE do template
    caindo ao lado de um IDIOMA do aluno chegava a TROCAR O IDIOMA do curso no meio do
    caminho. O apelido (env.ALIASES) e' o que sabe que sao o mesmo dado.
    """
    env_path = os.path.join(HERE, '.env')
    if not os.path.exists(env_path):
        with open(env_path, 'w', encoding='utf-8') as f:
            f.write(zip_env_text)
        return ['(criado do template)']
    from env import ALIASES
    apelidos = dict(ALIASES)
    apelidos.update({velha: nova for nova, velha in ALIASES.items()})
    local_keys = set()
    for line in open(env_path, encoding='utf-8'):
        s = line.strip()
        if s and not s.startswith('#') and '=' in s:
            local_keys.add(s.split('=', 1)[0].strip())
    add_lines, added = [], []
    for line in zip_env_text.splitlines():
        s = line.strip()
        if s and not s.startswith('#') and '=' in s:
            k = s.split('=', 1)[0].strip()
            if k not in local_keys and apelidos.get(k) not in local_keys:
                add_lines.append(line)
                added.append(k)
    if add_lines:
        with open(env_path, 'a', encoding='utf-8') as f:
            f.write('\n# (novas variaveis adicionadas pela atualizacao)\n'
                    + '\n'.join(add_lines) + '\n')
    return added


def _mostrar_novidades():
    """Imprime "o que mudou nesta versao" — as linhas que o pacote NOVO trouxe.

    POR QUE (Eduardo, 18/08/2026): "descrever controle de versoes com as melhorias
    implementadas". A versao do pacote sempre existiu (AAAA.MM.DD.hhmm no .together) e
    sempre foi invisivel: o aluno atualizava, lia "Atualizacao concluida" e voltava ao
    menu sem saber que ganhou uma tecla nova. Um recurso que ninguem sabe que existe
    nao foi entregue.

    Le o `.together` DEPOIS da extracao — ele veio dentro do zip, entao as novidades sao
    as da versao que acabou de chegar. Nunca levanta: nada aqui pode derrubar uma
    atualizacao que ja terminou.
    """
    try:
        d = _course()
        linhas = d.get('novidades') or []
        if not linhas:
            return
        print()
        print(Tf("O QUE MUDOU nesta versao ({0}):", d.get('versao') or '?'))
        for l in linhas:
            print(f"  • {l}")
    except Exception:
        pass


def _registrar_versao(zip_path):
    """Anota qual pacote esta instalado agora — o que o bootstrap.py compara, uma vez
    por dia, com o que esta publicado no site ("saiu versao nova?").

    Guardamos o TAMANHO do zip instalado porque e' o unico dado da versao publicada
    que da' para obter sem baixar os 12 MB (um HEAD devolve o Content-Length). Fica
    num arquivo LOCAL — ele nao vem dentro do zip, entao a atualizacao nao o apaga.
    """
    try:
        with open(os.path.join(HERE, '.together_versao'), 'w', encoding='utf-8') as f:
            json.dump({"tamanho": os.path.getsize(zip_path)}, f)
    except OSError:
        pass                                   # comodidade: nunca quebra a atualizacao


def _board_config_script(course):
    """Nome do script que configura o board deste curso.

    Prioriza o marcador .together (chave 'configure_script'); se faltar (pacotes
    antigos, gerados antes desta versao), DETECTA pelo nome — qualquer
    *configurar*board*.py na pasta (pega tanto o 01_configurar_board_processo.py
    do BPM quanto um configurar_board.py). Devolve None se nao achar nenhum."""
    name = (course.get('configure_script') or '').strip()
    if name and os.path.exists(os.path.join(HERE, name)):
        return name
    cands = [os.path.basename(p)
             for p in glob.glob(os.path.join(HERE, '*configurar*board*.py'))]
    if not cands:
        return None
    # prefere o que comeca com "01" (o 1o passo), senao o primeiro em ordem
    cands.sort(key=lambda n: (not n.startswith('01'), n))
    return cands[0]


def _run_board_config(course):
    """Roda o script de configurar o board logo apos atualizar — assim o aluno
    nao precisa lembrar de faze-lo a cada atualizacao (era o esquecimento mais
    comum). E idempotente: recria as listas/cards que faltarem e PRESERVA os
    seus dados. Se parar (ex.: .env ainda sem TOKEN/BOARD_ID), avisa sem quebrar
    a atualizacao, que ja terminou."""
    script = _board_config_script(course)
    if not script:
        print(T("  (nao achei o script de configurar o board nesta pasta; se preciso, "
              "rode o card 01 a mao.)"))
        return
    print(Tf("\nConfigurando o board automaticamente ({0}) — voce nao precisa mais rodar "
             "isso a mao...\n", script) + "-" * 60)
    try:
        r = subprocess.run([sys.executable, os.path.join(HERE, script)], cwd=HERE)
    except Exception as e:
        print("-" * 60)
        print(Tf("  nao consegui rodar o {0} automaticamente ({1}); rode o card 01 a mao.", script, e.__class__.__name__))
        return
    print("-" * 60)
    if r.returncode == 0:
        print(T("Board configurado. Tudo pronto."))
    else:
        print(T("A ATUALIZACAO TERMINOU (os scripts novos ja estao aqui)."))
        print(T("  O passo acima nao rodou ate o fim — e o normal se voce ainda nao fez o"))
        print(T("  Passo 1: ele precisa do seu projeto no .env. Siga pelo menu; quando o"))
        print(T("  Passo 1 estiver feito, este passo passa a rodar sozinho a cada atualizacao."))


def main():
    course = _course()
    print(Tf("Atualizar — {0}", course.get('label') or T("curso Together")))
    zp = _download_zip(course) or _find_zip(course)
    if not zp:
        url = (course.get('download_url') or '').strip()
        if url:
            raise SystemExit(
                Tf('Nao consegui baixar a versao nova nem achar o .zip na pasta Downloads.\n   '
                   'Baixe por este link e salve em Downloads: {0}\n   Depois rode a '
                   'atualizacao de novo.', url))
        raise SystemExit(
            T("Este pacote ainda nao tem atualizacao automatica (curso em pre-lancamento):\n"
            "  ele foi entregue sem link de download.\n"
            "  Voce ja esta com a versao que baixou — quando sair uma nova, o e-mail do\n"
            "  curso avisa. Basta salvar o .zip novo na pasta Downloads e rodar isto aqui:\n"
            "  o seu .env (token, quadro, progresso) e sempre preservado."))
    is_temp = os.path.dirname(os.path.abspath(zp)) == os.path.abspath(tempfile.gettempdir())
    print(Tf("  pacote: {0}", os.path.basename(zp)))

    # backup rolante dos .py atuais
    bak = os.path.join(HERE, '_backup_scripts')
    if os.path.exists(bak):
        shutil.rmtree(bak)
    os.makedirs(bak)
    for f in glob.glob(os.path.join(HERE, '*.py')):
        shutil.copy2(f, bak)

    # extrai tudo EXCETO o .env (preserva o do aluno) e EXCETO o Python portatil, que
    # ja esta na pasta e nao muda (sao ~11 MB — reescrever a cada atualizacao seria so
    # espera). Se por acaso a pasta python\ tiver sumido, ai sim ele volta pelo zip.
    tem_python = os.path.exists(os.path.join(HERE, 'python', 'python.exe'))
    zip_env_text, updated = None, []
    with zipfile.ZipFile(zp) as z:
        for n in z.namelist():
            if n.endswith('/'):
                continue                       # entrada de pasta
            if n == '.env':
                zip_env_text = z.read(n).decode('utf-8')
                continue
            if tem_python and n.startswith('python/'):
                continue                       # o Python portatil ja esta aqui
            destino = os.path.join(HERE, n)
            os.makedirs(os.path.dirname(destino), exist_ok=True)   # pacotes com subpastas
            with z.open(n) as src, open(destino, 'wb') as dst:
                dst.write(src.read())
            updated.append(n)

    added = _merge_env(zip_env_text) if zip_env_text else []
    print(Tf("  atualizados: {0} arquivo(s)", len(updated)))
    print(T("  .env: preservado") + (Tf("; novas chaves: {0}", ', '.join(added))
                                     if added else T(" (sem novas chaves)")))
    print(Tf("  backup dos .py antigos em: {0}/", os.path.relpath(bak, HERE)))
    _registrar_versao(zp)
    _mostrar_novidades()
    if is_temp:
        try:
            os.remove(zp)
        except OSError:
            pass
    print(T("Atualizacao concluida."))
    _run_board_config(course)


if __name__ == '__main__':
    main()
