# ============================================================
# 03 — Bank: explaining a credit decision to the regulator
#
# Colab:  !pip install winnex-madhava
#
# THE REAL PROBLEM
#   A credit application is denied. The applicant files a complaint
#   and the regulator asks the bank to explain the decision.
#
#   Under LGPD art. 20 (and GDPR art. 22), the applicant has the right
#   to a review of any decision taken solely by automated processing.
#   The bank must reconstruct which policies, which risk notes, which
#   precedent justified the denial — and it must be right, because the
#   answer goes into a supervisory file.
#
#   Approximate search is a problem here: if the retrieval misses the
#   policy clause that would have approved the application, the bank
#   has denied credit on incomplete grounds and cannot prove otherwise.
#
# WHAT THIS EXAMPLE MODELS
#   A policy and precedent store — exactly what loan officers search
#   when they write a decision. We retrieve the governing clauses and
#   produce the record that goes into the regulatory answer.
# ============================================================

# instalação automática — Colab, Jupyter e script local.
# No Colab, "!pip" dentro de comentário não executa: a instalação
# precisa acontecer de dentro da célula, antes dos imports.
import importlib
import subprocess
import sys

for _pkg in ["winnex-madhava"]:
    _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
    )
    # O interpretador do Colab já está em execução quando o pip termina.
    # Sem invalidar o cache, o import abaixo pode não enxergar o pacote.
    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 sqlite3
import numpy as np
import winnex_madhava as mad

DIM = 256

def embed(text: str) -> np.ndarray:
    v = np.zeros(DIM, dtype=np.float32)
    for token in text.lower().split():
        for i, ch in enumerate(token):
            v[(ord(ch) * 31 + i * 7) % DIM] += 1.0
    n = np.linalg.norm(v)
    return v / n if n else v

# ── 1. the policy and precedent repository ──────────────────
conn = sqlite3.connect(":memory:")
conn.execute("""
    CREATE TABLE politicas_credito (
        codigo         TEXT PRIMARY KEY,
        vigencia       TEXT,
        categoria      TEXT,
        titulo         TEXT,
        texto          TEXT,
        aprovado_por   TEXT
    )
""")

politicas = [
    ("POL-0142", "2026-01-01", "limite",
     "Comprometimento de renda máximo",
     "A parcela da operação não pode comprometer mais de 30% da renda "
     "líquida comprovada do proponente. Comprometimento entre 30% e 35% "
     "exige parecer fundamentado da alçada de crédito."),

    ("POL-0187", "2026-01-01", "garantia",
     "Operações com garantia real",
     "Operações acima de R$ 100.000,00 devem contar com garantia real "
     "ou fiança de terceiro com patrimônio comprovado. Exceção admitida "
     "para clientes com relacionamento superior a cinco anos."),

    ("POL-0203", "2026-02-01", "exceção",
     "Política de exceção para relacionamento",
     "Clientes com relacionamento superior a cinco anos e histórico de "
     "adimplemento integral podem obter aprovação com comprometimento de "
     "renda de até 35%, mediante registro de justificativa."),

    ("POL-0115", "2025-06-01", "restrição",
     "Restrição cadastral impeditiva",
     "Proponente com apontamento ativo nos últimos 90 dias em bureaus de "
     "crédito não é elegível até regularização. Regularização parcial "
     "admite nova análise caso a caso."),

    ("POL-0264", "2026-03-01", "documentação",
     "Comprovação de renda",
     "Para autônomos, exige-se declaração de imposto de renda do último "
     "exercício acrescida de extratos bancários dos três meses anteriores."),

    ("PRE-0088", "2025-11-12", "precedente",
     "Caso semelhante - comprometimento de 33%",
     "Aprovação concedida a cliente com onze anos de relacionamento e sem "
     "apontamentos, com comprometimento de 33% da renda. Fundamentação: "
     "POL-0203 aplicada em conjunto com histórico de adimplemento."),
]

for p in politicas:
    conn.execute("INSERT INTO politicas_credito VALUES (?,?,?,?,?,?)",
                 (*p, "comitê de crédito"))
conn.commit()

# ── 2. the application under dispute ────────────────────────
proposta = {
    "id": "PROP-2026-03-4471",
    "renda_liquida": 18_000.00,
    "valor_solicitado": 180_000.00,
    "parcela": 5_940.00,
    "relacionamento_anos": 7,
    "restricao_ativa": False,
    "decisao": "negado",
    "motivo_registrado": "comprometimento de renda superior ao limite",
}

comprometimento = proposta["parcela"] / proposta["renda_liquida"]
print(f"application  : {proposta['id']}")
print(f"income       : R$ {proposta['renda_liquida']:,.2f}")
print(f"payment      : R$ {proposta['parcela']:,.2f}")
print(f"commitment   : {comprometimento:.1%}")
print(f"relationship : {proposta['relacionamento_anos']} years")
print(f"decision     : {proposta['decisao'].upper()}\n")

# ── 3. index the policy corpus ──────────────────────────────
rows = conn.execute("""
    SELECT codigo, categoria, titulo, texto
    FROM politicas_credito
    ORDER BY codigo
""").fetchall()

textos = [f"{r[2]}. {r[3]}" for r in rows]
codigos = [r[0] for r in rows]
corpus = np.stack([embed(t) for t in textos]).astype(np.float32)

engine = mad.build_engine(
    corpus, dim=DIM, metric="cosine", k=4,
    k1_fraction=1.0, postfilter=True, audit_exhaustive=True,
)

# ── 4. reconstruct the grounds for the decision ─────────────
consulta = (f"comprometimento de renda {comprometimento:.0%} "
            f"cliente com {proposta['relacionamento_anos']} anos de relacionamento "
            f"operação sem garantia real")

r = mad.retrieve(engine, embed(consulta), corpus_text=textos, k=4)

print(f"regulatory query: {consulta}\n")
print("policy that governs this decision:")
q = embed(consulta)
for pos, idx in enumerate(r["indices"], 1):
    score = float(np.dot(q, corpus[idx]))
    print(f"  {pos}. [{score:.3f}] {codigos[idx]} ({rows[idx][1]})")
    print(f"       {rows[idx][2]}")

# ── 5. what the regulator receives ──────────────────────────
aplicaveis = [codigos[i] for i in r["indices"]]
nao_aplicaveis = [c for c in codigos if c not in aplicaveis]

print(f"\n─── supervisory response record ───")
print(f"  policies considered applicable : {aplicaveis}")
print(f"  policies excluded with proof   : {nao_aplicaveis}")
print(f"  bound violations               : {r['bound_violations']}")
print(f"  latency                        : {r['latency_ms']:.2f} ms")

assert r["bound_violations"] == 0

# ── 6. did the original decision miss something? ────────────
print("""
What this reconstruction reveals
────────────────────────────────
POL-0203 allows up to 35% commitment for clients with more than five
years of relationship and no active restriction. The applicant has
seven years, no restriction, and 33% commitment.

The retrieval surfaces POL-0203 — and proves that no other policy in
the repository is responsive to this query. Whether the original
denial was correct is a credit judgement; whether it was made on a
complete reading of policy is now on the record.
""")

# ── 7. the exposure, stated plainly ─────────────────────────
print("Why the bank cares")
print("─" * 60)
print("""A denial reconstructed from incomplete policy retrieval is a
decision taken on grounds the bank cannot defend. Under LGPD art. 20
and GDPR art. 22 the applicant can demand review, and the supervisory
answer must show the grounds were complete. The bound is what turns
"we searched our policies" into "we can show nothing relevant was
missed".
""")

conn.close()
