"""
Ricerca ibrida con AgileV (contratto 1.2): denso + pesi lessicali, senza database vettoriale.

Serve a chi tiene i vettori in una tabella (MySQL, pgvector) o vuole capire cosa fa Qdrant quando fonde.
Dipendenze: solo la libreria standard. Chiave in AGILEV_KEY.

    python ricerca_ibrida.py "art. 25-septies" doc1.txt doc2.txt ...

Come funziona (numeri misurati su d.lgs. 231/2001 + 138/2024, vedi NOVITA.md del 21/9/2026):
  - coseno denso: buono sulle domande in linguaggio naturale, sbaglia le citazioni di articolo;
  - punteggio sparso (somma dei pesi sui token in comune): risolve le citazioni;
  - ibrido = alfa * coseno + (1 - alfa) * sparso / max_sparso_della_domanda. alfa 0,6 è il compromesso
    migliore sul nostro corpus; 0,9 se le domande sono quasi tutte in linguaggio naturale.
"""
import json, os, sys, urllib.request

URL_DEFAULT = "https://api.agilev.agile.software/v1/embeddings"


def dall_ambiente(nome):
    """Un valore che c'è ma è VUOTO è un errore, non un ripiego verso l'indirizzo pubblico."""
    v = os.environ.get(nome)
    if v is None:
        return None, "assente"
    if not v.strip():
        raise ValueError(f"{nome} c'e' ma e' vuota: toglila del tutto, oppure dalle un valore")
    return v.strip(), "ambiente"


_u, _s = dall_ambiente("AGILEV_URL")
URL = _u or URL_DEFAULT
SORGENTE_URL = _s if _u else "predefinito"      # `predefinito` = nessuno l'ha configurato: indirizzo PUBBLICO


def embed(testi, chiave=None):
    """Chiama /v1/embeddings con return_sparse. Ritorna [(denso, {token: peso}), ...] nell'ordine dei testi."""
    chiave = chiave or (dall_ambiente("AGILEV_KEY")[0] or "")
    if not chiave:
        raise ValueError("chiave AgileV mancante: passa chiave=... o imposta AGILEV_KEY")
    corpo = json.dumps({"model": "agilev-m3", "input": testi, "return_sparse": True}).encode()
    req = urllib.request.Request(URL, data=corpo, headers={"Authorization": f"Bearer {chiave}",
                                                          "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=180) as r:
        dati = json.load(r)["data"]
    return [(d["embedding"], dict(zip(d["sparse_embedding"]["indices"], d["sparse_embedding"]["values"])))
            for d in sorted(dati, key=lambda d: d["index"])]


def coseno(a, b):
    """I vettori di AgileV sono già normalizzati: il prodotto scalare è il coseno."""
    return sum(x * y for x, y in zip(a, b))


def punteggio_sparso(q, d):
    """Somma dei prodotti dei pesi sui token in comune (prodotto scalare fra vettori sparsi)."""
    if len(q) > len(d):
        q, d = d, q
    return sum(w * d.get(t, 0.0) for t, w in q.items())


def classifica(domanda, documenti, alfa=0.6):
    """
    domanda: (denso, sparso); documenti: lista di (denso, sparso).
    Ritorna gli indici dei documenti dal migliore al peggiore, con il punteggio ibrido.
    Il punteggio sparso è normalizzato sul massimo ottenuto dalla domanda, così è confrontabile col coseno.
    """
    qd, qs = domanda
    densi = [coseno(qd, dd) for dd, _ in documenti]
    sparsi = [punteggio_sparso(qs, ds) for _, ds in documenti]
    massimo = max(sparsi) if sparsi and max(sparsi) > 0 else 1.0
    ibridi = [alfa * c + (1 - alfa) * s / massimo for c, s in zip(densi, sparsi)]
    return sorted(range(len(documenti)), key=lambda i: -ibridi[i]), ibridi


def main(argv):
    if len(argv) < 3:
        sys.exit(__doc__)
    domanda, percorsi = argv[1], argv[2:]
    testi = [open(p, encoding="utf-8").read() for p in percorsi]
    vettori = embed([domanda] + testi)          # una chiamata sola: la domanda e i documenti insieme
    ordine, punteggi = classifica(vettori[0], vettori[1:])
    for i in ordine:
        print(f"{punteggi[i]:.3f}  {percorsi[i]}")


if __name__ == "__main__":
    main(sys.argv)
