"""
Ótica B — os modelos da virada (Lula favorito). Autor: Saulo Dutra.

Mesmo motor, mesmas convenções de modelos.py:
- spread = Flávio − Lula em pontos percentuais sobre votos VÁLIDOS entre os dois;
- erro de pesquisa = spread_poll − spread_urna (direita − esquerda); NEGATIVO = subestimou a direita.

O que muda entre a Ótica A e a Ótica B não é o código: são cinco premissas, declaradas uma a uma
em `PREMISSAS` e testadas uma de cada vez (gráfico tornado). Rode depois de modelos.py.
"""
from __future__ import annotations

import json
import math
from dataclasses import dataclass, field, replace

import numpy as np
import pandas as pd
from scipy import optimize, stats

from modelos import (
    DADOS, DATA_2T, DIAS_RESTANTES, HOJE, N_SIM, PESQUISA, SAIDA, SITE_DATA,
    bayes_sequencial, carregar_2t, historico_1t_2t, house_effects, ler_csv, limpo, num, transferencia, vies_historico,
)

LULA = PESQUISA / "lula"
RNG = np.random.default_rng(20261025 + 1)
M1_2026 = 100 * (47.03 - 45.16) / (47.03 + 45.16)  # margem entre os dois no 1T (pp)
PP_VOTOS = 119_300_788 / 100  # votos válidos por ponto percentual (TSE, 1T)
REGIAO = {"N": "AC AM AP PA RO RR TO", "NE": "AL BA CE MA PB PE PI RN SE", "CO": "DF GO MT MS", "SE": "ES MG RJ SP", "S": "PR RS SC"}
REGIAO_UF = {uf: r for r, ufs in REGIAO.items() for uf in ufs.split()}


def csv_lula(nome: str) -> pd.DataFrame | None:
    p = LULA / nome
    if not p.exists():
        return None
    try:
        return pd.read_csv(p)
    except Exception:  # noqa: BLE001
        try:  # linhas com vírgula sem aspas (URLs) são puladas, não derrubam a frente inteira
            df = pd.read_csv(p, on_bad_lines="skip", engine="python")
            print(f"[aviso] {nome}: linhas malformadas ignoradas; lidas {len(df)}")
            return df
        except Exception as e:  # noqa: BLE001
            print(f"[aviso] não consegui ler {p}: {e}")
            return None


def sim_nao(x) -> bool | None:
    s = str(x).strip().lower()
    if s in ("", "nan", "n/d", "nd", "none", "<na>"):
        return None
    if s in ("s", "1") or s.startswith(("sim", "true", "yes", "virou", "virada")):
        return True
    if s in ("n", "0") or s.startswith(("não", "nao", "false", "no")):
        return False
    return None


# ----------------------------------------------------------------------------
# 1. Viés das pesquisas SÓ nos 2ºs turnos (prior da Ótica B)
# ----------------------------------------------------------------------------
def vies_2t(vb: dict) -> dict:
    """
    Mesmos registros canônicos de modelos.vies_historico (erro = direita − esquerda, negativo =
    subestimou a direita), mas só as linhas de 2º turno, um ponto por eleição, pesos iguais.
    A Ótica A mistura 1º e 2º turnos (o 1T de 2026 com peso 0,75) e chega a −1,0; a Ótica B
    argumenta que o erro de 1º turno é alocação tardia de indecisos e voto útil (Cox 1997), que
    não existe num quadro de dois nomes. Variante "grandes casas" (Datafolha, Ibope/Ipec, Quaest)
    mostrada como contraponto.
    """
    reg = [r for r in vb.get("registros", []) if str(r.get("turno", "")).upper().startswith("2")]
    por_ano: dict[int, list[float]] = {}
    por_ano_grandes: dict[int, list[float]] = {}
    for r in reg:
        por_ano.setdefault(r["ano"], []).append(r["erro_spread"])
        if any(g in str(r.get("instituto", "")).lower() for g in ("datafolha", "ibope", "ipec", "quaest")):
            por_ano_grandes.setdefault(r["ano"], []).append(r["erro_spread"])

    def resumo(d: dict[int, list[float]]) -> dict:
        if not d:
            return {"media": 0.0, "desvio": 2.5, "n_eleicoes": 0, "por_eleicao": {}}
        pts = np.array([np.mean(v) for _, v in sorted(d.items())])
        sd = float(np.std(pts, ddof=1)) if len(pts) > 1 else 2.5
        return {"media": round(float(pts.mean()), 2), "desvio": round(max(sd, 1.5), 2), "n_eleicoes": int(len(pts)),
                "por_eleicao": {str(k): round(float(np.mean(v)), 2) for k, v in sorted(d.items())}}

    todos, grandes = resumo(por_ano), resumo(por_ano_grandes)
    return {"prior_vies": {**todos, "regra": "média simples dos erros médios por 2º turno (2010–2022), todos os institutos; sd amostral com piso 1,5 pp"},
            "variante_grandes_casas": grandes,
            "registros": reg, "n_registros": len(reg),
            "leitura": "positivo = pesquisas subestimaram a ESQUERDA nos 2ºs turnos"}


# ----------------------------------------------------------------------------
# 2. Classe de referência: viradas em 2ºs turnos (governadores, capitais, exterior)
# ----------------------------------------------------------------------------
def _logistica(x: np.ndarray, y: np.ndarray) -> tuple[float, float] | None:
    """MLE de P(y=1) = 1/(1+exp(-(a+b·x))) com penalização L2 leve (evita separação perfeita)."""
    if len(x) < 8 or y.sum() == 0 or y.sum() == len(y):
        return None

    def nll(th: np.ndarray) -> float:
        z = th[0] + th[1] * x
        return float(np.sum(np.logaddexp(0, -z) * y + np.logaddexp(0, z) * (1 - y)) + 0.01 * (th[1] ** 2))

    res = optimize.minimize(nll, np.array([0.0, -0.1]), method="BFGS")
    return (float(res.x[0]), float(res.x[1])) if res.success or np.isfinite(res.fun) else None


def _base_viradas(df: pd.DataFrame, nome: str) -> dict | None:
    if df is None or df.empty:
        return None
    d = df.copy()
    for c in ("pct_lider_1t", "pct_segundo_1t", "pct_vencedor_2t"):
        if c in d.columns:
            d[c] = d[c].map(num)
    if "virou" not in d.columns or "pct_lider_1t" not in d.columns:
        return None
    if "incluir_na_estatistica" in d.columns:  # base internacional marca casos sem votação (desistência) para excluir
        inc = d["incluir_na_estatistica"].map(sim_nao)
        d = d[inc.fillna(True)]
    d["virou_b"] = d["virou"].map(sim_nao)
    d = d.dropna(subset=["pct_lider_1t", "pct_segundo_1t", "virou_b"])
    d = d[(d.pct_lider_1t > 0) & (d.pct_segundo_1t > 0)]
    if d.empty:
        return None
    # margem entre os dois (mesma régua de m1 em modelos.py)
    d["m1"] = 100 * (d.pct_lider_1t - d.pct_segundo_1t) / (d.pct_lider_1t + d.pct_segundo_1t)
    d["virou_b"] = d["virou_b"].astype(bool)
    if "pct_vencedor_2t" in d.columns:
        m2_abs = 2 * d.pct_vencedor_2t - 100
        d["m2"] = np.where(d.virou_b, -m2_abs, m2_abs)
    faixas = [(0, 3), (3, 6), (6, 10), (10, 100)]
    tab_faixas = []
    for lo, hi in faixas:
        sub = d[(d.m1 >= lo) & (d.m1 < hi)]
        n, k = int(len(sub)), int(sub.virou_b.sum())
        tab_faixas.append({"faixa": f"{lo}–{hi if hi < 100 else '∞'} pp", "n": n, "viradas": k,
                           "taxa": round(k / n, 3) if n else None, "laplace": round((k + 1) / (n + 2), 3)})
    x, y = d.m1.values.astype(float), d.virou_b.values.astype(float)
    lg = _logistica(x, y)
    p_2026 = None
    if lg:
        p_2026 = float(1 / (1 + math.exp(-(lg[0] + lg[1] * M1_2026))))
    sub3 = d[d.m1 < 3]
    out = {
        "base": nome, "n": int(len(d)), "viradas": int(d.virou_b.sum()), "taxa_geral": round(float(d.virou_b.mean()), 3),
        "faixas": tab_faixas,
        "logistica": {"a": round(lg[0], 4), "b": round(lg[1], 4)} if lg else None,
        "p_virada_m1_2026": round(p_2026, 3) if p_2026 is not None else None,
        "laplace_m1_menor_3": round((int(sub3.virou_b.sum()) + 1) / (len(sub3) + 2), 3) if len(sub3) else None,
        "n_m1_menor_3": int(len(sub3)), "viradas_m1_menor_3": int(sub3.virou_b.sum()),
        "curva": [{"m1": float(m), "p": round(float(1 / (1 + math.exp(-(lg[0] + lg[1] * m)))), 4)} for m in np.arange(0, 20.5, 0.5)] if lg else [],
    }
    # subconjunto análogo a 2026: líder do 1T NÃO era o incumbente (Flávio desafia o presidente)
    if "lider_incumbente" in d.columns:
        # a frente gravou "incumbente" / "situacao" / "oposicao" / sim / nao; incumbente = o próprio titular concorrendo
        d["li"] = d["lider_incumbente"].fillna("").astype(str).str.lower().map(lambda s: True if any(t in str(s) for t in ("incumb", "titular", "sim")) else (False if str(s).strip() not in ("", "nan", "n/d", "<na>") else None))
        sub = d[d.li == False]  # noqa: E712
        if len(sub):
            s3 = sub[sub.m1 < 5]
            out["lider_nao_incumbente"] = {"n": int(len(sub)), "viradas": int(sub.virou_b.sum()), "taxa": round(float(sub.virou_b.mean()), 3),
                                           "n_m1_menor_5": int(len(s3)), "viradas_m1_menor_5": int(s3.virou_b.sum()),
                                           "laplace_m1_menor_5": round((int(s3.virou_b.sum()) + 1) / (len(s3) + 2), 3) if len(s3) else None}
        sub = d[d.li == True]  # noqa: E712
        if len(sub):
            out["lider_incumbente"] = {"n": int(len(sub)), "viradas": int(sub.virou_b.sum()), "taxa": round(float(sub.virou_b.mean()), 3)}
    # regressão estrutural m2 ~ m1 (substitui o n = 7 presidencial)
    if "m2" in d.columns and len(d) >= 8:
        reg = stats.linregress(d.m1.values, d.m2.values)
        pred = reg.intercept + reg.slope * M1_2026
        resid_sd = float(np.sqrt(np.sum((d.m2.values - (reg.intercept + reg.slope * d.m1.values)) ** 2) / max(len(d) - 2, 1)))
        out["estrutural"] = {"slope": round(float(reg.slope), 3), "intercept": round(float(reg.intercept), 3), "r2": round(float(reg.rvalue ** 2), 3),
                             "resid_sd": round(resid_sd, 2), "n": int(len(d)), "pred_m2_2026": round(float(pred), 2),
                             "p_lula": round(float(stats.norm.cdf(0, pred, resid_sd)), 4)}
        out["pontos"] = [{"m1": round(float(a), 2), "m2": round(float(b), 2), "virou": bool(v), "rotulo": str(r)} for a, b, v, r in
                         zip(d.m1.values, d.m2.values, d.virou_b.values, (d["uf"] if "uf" in d.columns else d.get("pais", d.get("cidade", pd.Series([""] * len(d))))).astype(str) + " " + d["ano"].astype(str))]
    cols = [c for c in ("ano", "uf", "cidade", "pais", "lider_1t", "pct_lider_1t", "segundo_1t", "pct_segundo_1t", "vencedor_2t", "pct_vencedor_2t", "lider_incumbente", "lider_era_governo", "contexto", "fonte") if c in d.columns]
    out["tabela"] = json.loads(d.assign(m1=d.m1.round(2), virou=d.virou_b)[cols + ["m1", "virou"]].sort_values(["ano"] + ([c for c in ("uf", "cidade", "pais") if c in d.columns][:1])).to_json(orient="records", force_ascii=False))
    return out


def viradas() -> dict:
    bases = {}
    for nome, arq in (("governadores", "viradas_governador.csv"), ("capitais", "viradas_capitais.csv"), ("internacional", "viradas_internacional.csv")):
        b = _base_viradas(csv_lula(arq), nome)
        if b:
            bases[nome] = b
    # base brasileira agregada (governadores + capitais)
    frames = [csv_lula(a) for a in ("viradas_governador.csv", "viradas_capitais.csv")]
    frames = [f for f in frames if f is not None and not f.empty]
    if len(frames) == 2:
        b = _base_viradas(pd.concat(frames, ignore_index=True), "brasil_subnacional")
        if b:
            bases["brasil_subnacional"] = b
    # presidencial (7/7) para comparação na mesma régua
    pres = historico_1t_2t()
    bases["presidencial"] = {"base": "presidencial", "n": pres["regressao"]["n"], "viradas": 0, "taxa_geral": 0.0,
                             "laplace": pres["laplace_p_lider"], "nota": "7/7 líderes venceram; nenhum com margem < 5,2 pp"}
    escolhida = bases.get("governadores") or bases.get("brasil_subnacional")
    p_D = None
    if escolhida:
        p_D = escolhida.get("p_virada_m1_2026") or escolhida.get("laplace_m1_menor_3")
    return {"bases": bases, "p_virada_2026": round(p_D, 4) if p_D is not None else None,
            "base_usada": escolhida["base"] if escolhida else None,
            "nota": "P(virada) para 2026 = logística P(virou | margem do 1T entre os dois) avaliada em m1 = 2,03 pp na base de governadores (fallback: Laplace na faixa < 3 pp)."}


# ----------------------------------------------------------------------------
# 3. Transferência pela leitura favorável a Lula + prêmio do incumbente
# ----------------------------------------------------------------------------
CHAVES = {"cury": ("cury",), "santos": ("renan", "santos", "missão", "missao"), "caiado": ("caiado",), "zema": ("zema",), "samara": ("samara", "up")}
FALLBACK_B = {  # (Flávio, Lula, branco/nulo/abstém) — a leitura publicada mais favorável a Lula por grupo, out/2026
    "cury": (0.34, 0.23, 0.43), "santos": (0.54, 0.28, 0.18), "caiado": (0.46, 0.30, 0.24), "zema": (0.48, 0.25, 0.27),
    "samara": (0.05, 0.70, 0.25), "outros": (0.10, 0.60, 0.30),
}
FALLBACK_FONTE = {"cury": "Quaest 02-03/10 (34/23/41)", "santos": "Datafolha 29/09-01/10 (54/28/16)", "caiado": "Datafolha 29/09-01/10 (46/30/22)",
                  "zema": "Datafolha set/26 (48/25/27)", "samara": "estimativa do autor", "outros": "estimativa do autor"}


def leituras_segunda_opcao() -> dict:
    """Lê pesquisa/lula/segunda_opcao_2t.csv e escolhe, por eleitorado, a leitura publicada (campo >= 29/09) mais favorável a Lula."""
    df = csv_lula("segunda_opcao_2t.csv")
    esc = {k: {"premissa": v, "fonte": FALLBACK_FONTE[k], "origem": "fallback"} for k, v in FALLBACK_B.items()}
    todas = []
    if df is None or df.empty:
        return {"escolhidas": esc, "leituras": todas}
    for _, r in df.iterrows():
        nome = str(r.get("eleitorado_1t", "")).lower()
        chave = next((k for k, als in CHAVES.items() if any(a in nome for a in als)), None)
        lu, fl = num(r.get("lula")), num(r.get("flavio"))
        if chave is None or math.isnan(lu) or math.isnan(fl):
            continue
        bn = num(r.get("branco_nulo"))
        ns = num(r.get("nao_sabe"))
        resto = max(0.0, 100 - lu - fl) if math.isnan(bn) else bn + (0 if math.isnan(ns) else ns)
        fim = str(r.get("fim", ""))[:10]
        item = {"eleitorado": chave, "instituto": str(r.get("instituto", "")), "fim": fim, "lula": lu, "flavio": fl, "resto": round(resto, 1),
                "n_sub": num(r.get("n_sub")), "fonte": str(r.get("fonte", "")), "saldo_lula": round(lu - fl, 1)}
        todas.append(item)
    for k in CHAVES:
        cand = [t for t in todas if t["eleitorado"] == k and t["fim"] >= "2026-09-29"]
        if cand:
            melhor = max(cand, key=lambda t: t["saldo_lula"])
            tot = melhor["lula"] + melhor["flavio"] + melhor["resto"]
            esc[k] = {"premissa": (melhor["flavio"] / tot, melhor["lula"] / tot, melhor["resto"] / tot),
                      "fonte": f"{melhor['instituto']} {melhor['fim']} ({melhor['flavio']:.0f}/{melhor['lula']:.0f}/{melhor['resto']:.0f})", "origem": "medido"}
    return {"escolhidas": esc, "leituras": sorted(todas, key=lambda t: (t["eleitorado"], t["fim"]))}


def pool_medido_pre1t() -> dict[str, float]:
    """Terceira via medida em VÁLIDOS nas últimas pesquisas de 1T (campo até 03/10) de Datafolha e Quaest, média por candidato."""
    df = ler_csv("pesquisas_1t.csv")
    out: dict[str, list[float]] = {}
    if df is None:
        return {}
    for _, r in df.iterrows():
        if str(r.get("tipo", "")).strip().lower() != "validos" or str(r.get("fim", ""))[:10] < "2026-10-02":
            continue
        if not any(i in str(r.get("instituto", "")).lower() for i in ("datafolha", "quaest")):
            continue
        for k in ("cury", "santos", "caiado", "zema", "samara"):
            x = num(r.get(k))
            if not math.isnan(x):
                out.setdefault(k, []).append(x)
    return {k: round(float(np.mean(v)), 2) for k, v in out.items()}


def ajuste_selecao(premissas: dict[str, tuple[float, float, float]], rho: float | None, medido: dict[str, float]) -> dict[str, tuple[float, float, float]]:
    """
    O pool que SOBROU na urna (7,68 pp) é o que resistiu ao voto útil pró-Flávio das últimas 48 h: as pesquisas
    de 03/10 mediam ~10,5 pp de terceira via e Lula foi medido certo. Se uma fração rho dos que saíram eram
    eleitores de "segunda opção Flávio", a parcela de Flávio no pool restante é menor que a medida.
    rho = f_medido (proporcional) reproduz a premissa original (Ótica A); rho = 1 é o extremo. Em pp:
    F = f·Pm − rho·Δ ; L e abstenção dividem o resto dos que saíram na proporção medida. Soma = Pa.
    """
    if rho is None:
        return premissas
    r = json.loads((DADOS / "resultado_1t_2026.json").read_text(encoding="utf-8"))["validos"]
    out = {}
    for k, (f, lu, a) in premissas.items():
        pm, pa = medido.get(k), r[k]["pct"]
        # só eleitorados medidos com pelo menos 2 pp: abaixo disso o "1%" das pesquisas é arredondamento
        if pm is None or pm < 2.0 or pm <= pa or pa <= 0:
            out[k] = (f, lu, a)
            continue
        delta = pm - pa
        rr = rho if rho >= 0 else f
        f_pp = max(f * pm - rr * delta, 0.0)
        resto = (1 - rr) * delta
        l_pp = max(lu * pm - resto * (lu / (lu + a) if lu + a > 0 else 0.5), 0.0)
        a_pp = max(pa - f_pp - l_pp, 0.0)
        s = f_pp + l_pp + a_pp
        out[k] = (f_pp / s, l_pp / s, a_pp / s)
    return out


def transferencia_param(premissas: dict[str, tuple[float, float, float]], mob_media: float, n_sim: int = N_SIM, conc: float = 40.0) -> dict:
    """Mesmo motor de modelos.transferencia, com premissas e choque de mobilização parametrizados (mob < 0 favorece Lula)."""
    r = json.loads((DADOS / "resultado_1t_2026.json").read_text(encoding="utf-8"))
    v = r["validos"]
    base_f, base_l = v["flavio"]["pct"], v["lula"]["pct"]
    sims_f = base_f * RNG.beta(0.965 * 60, 0.035 * 60, n_sim)
    sims_l = base_l * RNG.beta(0.965 * 60, 0.035 * 60, n_sim)
    choque = RNG.normal(mob_media, 1.2, n_sim)
    sims_f += choque / 2
    sims_l -= choque / 2
    detalhe = {}
    for cand, (pf, pl, pa) in premissas.items():
        pct = v[cand]["pct"]
        draws = RNG.dirichlet(np.array([pf, pl, pa]) * conc, n_sim)
        sims_f += pct * draws[:, 0]
        sims_l += pct * draws[:, 1]
        detalhe[cand] = {"pct_1t": pct, "para_flavio": round(pf, 3), "para_lula": round(pl, 3), "abstem": round(pa, 3),
                         "esperado_flavio_pp": round(pct * pf, 2), "esperado_lula_pp": round(pct * pl, 2)}
    share = 100 * sims_f / (sims_f + sims_l)
    spread = 2 * share - 100
    return {"premissas": detalhe, "mob_media": mob_media, "spread_sims": spread,
            "spread": {"media": round(float(spread.mean()), 2), "p05": round(float(np.percentile(spread, 5)), 2), "p95": round(float(np.percentile(spread, 95)), 2)},
            "p_lula": round(float(np.mean(spread < 0)), 4), "pool_transferivel_pp": round(sum(v[c]["pct"] for c in premissas), 2)}


def residual_incumbente() -> dict:
    """
    Calibração do "prêmio do incumbente": nas duas eleições com presidente candidato e "segunda opção" publicada
    (2014: Dilma; 2022: Bolsonaro), compara a parcela do pool da terceira via que a segunda opção do INÍCIO do 2º turno
    previa para o incumbente (entre os decididos) com a parcela que ele de fato levou (ganho 1T→2T / pool, em válidos).
    Resíduo em fração do pool; aplicado ao pool de 2026 (7,81 pp) vira o deslocamento de spread da Ótica B.
    Fontes: pesquisa/lula/precedente_segunda_opcao_2014_2022.csv e crescimento_1t_2t.csv (frente 5).
    """
    prec, cres = csv_lula("precedente_segunda_opcao_2014_2022.csv"), csv_lula("crescimento_1t_2t.csv")
    if prec is None or cres is None:
        return {"casos": [], "residual_medio_pool": None}
    lado_inc = {2014: "esquerda", 2022: "direita"}
    casos = []
    for ano, lado in lado_inc.items():
        c = cres[cres["ano"].map(num) == ano]
        pool_row = c[c["candidato"].astype(str).str.contains("demais candidatos", case=False)]
        lider = c[c["candidato"].astype(str).str.contains("lider do 1T", case=False)]
        seg = c[c["candidato"].astype(str).str.contains("2o do 1T", case=False)]
        if pool_row.empty or lider.empty or seg.empty:
            continue
        pool = num(pool_row.iloc[0]["pct_1t"])
        inc_e_lider = (ano == 2014)  # Dilma liderou o 1T de 2014; Bolsonaro foi 2º em 2022
        ganho_inc = num((lider if inc_e_lider else seg).iloc[0]["ganho_pp"])
        p = prec[(prec["ano"].map(num) == ano) & prec["instituto"].astype(str).str.lower().str.startswith("datafolha")].copy()
        p = p[~p["instituto"].astype(str).str.contains("validos", case=False)]
        p["fim"] = p["fim"].astype(str)
        primeira = p[p["fim"] == p["fim"].min()]  # medição mais antiga do 2T (análoga ao momento atual de 2026)
        num_, den_ = 0.0, 0.0
        detalhe = []
        for _, r in primeira.iterrows():
            e, d = num(r["pct_esquerda"]), num(r["pct_direita"])
            if math.isnan(e) or math.isnan(d) or e + d <= 0:
                continue
            cand = str(r["eleitorado_1t"])
            cc = c[c["candidato"].astype(str).str.contains(cand.split()[0], case=False)]
            w = num(cc.iloc[0]["pct_1t"]) if not cc.empty else float("nan")
            if math.isnan(w):
                continue
            share_inc = (e if lado == "esquerda" else d) / (e + d)
            num_ += w * share_inc
            den_ += w
            detalhe.append({"eleitorado": cand, "pct_1t": w, "segunda_opcao_inc": round(100 * share_inc, 1), "campo_fim": r["fim"]})
        if den_ == 0:
            continue
        previsto = num_ / den_  # parcela do pool (decididos) prevista para o incumbente; resto do pool assume a mesma média
        real = ganho_inc / pool
        casos.append({"ano": ano, "incumbente": "Dilma Rousseff" if ano == 2014 else "Jair Bolsonaro", "pool_pp": pool, "ganho_incumbente_pp": ganho_inc,
                      "parcela_prevista_pct": round(100 * previsto, 1), "parcela_real_pct": round(100 * real, 1), "residual_pool_pct": round(100 * (real - previsto), 1),
                      "medicoes": detalhe})
    if not casos:
        return {"casos": [], "residual_medio_pool": None}
    res = float(np.mean([k["residual_pool_pct"] for k in casos])) / 100
    pool_2026 = 7.81
    return {"casos": casos, "residual_medio_pool": round(res, 3), "pool_2026_pp": pool_2026,
            "deslocamento_spread_2026_pp": round(-2 * res * pool_2026, 2),
            "nota": "resíduo = parcela real do pool que o incumbente levou − parcela prevista pela segunda opção do início do 2T; "
                    "aplicado ao pool de 2026 e convertido em spread (×2). Negativo = a favor de Lula, o incumbente de 2026."}


def crescimento_incumbentes() -> dict:
    """1T→2T nos três 2ºs turnos com presidente candidato (válidos, TSE via historico_2t.csv)."""
    df = ler_csv("historico_2t.csv")
    casos = {2006: "Lula (PT)", 2014: "Dilma Rousseff (PT)", 2022: "Jair Bolsonaro (PL)"}
    out = []
    if df is None:
        return {"casos": out}
    for _, r in df.iterrows():
        ano = int(num(r.get("ano")))
        if ano not in casos:
            continue
        inc = casos[ano]
        lider, seg = str(r.lider_1t), str(r.segundo_1t)
        pl1, ps1 = num(r.pct_lider_1t), num(r.pct_segundo_1t)
        pv2, pp2 = num(r.pct_vencedor_2t), num(r.pct_perdedor_2t)
        venc = str(r.vencedor_2t)
        inc_e_lider = inc.split(" (")[0].lower() in lider.lower()
        inc_1t, des_1t = (pl1, ps1) if inc_e_lider else (ps1, pl1)
        inc_venceu = inc.split(" (")[0].lower() in venc.lower()
        inc_2t, des_2t = (pv2, pp2) if inc_venceu else (pp2, pv2)
        out.append({"ano": ano, "incumbente": inc, "desafiante": seg if inc_e_lider else lider, "inc_1t": inc_1t, "inc_2t": inc_2t,
                    "des_1t": des_1t, "des_2t": des_2t, "ganho_inc": round(inc_2t - inc_1t, 2), "ganho_des": round(des_2t - des_1t, 2),
                    "diferencial_incumbente": round((inc_2t - inc_1t) - (des_2t - des_1t), 2), "incumbente_venceu": inc_venceu})
    med = float(np.mean([c["diferencial_incumbente"] for c in out])) if out else 0.0
    return {"casos": out, "diferencial_medio_pp": round(med, 2),
            "nota": "diferencial = (ganho do incumbente 1T→2T) − (ganho do desafiante), em pp de válidos; inclui o efeito da terceira via, por isso a Ótica B usa só uma fração dele (0,8 pp)."}


# ----------------------------------------------------------------------------
# 4. O motor comum: premissas → Monte Carlo
# ----------------------------------------------------------------------------
@dataclass
class Premissas:
    nome: str
    vies_media: float
    vies_sd: float
    transf: dict[str, tuple[float, float, float]]
    mob_media: float
    estrutural: str  # "presidencial" | "governadores"
    pesos: dict[str, float] = field(default_factory=dict)
    rotulos: dict[str, str] = field(default_factory=dict)
    rho_selecao: float | None = None  # None = sem ajuste; 0.75 = 3/4 dos que saíram do pool eram "segunda opção Flávio"


MEDIDO_PRE1T: dict[str, float] = {}


def motor(p: Premissas, he: dict, hist_pres: dict, hist_gov: dict | None, p_virada: float | None, cache: dict) -> dict:
    n = N_SIM
    dias = max(DIAS_RESTANTES, 1)
    sd_drift = 0.45 * math.sqrt(dias)
    comp = {}
    mu, se = he.get("spread_hoje", float("nan")), he.get("se_hoje") or 2.0
    if not math.isnan(mu):
        comp["A_pesquisas_vies"] = RNG.normal(mu, se, n) - RNG.normal(p.vies_media, p.vies_sd, n) + RNG.normal(0, sd_drift, n)
    transf = ajuste_selecao(p.transf, p.rho_selecao, MEDIDO_PRE1T)
    chave_tr = (tuple(sorted((k, tuple(round(x, 4) for x in v)) for k, v in transf.items())), round(p.mob_media, 3))
    if chave_tr not in cache:
        cache[chave_tr] = transferencia_param(transf, p.mob_media)
    tr = cache[chave_tr]
    comp["B_transferencia"] = tr["spread_sims"] + RNG.normal(0, sd_drift * 0.6, n)
    if p.estrutural == "governadores" and hist_gov and hist_gov.get("estrutural"):
        e = hist_gov["estrutural"]
        comp["C_estrutural"] = RNG.normal(e["pred_m2_2026"], e["resid_sd"], n)
    else:
        comp["C_estrutural"] = RNG.normal(hist_pres["pred_m2_2026"], hist_pres["regressao"]["resid_sd"], n)
    if p.pesos.get("D_classe_referencia", 0) > 0 and p_virada is not None:
        sd_d = (hist_gov or {}).get("estrutural", {}).get("resid_sd", 8.0) if hist_gov else 8.0
        mu_d = -sd_d * stats.norm.ppf(p_virada)  # P(spread < 0) = p_virada
        comp["D_classe_referencia"] = RNG.normal(mu_d, sd_d, n)
    pesos = {k: v for k, v in p.pesos.items() if k in comp and v > 0}
    s = sum(pesos.values())
    pesos = {k: v / s for k, v in pesos.items()}
    idx = RNG.choice(list(pesos), size=n, p=list(pesos.values()))
    comb = np.select([idx == k for k in pesos], [comp[k] for k in pesos])
    comp["Combinado"] = comb

    def resumo(x: np.ndarray) -> dict:
        return {"p_lula": round(float(np.mean(x < 0)), 4), "p_flavio": round(float(np.mean(x > 0)), 4), "media": round(float(x.mean()), 2),
                "p05": round(float(np.percentile(x, 5)), 2), "p50": round(float(np.percentile(x, 50)), 2), "p95": round(float(np.percentile(x, 95)), 2), "sd": round(float(x.std()), 2)}

    bins = np.arange(-20, 20.5, 0.5)
    h, _ = np.histogram(comb, bins=bins, density=True)
    return {"nome": p.nome, "modelos": {k: resumo(v) for k, v in comp.items()}, "pesos": pesos, "transferencia": {k: v for k, v in tr.items() if k != "spread_sims"},
            "hist": {"bins": [round(float(b), 2) for b in bins[:-1]], "dens": [round(float(x), 5) for x in h]},
            "premissas": {"vies_media": p.vies_media, "vies_sd": p.vies_sd, "mob_media": p.mob_media, "estrutural": p.estrutural, "rho_selecao": p.rho_selecao, "rotulos": p.rotulos},
            "sd_drift": round(sd_drift, 2), "dias_restantes": dias, "n_sim": n}


# ----------------------------------------------------------------------------
# 5. Break-even, sensibilidades e geografia
# ----------------------------------------------------------------------------
def break_even(trB: dict) -> dict:
    """Quanto do pool precisa mudar de lado para zerar o spread, nas premissas B (determinístico, na média)."""
    spread = trB["spread"]["media"]
    pool = trB["pool_transferivel_pp"]
    pp_necessarios = max(spread, 0) / 2  # cada 1 pp que troca de Flávio p/ Lula move o spread em 2
    base = {"spread_medio_B": spread, "pool_pp": pool, "pp_que_precisam_trocar_de_lado": round(pp_necessarios, 2),
            "votos_que_precisam_trocar": int(round(pp_necessarios * PP_VOTOS)), "fracao_do_pool": round(pp_necessarios / pool, 3) if pool else None}
    # parcela de Lula no pool que entrega 50%: com abstenção do pool fixa em cada premissa, resolve s tal que spread = 0
    r = json.loads((DADOS / "resultado_1t_2026.json").read_text(encoding="utf-8"))["validos"]
    f1, l1 = r["flavio"]["pct"] * 0.965, r["lula"]["pct"] * 0.965
    abst = sum(r[c]["pct"] * v["abstem"] for c, v in trB["premissas"].items())
    util = pool - abst  # pp do pool que de fato votam em um dos dois
    s_lula = (f1 - l1 + util) / (2 * util) if util > 0 else None  # Lula precisa de s_lula do pool útil
    base["parcela_lula_no_pool_util_para_empatar"] = round(s_lula, 3) if s_lula else None
    base["pool_util_pp"] = round(util, 2)
    base["parcela_lula_no_pool_util_premissa_B"] = round(sum(r[c]["pct"] * v["para_lula"] for c, v in trB["premissas"].items()) / util, 3) if util > 0 else None
    return base


def sensibilidades(pB: Premissas, he: dict, hist_pres: dict, hist_gov: dict | None, p_virada: float | None, cache: dict) -> dict:
    vies = []
    for m in (-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5):
        r = motor(replace(pB, nome=f"vies={m}", vies_media=m), he, hist_pres, hist_gov, p_virada, cache)
        vies.append({"vies_media": m, "p_lula": r["modelos"]["Combinado"]["p_lula"], "p_lula_A": r["modelos"]["A_pesquisas_vies"]["p_lula"]})
    mob = []
    for m in (-4.0, -3.0, -2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0):
        r = motor(replace(pB, nome=f"mob={m}", mob_media=m), he, hist_pres, hist_gov, p_virada, cache)
        mob.append({"mob_media": m, "p_lula": r["modelos"]["Combinado"]["p_lula"], "p_lula_B": r["modelos"]["B_transferencia"]["p_lula"]})
    sel = []
    for rho in (None, 0.5, 0.75, 0.9, 1.0):
        r = motor(replace(pB, nome=f"rho={rho}", rho_selecao=rho), he, hist_pres, hist_gov, p_virada, cache)
        sel.append({"rho": rho, "p_lula": r["modelos"]["Combinado"]["p_lula"], "p_lula_B": r["modelos"]["B_transferencia"]["p_lula"], "spread_B": r["transferencia"]["spread"]["media"]})
    return {"vies": vies, "mobilizacao": mob, "selecao": sel,
            "nota": "mob < 0 = comparecimento líquido favorável a Lula (pp de válidos); viés < 0 = pesquisas subestimam a direita; rho = fração dos que saíram do pool da terceira via que eram segunda opção Flávio"}


def geografia() -> dict:
    a = ler_csv("resultado_1t_2026_uf.csv")
    if a is None:
        return {}
    a = a.copy()
    a["uf"] = a["uf"].astype(str).str.upper().str.strip()
    for c in ("eleitores", "flavio", "lula", "abstencao"):
        a[c] = a[c].map(num)
    a["regiao"] = a["uf"].map(REGIAO_UF)
    reg = []
    for r, g in a.groupby("regiao"):
        el = float(g.eleitores.sum())
        # comparecimento efetivo e válidos: usa a abstenção da UF e desconta brancos/nulos nacionais (4,77%)
        comp = float((g.eleitores * (1 - g.abstencao / 100)).sum())
        w = (g.eleitores * (1 - g.abstencao / 100)).values
        lula_v = float(np.average(g.lula.values, weights=w))
        flavio_v = float(np.average(g.flavio.values, weights=w))
        abst = 100 * (1 - comp / el)
        # cada 1 pp a menos de abstenção na região = el/100 eleitores a mais, 95,2% válidos, divididos como o 1T
        ganho_lula_pp = el / 100 * 0.952 * (lula_v - flavio_v) / 100 / PP_VOTOS
        reg.append({"regiao": r, "eleitores": int(el), "abstencao_1t": round(abst, 2), "lula_validos": round(lula_v, 2), "flavio_validos": round(flavio_v, 2),
                    "spread_validos": round(flavio_v - lula_v, 2), "ganho_lula_por_pp_abstencao_pp_nacional": round(ganho_lula_pp, 3),
                    "ganho_lula_por_pp_abstencao_votos": int(round(el / 100 * 0.952 * (lula_v - flavio_v) / 100))})
    cen = []
    for d in (1, 2, 3):
        ne = next((x for x in reg if x["regiao"] == "NE"), None)
        se_ = next((x for x in reg if x["regiao"] == "SE"), None)
        s_ = next((x for x in reg if x["regiao"] == "S"), None)
        if ne:
            cen.append({"cenario": f"abstenção no NE cai {d} pp", "efeito_spread_pp": round(-2 * d * ne["ganho_lula_por_pp_abstencao_pp_nacional"], 2),
                        "votos_liquidos_lula": int(d * ne["ganho_lula_por_pp_abstencao_votos"])})
        if se_ and s_:
            g = d * (se_["ganho_lula_por_pp_abstencao_pp_nacional"] + s_["ganho_lula_por_pp_abstencao_pp_nacional"])
            cen.append({"cenario": f"abstenção no SE e no S cai {d} pp", "efeito_spread_pp": round(-2 * g, 2),
                        "votos_liquidos_lula": int(d * (se_["ganho_lula_por_pp_abstencao_votos"] + s_["ganho_lula_por_pp_abstencao_votos"]))})
    out = {"regioes": sorted(reg, key=lambda x: -x["eleitores"]), "cenarios_abstencao": cen,
           "nota": "ESTIMATIVA: votantes extras divididos como os válidos do 1T na região; 95,2% válidos (brancos+nulos 4,77% no 1T). Efeito no spread = 2 × ganho líquido em pp."}
    # 2022: quanto cada UF mudou do 1T para o 2T (se a frente trouxe o arquivo)
    b = csv_lula("resultado_2022_2t_uf.csv")
    if b is not None and not b.empty and {"lula_2t", "bolsonaro_2t", "lula_1t", "bolsonaro_1t"} <= set(b.columns):
        b = b.copy()
        b["uf"] = b["uf"].astype(str).str.upper().str.strip()
        for c in ("lula_2t", "bolsonaro_2t", "lula_1t", "bolsonaro_1t", "abstencao_2t", "abstencao_1t", "abstencao_2t_pct", "abstencao_1t_pct"):
            if c in b.columns:
                b[c] = b[c].map(num)
        if "abstencao_2t_pct" in b.columns:  # a frente gravou abstenção em votos e em %; a tabela usa %
            b["abstencao_2t"], b["abstencao_1t"] = b["abstencao_2t_pct"], b.get("abstencao_1t_pct", b["abstencao_1t"])
        b["spread_1t"] = 100 * (b.bolsonaro_1t - b.lula_1t) / (b.bolsonaro_1t + b.lula_1t)
        b["spread_2t"] = 100 * (b.bolsonaro_2t - b.lula_2t) / (b.bolsonaro_2t + b.lula_2t)
        b["delta"] = b.spread_2t - b.spread_1t
        b["regiao"] = b["uf"].map(REGIAO_UF)
        cols = [c for c in ("uf", "regiao", "spread_1t", "spread_2t", "delta", "abstencao_1t", "abstencao_2t") if c in b.columns]
        out["mudanca_2022_1t_2t"] = json.loads(b[cols].round(2).sort_values("delta").to_json(orient="records", force_ascii=False))
        out["mudanca_2022_por_regiao"] = {str(r): round(float(g.delta.mean()), 2) for r, g in b.groupby("regiao")}
    return out


def rejeicao_atual() -> dict:
    df = csv_lula("rejeicao_serie.csv")
    if df is None or df.empty:
        return {}
    d = df.copy()
    d["rejeicao"] = d["rejeicao"].map(num)
    d["fim"] = d["fim"].astype(str).str[:10]
    d["cand"] = d["candidato"].astype(str).str.lower().map(lambda s: "lula" if "lula" in s else ("flavio" if "fl" in s or "bolsonaro" in s else "outro"))
    seg = d["segmento"].astype(str).str.strip().str.lower() if "segmento" in d.columns else pd.Series([""] * len(d))
    geral = d[(seg.isin(("", "geral", "total", "nan", "todos"))) & d.cand.isin(("lula", "flavio"))].dropna(subset=["rejeicao"])
    ult = {}
    for (inst, cand), g in geral.groupby(["instituto", "cand"]):
        g = g.sort_values("fim")
        ult.setdefault(inst, {})[cand] = {"rejeicao": float(g.iloc[-1].rejeicao), "fim": g.iloc[-1].fim, "fonte": str(g.iloc[-1].get("fonte", ""))}
    tab = [{"instituto": i, "fim": max(v.get("lula", {}).get("fim", ""), v.get("flavio", {}).get("fim", "")), "lula": v.get("lula", {}).get("rejeicao"), "flavio": v.get("flavio", {}).get("rejeicao"),
            "fonte": v.get("lula", {}).get("fonte") or v.get("flavio", {}).get("fonte")} for i, v in ult.items() if "lula" in v and "flavio" in v]
    tab = sorted(tab, key=lambda x: x["fim"], reverse=True)
    serie = json.loads(geral[["instituto", "fim", "cand", "rejeicao"]].sort_values("fim").to_json(orient="records", force_ascii=False))
    media = {c: round(float(np.mean([t[c] for t in tab if t[c] is not None])), 1) for c in ("lula", "flavio") if any(t[c] is not None for t in tab)}
    return {"ultimas": tab, "serie": serie, "media_ultimas": media}


def incumbentes() -> dict:
    df = csv_lula("reeleicao_incumbentes.csv")
    if df is None or df.empty or "venceu" not in df.columns:
        return {}
    d = df.copy()
    # pleitos sem competição real (contestados, oposição presa, não reconhecidos) ficam fora da taxa
    suspeito = d["venceu"].fillna("").astype(str).str.lower().str.contains("contestad|não reconhecida|nao reconhecida|presos|sem oposi|oea")
    d = d[~suspeito]
    d["v"] = d["venceu"].map(sim_nao)
    d = d.dropna(subset=["v"])
    d["apr"] = d["aprovacao_pre"].map(num) if "aprovacao_pre" in d.columns else np.nan
    out = {"n": int(len(d)), "venceram": int(d.v.sum()), "taxa": round(float(d.v.mean()), 3) if len(d) else None,
           "tabela": json.loads(d.drop(columns=["v", "apr"]).to_json(orient="records", force_ascii=False))}
    com = d.dropna(subset=["apr"])
    if len(com) >= 6:
        lg = _logistica(com.apr.values.astype(float), com.v.values.astype(float))
        if lg:
            out["logistica_aprovacao"] = {"a": round(lg[0], 4), "b": round(lg[1], 4),
                                          "curva": [{"aprovacao": float(x), "p": round(float(1 / (1 + math.exp(-(lg[0] + lg[1] * x)))), 4)} for x in range(20, 81, 5)]}
    return out


# ----------------------------------------------------------------------------
# main
# ----------------------------------------------------------------------------
def main() -> None:
    polls = carregar_2t()
    he = house_effects(polls)
    vb = vies_historico()
    vb2 = vies_2t(vb)
    hist_pres = historico_1t_2t()
    trA = transferencia()
    vir = viradas()
    hist_gov = vir["bases"].get("governadores") or vir["bases"].get("brasil_subnacional")
    p_virada = vir.get("p_virada_2026")
    leit = leituras_segunda_opcao()
    transf_B = {k: tuple(v["premissa"]) for k, v in leit["escolhidas"].items()}
    transf_A = {k: (v["para_flavio"], v["para_lula"], v["abstem"]) for k, v in trA["premissas"].items()}
    inc = crescimento_incumbentes()
    resid = residual_incumbente()
    mob_B = resid.get("deslocamento_spread_2026_pp")
    mob_B = float(mob_B) if mob_B is not None else -0.8
    mob_B = max(mob_B, -3.0)  # teto de prudência: nunca mais que 3 pp de spread
    MEDIDO_PRE1T.update(pool_medido_pre1t())

    pA = Premissas("Ótica A", vb["prior_vies"]["media"], vb["prior_vies"]["desvio"], transf_A, 0.0, "presidencial",
                   {"A_pesquisas_vies": 0.45, "B_transferencia": 0.35, "C_estrutural": 0.20},
                   {"vies": "1T e 2T misturados, 1T de 2026 com peso 0,75", "transferencia": "média Quaest/Datafolha", "mobilizacao": "neutra (0)",
                    "estrutural": "presidencial, n = 7 (7/7)", "pesos": "A 45 / B 35 / C 20, sem classe de referência",
                    "selecao": "sem ajuste: o pool restante vota como o pool medido"}, None)
    pB = Premissas("Ótica B", vb2["prior_vies"]["media"], vb2["prior_vies"]["desvio"], transf_B, mob_B, "governadores",
                   {"A_pesquisas_vies": 0.30, "B_transferencia": 0.30, "C_estrutural": 0.20, "D_classe_referencia": 0.20},
                   {"vies": "só 2ºs turnos (2010–2022), pesos iguais", "transferencia": "leitura publicada mais favorável a Lula por eleitorado",
                    "mobilizacao": f"{mob_B:+.1f} pp (prêmio do incumbente: resíduo da segunda opção em 2014 e 2022, proporcional ao pool)".replace(".", ","),
                    "estrutural": f"governadores, n = {hist_gov['n'] if hist_gov else '?'}", "pesos": "A 30 / B 30 / C 20 / D 20 (classe de referência)",
                    "selecao": "3/4 dos que saíram do pool nas últimas 48 h eram segunda opção Flávio (rho = 0,75)"}, 0.75)
    cache: dict = {}
    rA = motor(pA, he, hist_pres, hist_gov, p_virada, cache)
    rB = motor(pB, he, hist_pres, hist_gov, p_virada, cache)
    # tornado: uma premissa de cada vez, partindo da Ótica A
    trocas = {
        "vies": lambda p: replace(p, vies_media=pB.vies_media, vies_sd=pB.vies_sd),
        "transferencia": lambda p: replace(p, transf=pB.transf),
        "mobilizacao": lambda p: replace(p, mob_media=pB.mob_media),
        "estrutural": lambda p: replace(p, estrutural=pB.estrutural),
        "pesos": lambda p: replace(p, pesos=pB.pesos),
        "selecao": lambda p: replace(p, rho_selecao=pB.rho_selecao),
    }
    tornado = []
    for k, f in trocas.items():
        r1 = motor(f(replace(pA, nome=f"A+{k}")), he, hist_pres, hist_gov, p_virada, cache)
        tornado.append({"premissa": k, "de": pA.rotulos[k], "para": pB.rotulos[k], "p_lula_A": rA["modelos"]["Combinado"]["p_lula"],
                        "p_lula_trocando_so_esta": r1["modelos"]["Combinado"]["p_lula"], "delta_pp": round(100 * (r1["modelos"]["Combinado"]["p_lula"] - rA["modelos"]["Combinado"]["p_lula"]), 1)})
    # caminho acumulado A → B
    acum, p_cur = [], pA
    for k, f in trocas.items():
        p_cur = f(replace(p_cur, nome=f"acum+{k}"))
        acum.append({"premissa": k, "p_lula": motor(p_cur, he, hist_pres, hist_gov, p_virada, cache)["modelos"]["Combinado"]["p_lula"]})
    sens = sensibilidades(pB, he, hist_pres, hist_gov, p_virada, cache)
    transf_B_aj = ajuste_selecao(pB.transf, pB.rho_selecao, MEDIDO_PRE1T)
    trB = cache[(tuple(sorted((k, tuple(round(x, 4) for x in v)) for k, v in transf_B_aj.items())), round(pB.mob_media, 3))]
    be = break_even({k: v for k, v in trB.items() if k != "spread_sims"})
    be["pool_medido_pre1t"] = MEDIDO_PRE1T
    be["premissas_B_ajustadas"] = {k: {"para_flavio": round(v[0], 3), "para_lula": round(v[1], 3), "abstem": round(v[2], 3), "medido_pre1t": MEDIDO_PRE1T.get(k)} for k, v in transf_B_aj.items()}
    # Bayes com prior da Ótica B (estrutural de governadores + viés só de 2T)
    hist_B = {"pred_m2_2026": hist_gov["estrutural"]["pred_m2_2026"], "regressao": {"resid_sd": hist_gov["estrutural"]["resid_sd"]}} if hist_gov and hist_gov.get("estrutural") else hist_pres
    recentes = [p for p in polls if p.fim >= "2026-09-30"]
    byB = bayes_sequencial(hist_B, {"prior_vies": vb2["prior_vies"]}, recentes)
    for passo in byB["passos"]:
        passo["p_lula"] = round(1 - passo["p_flavio"], 4)
    resultado = {
        "gerado_em": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M"), "hoje": HOJE.strftime("%Y-%m-%d"), "data_2t": DATA_2T.strftime("%Y-%m-%d"), "dias_restantes": DIAS_RESTANTES,
        "m1_2026": round(M1_2026, 2),
        "vies_2t": vb2, "vies_A": vb["prior_vies"],
        "viradas": vir, "leituras_segunda_opcao": leit, "incumbentes_1t_2t": inc, "residual_incumbente": resid,
        "otica_A": rA, "otica_B": rB, "tornado": tornado, "caminho_A_para_B": acum, "sensibilidades": sens, "break_even": be,
        "bayes_B": byB, "geografia": geografia(), "rejeicao": rejeicao_atual(), "reeleicao_incumbentes": incumbentes(),
        "pesquisas_2t_n": he.get("n_pesquisas"), "spread_hoje": he.get("spread_hoje"), "se_hoje": he.get("se_hoje"),
    }
    resultado = limpo(resultado)
    (SAIDA / "resultados_lula.json").write_text(json.dumps(resultado, ensure_ascii=False, indent=1, allow_nan=False), encoding="utf-8")
    (SITE_DATA / "resultados_lula.json").write_text(json.dumps(resultado, ensure_ascii=False, allow_nan=False), encoding="utf-8")
    print("viés 2T:", vb2["prior_vies"], "| grandes casas:", vb2["variante_grandes_casas"])
    print("viradas:", {k: (v.get("n"), v.get("taxa_geral"), v.get("p_virada_m1_2026")) for k, v in vir["bases"].items()}, "| P(virada 2026) =", p_virada)
    print("transferência B:", {k: v["fonte"] for k, v in leit["escolhidas"].items()})
    print("incumbentes:", inc.get("diferencial_medio_pp"), [(c["ano"], c["diferencial_incumbente"]) for c in inc.get("casos", [])])
    print("resíduo do incumbente:", [(c["ano"], c["parcela_prevista_pct"], c["parcela_real_pct"]) for c in resid.get("casos", [])], "-> mob_B =", mob_B)
    for nome, r in (("A", rA), ("B", rB)):
        print(f"Ótica {nome}:", {k: (v["p_lula"], v["media"]) for k, v in r["modelos"].items()})
    print("tornado:", [(t["premissa"], t["p_lula_trocando_so_esta"]) for t in tornado])
    print("acumulado:", [(a["premissa"], a["p_lula"]) for a in acum])
    print("break-even:", be)
    print("bayes B:", byB["posterior"])


if __name__ == "__main__":
    main()
