"""Monta a pasta pública do site para o Vercel, só com arquivos versionados no git.

Uso:
    python3 scripts/publicar_site.py
    cd publicacao/comparacao-governos && npx vercel deploy --prod

Entram: site/, metodologia/, fontes/, eventos/, temas/, dados/tratados/, scripts/, README.md,
os manifestos de dados/brutos/ e os brutos que o site liga a alguma série (mapa do
exportar_site.py). Ficam fora: documentos/ (plano pessoal), .claude/, os brutos que nenhuma
série usa e qualquer arquivo com dados pessoais (D-026). O conteúdo vem do último commit
(git archive), nunca da pasta de trabalho. O Vercel gratuito aceita até 100 MB por envio.

Na cópia publicada, o conteúdo de site/ fica na raiz e cada página responde em /<pagina>
(/economia, /debate...); a raiz abre Economia e /site/<pagina>.html redireciona para o novo
endereço. No servidor local nada muda: http://localhost:8766/site/economia.html.
"""

import html
import io
import json
import re
import shutil
import subprocess
import tarfile

from comum import RAIZ
from exportar_site import LIMITE_BRUTO_SITE

DESTINO = RAIZ / "publicacao" / "comparacao-governos"
PASTAS = ("site/", "metodologia/", "fontes/", "eventos/", "temas/", "dados/tratados/", "scripts/")
# arquivos com nome, CPF/CNPJ, título de eleitor ou data de nascimento de pessoas: nunca no site
PROIBIDOS = [r"^dados/brutos/ibama/.*\.zip$", r"^dados/brutos/tse/consulta_cand", r"^documentos/"]
# brutos que as páginas linkam diretamente, fora do mapa de séries (a transcrição do debate, que é pública)
BRUTOS_LINKADOS = [r"^dados/brutos/debates/transcricao_.*\.md$"]
LIMITE_MB = 95

INICIAL = "economia"
# textos e planilhas que, abertos no navegador, vão para o leitor (/documento); os brutos baixados das fontes ficam como estão
NO_LEITOR = ["/:arquivo(.*\\.md)", "/:arquivo((?:eventos|fontes|dados/tratados|metodologia)/.*\\.csv)"]


def vercel(paginas):
    """Configuração do Vercel: cada página responde em /<pagina>; a raiz abre a página inicial."""
    grupo = "|".join(paginas)
    return {
        "redirects": [
            # .md e .csv clicados no navegador abrem no leitor, com o design do site; quem baixa por script (curl, pandas, R)
            # não manda text/html no accept e recebe o arquivo bruto, e ?bruto=1 fura o desvio (é o que o botão de baixar usa)
            *({"source": origem, "destination": "/documento?f=/:arquivo", "permanent": False,
               "has": [{"type": "header", "key": "accept", "value": ".*text/html.*"}],
               "missing": [{"type": "query", "key": "bruto"}]} for origem in NO_LEITOR),
            # endereços antigos (/site/<pagina>.html) e com .html: permanentes para o endereço limpo
            {"source": f"/site/:pagina({grupo}).html", "destination": "/:pagina", "permanent": True},
            {"source": f"/:pagina({grupo}).html", "destination": "/:pagina", "permanent": True},
            {"source": f"/:pagina({grupo})/", "destination": "/:pagina", "permanent": True},
            {"source": "/site/", "destination": "/", "permanent": True},
            {"source": "/site/:caminho*", "destination": "/:caminho*", "permanent": True},
        ],
        "rewrites": [
            {"source": "/", "destination": f"/{INICIAL}.html"},
            {"source": f"/:pagina({grupo})", "destination": "/:pagina.html"},
        ],
        "headers": [
            # fora dos buscadores até o lançamento
            {"source": "/(.*)", "headers": [{"key": "X-Robots-Tag", "value": "noindex"}]},
            # fichas, notas e CSVs abrem como texto no navegador, em vez de baixar
            {"source": "/(.*)\\.md", "headers": [{"key": "Content-Type", "value": "text/plain; charset=utf-8"}]},
            {"source": "/(.*)\\.csv", "headers": [{"key": "Content-Type", "value": "text/plain; charset=utf-8"}]},
        ],
    }


def git(*args):
    return subprocess.run(["git", *args], capture_output=True, text=True, cwd=RAIZ, check=True).stdout


def brutos_usados():
    """Brutos que o site liga a alguma série, segundo os pacotes de site/dados/*.js do commit."""
    usados = set()
    for js in git("ls-files", "site/dados/*.js").split():
        texto = git("show", f"HEAD:{js}")
        dados = json.loads(texto.split("=", 1)[1].strip().rstrip(";"))
        for lista in dados.get("brutos_serie", {}).values():
            for arquivo in lista:
                info = dados["brutos"].get(arquivo)
                if info:
                    usados.add(f"dados/brutos/{info['pasta']}/{arquivo}")
    return usados


def achatar_site(raiz):
    """Tira as páginas e os recursos de site/ e põe na raiz publicada, para os endereços
    /economia, /debate etc. Os links relativos das páginas continuam valendo (estilo.css,
    img/, dados/*.js vêm junto; ../metodologia/... a partir da raiz cai em /metodologia/...);
    os links entre páginas perdem o .html, também nos seletores do CSS (o que oculta Escândalos
    do menu usa o href). Os arquivos-fonte e o servidor local não mudam."""
    site = raiz / "site"
    paginas = sorted(p.stem for p in site.glob("*.html"))
    for arquivo in sorted(p for p in site.rglob("*") if p.is_file()):
        alvo = raiz / arquivo.relative_to(site)  # site/dados/*.js se junta a dados/tratados/ e dados/brutos/
        if alvo.exists():
            raise SystemExit(f"{arquivo.relative_to(raiz)} colide com {alvo.relative_to(raiz)} na raiz publicada")
        alvo.parent.mkdir(parents=True, exist_ok=True)
        arquivo.rename(alvo)
    shutil.rmtree(site)
    entre_paginas = re.compile(r'href="(%s)\.html(#[^"]*)?"' % "|".join(map(re.escape, paginas)))
    for arquivo in [*(raiz / f"{nome}.html" for nome in paginas), *raiz.glob("*.css")]:
        arquivo.write_text(entre_paginas.sub(r'href="\1\2"', arquivo.read_text(encoding="utf-8")), encoding="utf-8")
    return paginas


TEMAS = {"economia": "Economia", "emprego_renda": "Emprego e renda", "orcamento": "Orçamento", "educacao": "Educação",
         "saude": "Saúde", "social": "Social", "meio_ambiente": "Meio ambiente", "politica_externa": "Política externa"}
PASTAS_NOMES = {"fontes/fichas": "Fichas dos indicadores", "fontes": "Fontes", "fontes/registro": "Registro de fontes",
                "dados": "Dados", "dados/tratados": "Dados tratados", "dados/brutos": "Arquivos baixados das fontes",
                "scripts": "Programas de coleta e cálculo", "eventos": "Linhas do tempo e notas", "eventos/debates": "Debate",
                "temas": "Leituras por tema", "metodologia/modelos": "Modelos de ficha"}


def _titulo_pasta(rel):
    partes = rel.parts
    if len(partes) == 3 and partes[:2] == ("fontes", "fichas"):
        return "Fichas dos indicadores · " + TEMAS.get(partes[2], partes[2])
    if len(partes) == 3 and partes[:2] == ("dados", "brutos"):
        return "Arquivos baixados · " + partes[2]
    return PASTAS_NOMES.get(rel.as_posix(), partes[-1].replace("_", " ").replace("-", " ").capitalize())


def _titulo_md(arquivo):
    """Nome do indicador (cabeçalho da ficha) ou primeiro título do texto."""
    texto = arquivo.read_text(encoding="utf-8", errors="replace")
    m = re.search(r"^nome:\s*(.+)$", texto.split("\n---", 1)[0], re.M) if texto.startswith("---\n") else None
    m = m or re.search(r"^#\s+(.+?)\s*#*\s*$", texto, re.M)
    return re.sub(r"[*_`]", "", m.group(1)) if m else arquivo.name


def indices(raiz):
    """O Vercel não lista pastas: cada pasta sem index.html ganha uma listagem no estilo do site, para que links
    como fontes/fichas/<tema>/ funcionem como no servidor local. Pastas com o nome de uma página (metodologia/)
    ficam sem listagem, para não disputar o endereço com a página."""
    paginas = {p.stem for p in raiz.glob("*.html")}
    base = (raiz / f"{INICIAL}.html").read_text(encoding="utf-8")
    versao = re.search(r'estilo\.css\?v=([\w.-]+)', base).group(1)
    cabecalho = re.search(r'<header class="barra">.*?</header>', base, re.S).group(0).replace(' aria-current="page"', "")
    cabecalho = re.sub(r'href="(%s)(#[^"]*)?"' % "|".join(map(re.escape, sorted(paginas))), r'href="/\1\2"', cabecalho)
    cabecalho = cabecalho.replace('<span id="status-texto"></span>', '<span id="status-texto">Arquivos do projeto</span>')
    # nas listagens os links do menu são absolutos (/escandalos); as regras do estilo.css que ocultam um link valem também para eles
    ocultos = re.findall(r'\.temas a\[href="([\w-]+)"\]\s*\{\s*display:\s*none', (raiz / "estilo.css").read_text(encoding="utf-8"))
    ocultar = "".join(f'  .temas a[href="/{nome}"] {{ display: none; }}\n' for nome in ocultos)
    for pasta in sorted([raiz, *(p for p in raiz.rglob("*") if p.is_dir())]):
        rel = pasta.relative_to(raiz)
        if ".vercel" in rel.parts or pasta in (raiz, raiz / "site") or (pasta / "index.html").exists() or rel.as_posix() in paginas:
            continue
        linhas = []
        if len(rel.parts) > 1:
            linhas.append('<li><a href="../"><span class="t">Pasta acima</span><span class="d">'
                          f'/{html.escape(rel.parent.as_posix())}/</span><span class="k">Pasta</span></a></li>')
        for item in sorted(pasta.iterdir(), key=lambda p: (p.is_file(), p.name)):
            if item.name.startswith("."):
                continue
            if item.is_dir():
                titulo, tipo, detalhe = _titulo_pasta(item.relative_to(raiz)), "Pasta", item.name + "/"
            else:
                kb = f"{max(1, round(item.stat().st_size / 1024)):,} KB".replace(",", ".")
                ext = item.suffix.lower()
                titulo = _titulo_md(item) if ext == ".md" else item.name
                tipo = {".md": "Texto", ".csv": "Planilha"}.get(ext, ext.lstrip(".").upper() or "Arquivo")
                detalhe = (item.name + " · " if ext == ".md" and titulo != item.name else "") + kb
            href = item.name + ("/" if item.is_dir() else "")
            linhas.append(f'<li><a href="{html.escape(href, quote=True)}"><span class="t">{html.escape(titulo)}</span>'
                          f'<span class="d">{html.escape(detalhe)}</span><span class="k">{tipo}</span></a></li>')
        titulo = _titulo_pasta(rel)
        aviso = ('<p class="sobre">Os arquivos estão como vieram de cada órgão, sem edição. Os acima de 2 MB ficam no repositório '
                 'do projeto e não são publicados aqui; o manifesto.csv da pasta traz a URL de origem, a data e o SHA-256 de cada um.</p>'
                 if rel.parts[:2] == ("dados", "brutos") else "")
        (pasta / "index.html").write_text(f"""<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>{html.escape(titulo)} · Lula × Bolsonaro</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Newsreader:opsz,wght@6..72,300;6..72,400;6..72,500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/estilo.css?v={versao}">
<style>
  .pasta {{ max-width: 880px; margin: 0 auto; padding: 4px 0 24px; }}
  .pasta .trilha {{ display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin: 8px 0 18px; font-size: 13.5px; color: var(--tinta-2); }}
  .pasta .trilha a {{ color: var(--link); font-weight: 500; }}
  .pasta h1 {{ font-size: clamp(30px, 5vw, 44px); line-height: 1.1; margin: 0 0 12px; text-wrap: balance; }}
  .pasta .sobre {{ font-size: 16px; line-height: 1.55; color: var(--tinta-2); max-width: 62ch; margin: 0 0 18px; }}
  .itens {{ list-style: none; margin: 8px 0 0; padding: 0; }}
  .itens a {{ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 16px; align-items: center; padding: 13px 4px 13px 0; border-top: 1px solid var(--linha); color: var(--tinta); text-decoration: none; }}
  .itens li:last-child a {{ border-bottom: 1px solid var(--linha); }}
  .itens .t {{ font-weight: 600; font-size: 16px; overflow-wrap: anywhere; }}
  .itens .d {{ grid-column: 1; font-size: 13.5px; color: var(--tinta-2); overflow-wrap: anywhere; }}
  .itens .k {{ grid-column: 2; grid-row: 1 / span 2; font-size: 12.5px; color: var(--tinta-2); white-space: nowrap; }}
  .itens .k::after {{ content: " →"; font-size: 15px; }}
  .itens a:focus-visible {{ outline: 2px solid var(--link); outline-offset: 2px; border-radius: 6px; }}
  @media (hover: hover) {{ .itens a:hover .t {{ text-decoration: underline; text-underline-offset: 3px; }} }}
{ocultar}</style>
</head>
<body>
<div class="app">
  {cabecalho}
  <main class="pasta">
    <nav class="trilha" aria-label="Onde você está"><a href="/metodologia#documentos">← Metodologia e documentos</a><code>/{html.escape(rel.as_posix())}/</code></nav>
    <h1 class="serifa">{html.escape(titulo)}</h1>
    {aviso}
    <ul class="itens">
      {"".join(linhas)}
    </ul>
  </main>
  <footer class="rodape">
    <a href="/metodologia">Metodologia</a>
    <span>Um projeto da <a href="https://otther.co">Otther</a> · sem vínculo com partidos ou candidatos · sem financiamento externo</span>
  </footer>
</div>
<script src="/menu.js?v={versao}"></script>
</body>
</html>
""", encoding="utf-8")

def main():
    if git("status", "--porcelain", "--", *PASTAS, "README.md").strip():
        print("Aviso: há mudanças não commitadas; a publicação usa o último commit.")
    versionados = git("ls-files").split("\n")
    usados = brutos_usados()
    entram, grandes = [], []
    for arquivo in filter(None, versionados):
        if any(re.search(p, arquivo) for p in PROIBIDOS):
            continue
        if arquivo == "README.md" or arquivo.startswith(PASTAS):
            entram.append(arquivo)
        elif arquivo.startswith("dados/brutos/") and (arquivo.endswith("/manifesto.csv") or arquivo in usados
                                                       or any(re.search(b, arquivo) for b in BRUTOS_LINKADOS)):
            if not arquivo.endswith("/manifesto.csv") and (RAIZ / arquivo).stat().st_size > LIMITE_BRUTO_SITE:
                grandes.append(arquivo)  # fica no repositório; o site linka a origem (exportar_site.py)
                continue
            entram.append(arquivo)

    pacote = subprocess.run(["git", "archive", "--format=tar", "HEAD", "--", *entram],
                            capture_output=True, cwd=RAIZ, check=True).stdout
    # limpa a pasta, mas guarda o vínculo com o projeto do Vercel (.vercel/)
    if DESTINO.exists():
        for item in DESTINO.iterdir():
            if item.name != ".vercel":
                shutil.rmtree(item) if item.is_dir() else item.unlink()
    DESTINO.mkdir(parents=True, exist_ok=True)
    with tarfile.open(fileobj=io.BytesIO(pacote)) as tar:
        tar.extractall(DESTINO, filter="data")
    paginas = achatar_site(DESTINO)
    (DESTINO / "vercel.json").write_text(json.dumps(vercel(paginas), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    indices(DESTINO)

    arquivos = [p for p in DESTINO.rglob("*") if p.is_file() and ".vercel" not in p.parts]
    total = sum(p.stat().st_size for p in arquivos) / 1048576
    vazou = [str(p.relative_to(DESTINO)) for p in arquivos
             if any(re.search(r, str(p.relative_to(DESTINO))) for r in PROIBIDOS)]
    if vazou:
        raise SystemExit(f"Arquivo proibido na publicação: {vazou}")
    if total > LIMITE_MB:
        raise SystemExit(f"{total:.1f} MB: acima do limite de {LIMITE_MB} MB do envio gratuito do Vercel")
    print(f"{DESTINO.relative_to(RAIZ)}: {len(arquivos)} arquivos, {total:.1f} MB (commit {git('rev-parse', '--short', 'HEAD').strip()}); "
          f"{len(grandes)} brutos acima de 2 MB ficaram só no repositório")


if __name__ == "__main__":
    main()
