# ============================================================
# 17 — The encoder decides whether the proof means anything
#
# Colab:  !pip install winnex-madhava sentence-transformers
#
# POR QUE ESTE EXEMPLO EXISTE
#   Os exemplos 01 a 12 usam um embedding de demonstração que soma
#   ord(ch) % DIM. Ele roda sem download e sem chave — mas não
#   captura significado. Este exemplo mede o estrago, e mostra o
#   mesmo teste com um encoder real.
#
#   O bound do Madhava prova que o top-K é exato. Ele NÃO prova que
#   o ranking faz sentido. Essa parte é responsabilidade do encoder,
#   e é onde a maioria dos pipelines falha em silêncio.
# ============================================================

# instalação automática — Colab, Jupyter e script local.
import importlib
import subprocess
import sys

for _pkg in ["winnex-madhava", "sentence-transformers"]:
    _mod = _pkg.replace("-", "_")
    try:
        __import__(_mod)
        continue
    except ImportError:
        pass
    _r = subprocess.run(
        [sys.executable, "-m", "pip", "install", "-q", "--upgrade", _pkg],
        capture_output=True, text=True
    )
    importlib.invalidate_caches()
    try:
        __import__(_mod)
    except ImportError:
        sys.stderr.write(_r.stdout or "")
        sys.stderr.write(_r.stderr or "")
        raise SystemExit(
            "\n\nNão foi possível importar " + _pkg + " após instalar.\n"
            "No Colab: menu 'Ambiente de execução' -> 'Reiniciar sessão',\n"
            "e execute esta célula novamente.\n"
        )

import numpy as np

DIM_DEMO = 128

# ── os pares de teste ───────────────────────────────────────
# Cada linha: (frase A, frase B, são relacionados?)
# As paráfrases NÃO compartilham quase nenhum caractere — é de
# propósito. Um encoder de caracteres não tem como acertar.
PARES = [
    ("renda comprometida acima do limite",
     "comprometimento de renda excessivo", True),
    ("o carro não liga de manhã",
     "o automóvel apresenta falha ao amanhecer", True),
    ("multa por atraso na entrega",
     "penalidade por demora na remessa", True),
    ("cláusula de rescisão antecipada",
     "multa por término contratual adiantado", True),
    ("paciente com febre e dor abdominal",
     "quadro infeccioso com dor no abdome", True),
    ("o carro não liga de manhã",
     "o veículo não pega cedo", True),
    ("cláusula de rescisão antecipada",
     "prazo de entrega do fornecedor", False),
    ("o carro não liga de manhã",
     "o paciente tem febre alta", False),
    ("multa por atraso na entrega",
     "o cachorro late à noite", False),
    ("renda comprometida acima do limite",
     "contrato de aluguel comercial", False),
    ("paciente com febre e dor abdominal",
     "nota fiscal de serviços", False),
]


def emb_demo(t: str) -> np.ndarray:
    """O embedding dos exemplos 01–12: hash de caracteres. Sem semântica."""
    v = np.zeros(DIM_DEMO, dtype=np.float32)
    for tok in t.lower().split():
        for i, ch in enumerate(tok):
            v[(ord(ch) * 31 + i * 7) % DIM_DEMO] += 1.0
    n = np.linalg.norm(v)
    return v / n if n else v


def avaliar(nome, vetores_a, vetores_b):
    """Separação entre paráfrases e pares sem relação."""
    par, nao = [], []
    for i, (_, _, relacionado) in enumerate(PARES):
        s = float(vetores_a[i] @ vetores_b[i])
        (par if relacionado else nao).append(s)
    par, nao = np.array(par), np.array(nao)
    margem = par.min() - nao.max()

    print(f"\n{nome}")
    print(f"  paraphrases       min={par.min():.4f}  mean={par.mean():.4f}")
    print(f"  unrelated         max={nao.max():.4f}  mean={nao.mean():.4f}")

    separados = [p for p in par if p > nao.max()]
    print(f"  paraphrases above the worst negative : "
          f"{len(separados)}/{len(par)}")
    print(f"  margin (min_par − max_nao)           : {margem:+.4f}")

    if margem > 0:
        print(f"  → a threshold separates the two classes")
    else:
        print(f"  → NO threshold can separate them")
    return par, nao


# ── 1. o encoder de demonstração ────────────────────────────
print("=" * 62)
print("ENCODER DE DEMONSTRAÇÃO — hash de caracteres")
print("=" * 62)

a_demo = np.stack([emb_demo(x) for x, _, _ in PARES])
b_demo = np.stack([emb_demo(y) for _, y, _ in PARES])
avaliar("resultado", a_demo, b_demo)

# ── 2. um encoder real ──────────────────────────────────────
print("\n" + "=" * 62)
print("ENCODER REAL — all-MiniLM-L6-v2 (384 dims, ~90 MB)")
print("=" * 62)

from sentence_transformers import SentenceTransformer

modelo = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
a_real = modelo.encode([x for x, _, _ in PARES], normalize_embeddings=True)
b_real = modelo.encode([y for _, y, _ in PARES], normalize_embeddings=True)
par_real, nao_real = avaliar("resultado", a_real, b_real)

# ── 3. o mesmo corpus, os dois encoders ─────────────────────
# A pergunta que importa não é "qual pontua mais alto" — é "a
# resposta muda". Indexamos o mesmo corpus e comparamos o top-3.
print("\n" + "=" * 62)
print("A MESMA BUSCA, OS DOIS ENCODERS")
print("=" * 62)

corpus = [
    "O locatário pagará aluguel mensal de R$ 2.000,00 corrigido anualmente.",
    "Fica reajustado o valor do aluguel para R$ 2.400,00 a partir de janeiro.",
    "A multa por rescisão antecipada é de três aluguéis.",
    "As partes se obrigam a manter sigilo pelo prazo de 5 anos.",
    "O foro de eleição é a comarca de Goiânia.",
    "A garantia locatícia é fiança bancária no valor de R$ 7.200,00.",
]
consulta = "até quando devo guardar segredo sobre o negócio"

# nota: a consulta não usa a palavra "sigilo" nem "confidencialidade".
# Ela descreve a situação em linguagem comum. Um encoder de caracteres
# não tem como ligar as duas coisas; um encoder treinado tem.
print(f"\nquery: {consulta!r}")
print("(no word in common with the correct answer: 'confidentiality')")

import winnex_madhava as mad


def buscar(nome, enc_a, enc_b, dim):
    c = np.stack([enc_a(t) for t in corpus]).astype(np.float32)
    e = mad.build_engine(c, dim=dim, k=3, k1_fraction=1.0,
                         postfilter=True, audit_exhaustive=True)
    q = enc_b(consulta).astype(np.float32)
    r = mad.retrieve(e, q, k=3)
    print(f"\n{nome}:")
    for pos, idx in enumerate(r["indices"], 1):
        score = float(np.dot(q, c[idx]))
        print(f"  {pos}. [{score:.3f}] {corpus[idx][:58]}")
    print(f"  bound_violations={r['bound_violations']}")
    return r


buscar("demo (hash de caracteres)", emb_demo, emb_demo, DIM_DEMO)

enc = modelo.encode
buscar("real (all-MiniLM-L6-v2)",
       lambda t: np.asarray(enc([t], normalize_embeddings=True)[0]),
       lambda t: np.asarray(enc([t], normalize_embeddings=True)[0]),
       384)

# ── 4. conclusão honesta ────────────────────────────────────
print("""
O que este exemplo mostra
─────────────────────────
Os dois encoders devolvem bound_violations = 0. O Madhava prova o
top-K exato nos dois casos. A diferença está em QUAL pergunta foi
respondida:

  · com o encoder de demonstração, "exato" significa exato em
    relação a um ranking de sobreposição de caracteres;
  · com um encoder real, significa exato em relação a significado.

A prova do bound nunca substitui a qualidade do embedding. Ela só
garante que, dada a representação, a busca não perdeu nada.

Onde o encoder real também erra
───────────────────────────────""")

for i, (x, y, rel) in enumerate(PARES):
    if not rel:
        s = float(a_real[i] @ b_real[i])
        if s > par_real.min():
            print(f"  o par sem relação {x[:32]!r} / {y[:32]!r}")
            print(f"    pontua {s:.4f}, acima da paráfrase mais fraca "
                  f"({par_real.min():.4f}).")
            print("    Nem um encoder treinado separa tudo — por isso existe")
            print("    a segunda etapa do quality gate (exemplo 05).")

print("\nOK — a diferença entre prova e significado está medida.")
