#!/usr/bin/env python3
"""
NIGIN — Simulazione realistica: 10 mezzi / 250 consegne in Umbria.
Baseline = Nearest-Neighbor puro (come guidano davvero molti autisti, dal più
vicino al più vicino). Ottimizzato = motore reale (ILS) del server.
Distanze = OSRM reale (rete stradale vera), NON linea d'aria.

ONESTÀ: le 250 fermate sono punti VEROSIMILI distribuiti sui comuni umbri reali,
NON gli indirizzi reali di un cliente. È una simulazione del meccanismo su un
volume realistico, non un caso cliente. Nessun euro, nessun nome commerciale.

Usa le funzioni VERE del motore: nearest_neighbor(), ils(), cost_total().
Non tocca repository, SaaS, .env, storage o Git.
"""
import ast, json, math, random, time, urllib.request, urllib.error

SOURCE = "/opt/nigin/app/vrp_saas/vrp_osrm_fleet_backup.py"
OSRM = "http://127.0.0.1:5002"
N_MEZZI = 10
N_CONSEGNE = 250
SEED = 20260723

# Comuni umbri reali (lon, lat) con peso ~ popolazione (dove stanno i clienti)
COMUNI = [
    ("Perugia",        12.3908, 43.1107, 165),
    ("Terni",          12.6427, 42.5636, 110),
    ("Foligno",        12.7033, 42.9558, 57),
    ("Città di Castello",12.2386,43.4560, 40),
    ("Spoleto",        12.7386, 42.7402, 38),
    ("Gubbio",         12.5783, 43.3517, 32),
    ("Assisi",         12.6169, 43.0708, 28),
    ("Bastia Umbra",   12.5497, 43.0678, 22),
    ("Corciano",       12.2939, 43.1300, 21),
    ("Marsciano",      12.3389, 42.9128, 19),
    ("Narni",          12.5164, 42.5175, 20),
    ("Orvieto",        12.1108, 42.7185, 20),
    ("Todi",           12.4064, 42.7828, 17),
    ("Umbertide",      12.3272, 43.3050, 17),
    ("Castiglione del Lago",12.0503,43.1264,15),
    ("Magione",        12.2036, 43.1428, 15),
    ("Bevagna",        12.6100, 42.9331, 5),
    ("Montefalco",     12.6494, 42.8917, 6),
    ("Amelia",         12.4147, 42.5583, 12),
    ("Spello",         12.6708, 42.9917, 9),
]

DEPOT = (12.6427, 42.5636)  # Terni

def genera_fermate(seed=SEED):
    """250 fermate distribuite sui comuni umbri reali, pesate per popolazione.
    Dispersione realistica: fino a ~3-4 km attorno al centro di ogni comune."""
    rng = random.Random(seed)
    tot = sum(c[3] for c in COMUNI)
    fermate = []
    for _ in range(N_CONSEGNE):
        r = rng.uniform(0, tot); acc = 0
        for nome, lon, lat, peso in COMUNI:
            acc += peso
            if r <= acc:
                dlon = rng.gauss(0, 0.025); dlat = rng.gauss(0, 0.020)
                fermate.append((round(lon+dlon, 5), round(lat+dlat, 5)))
                break
    return fermate

def cluster_per_mezzo(fermate, seed=SEED+1):
    """ESATTAMENTE 10 mezzi (zone), carico BILANCIATO ~25 fermate a mezzo.
    Ogni mezzo copre un settore geografico della regione (giri regionali
    realistici), ma nessuna zona resta vuota o troppo sovraccarica."""
    rng = random.Random(seed)
    lons = [f[0] for f in fermate]; lats = [f[1] for f in fermate]
    lo_lon, hi_lon = min(lons), max(lons); lo_lat, hi_lat = min(lats), max(lats)
    # semi iniziali sparsi su tutta la regione
    semi = [((lo_lon+(hi_lon-lo_lon)*rng.random()),
             (lo_lat+(hi_lat-lo_lat)*rng.random())) for _ in range(N_MEZZI)]
    cap = math.ceil(len(fermate)/N_MEZZI) + 3   # tetto morbido per bilanciare
    gruppi = [[] for _ in range(N_MEZZI)]
    for _ in range(30):
        gruppi = [[] for _ in range(N_MEZZI)]
        # ogni fermata al mezzo più vicino che non ha ancora superato il tetto
        for f in fermate:
            ordine = sorted(range(N_MEZZI),
                            key=lambda i: (f[0]-semi[i][0])**2+(f[1]-semi[i][1])**2)
            scelto = next((i for i in ordine if len(gruppi[i]) < cap), ordine[0])
            gruppi[scelto].append(f)
        nuovi = []
        for i, g in enumerate(gruppi):
            if g:
                nuovi.append((sum(p[0] for p in g)/len(g), sum(p[1] for p in g)/len(g)))
            else:
                nuovi.append(semi[i])
        if nuovi == semi: break
        semi = nuovi
    # garanzia: nessuna zona vuota — se capita, sposta fermate dai gruppi più grandi
    for i in range(N_MEZZI):
        if not gruppi[i]:
            big = max(range(N_MEZZI), key=lambda k: len(gruppi[k]))
            gruppi[i].append(gruppi[big].pop())
    return gruppi

def osrm_matrix(points, timeout=30):
    """Restituisce DUE matrici: (durate in secondi, distanze in metri).
    Stesso ordine e stesse annotazioni del motore reale."""
    coords = ";".join(f"{lon},{lat}" for lon, lat in points)
    url = f"{OSRM}/table/v1/driving/{coords}?annotations=duration,distance"
    with urllib.request.urlopen(url, timeout=timeout) as r:
        data = json.loads(r.read().decode())
    if data.get("code") != "Ok":
        raise RuntimeError(f"OSRM code={data.get('code')}")
    return data["durations"], data["distances"]

def load_engine():
    src = open(SOURCE).read(); tree = ast.parse(src, SOURCE); sel = []
    for node in tree.body:
        if isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef)): sel.append(node)
        if isinstance(node, ast.FunctionDef) and node.name == "solve_canonical_request": break
    ns = {"__file__": SOURCE}
    exec(compile(ast.Module(body=sel, type_ignores=[]), SOURCE, "exec"), ns, ns)
    return ns

def una_ripetizione(ns, seed):
    """Esegue una simulazione completa 10 mezzi/250 consegne con un dato seed.
    Restituisce (km_nn_tot, km_opt_tot, delta_pct)."""
    fermate = genera_fermate(seed=seed)
    gruppi = cluster_per_mezzo(fermate, seed=seed+1)
    tot_nn = 0.0; tot_opt = 0.0
    for g in gruppi:
        m, d = osrm_matrix([DEPOT] + g)          # m=durate, d=distanze
        route_nn = ns["nearest_neighbor"](m)
        km_nn = ns["cost_km"](d, route_nn)/1000.0
        route_opt, _, _ = ns["ils"](m, d, time_limit=None, seed=11, iteration_limit=8)
        km_opt = ns["cost_km"](d, route_opt)/1000.0
        tot_nn += km_nn; tot_opt += km_opt
    dpct = (tot_nn-tot_opt)/tot_nn*100 if tot_nn else 0
    return tot_nn, tot_opt, dpct

def main():
    print("="*66)
    print("NIGIN — SIMULAZIONE 10 MEZZI / 250 CONSEGNE — UMBRIA (OSRM reale)")
    print("="*66)
    print("Fermate: punti VEROSIMILI su comuni umbri reali (NON indirizzi cliente).")
    print("Baseline: Nearest-Neighbor puro. Ottimizzato: motore reale (ILS).")
    print("Distanze: OSRM, rete stradale vera. Deposito: Terni.")
    print("3 ripetizioni con punti diversi -> range, non un singolo numero.")
    print("-"*66)

    ns = load_engine()
    # verifica OSRM una volta
    dur_d, dist_d = osrm_matrix([DEPOT, (12.3908, 43.1107)])  # Terni->Perugia
    print(f"Check OSRM: Terni -> Perugia = {dist_d[0][1]/1000:.0f} km. OSRM ok.\n")

    print(f"{'Ripetiz.':>9} | {'NN totale':>11} | {'Ottimizzato':>12} | {'Differenza':>11} | {'Δ%':>6}")
    print("-"*66)
    risultati = []
    t0 = time.perf_counter()
    for rip, seed in enumerate([20260723, 777, 424242], 1):
        nn, opt, dpct = una_ripetizione(ns, seed)
        risultati.append((nn, opt, dpct))
        print(f"{rip:>9} | {nn:>8,.0f} km | {opt:>9,.0f} km | {nn-opt:>8,.0f} km | {dpct:>5.1f}%")
    wall = time.perf_counter()-t0

    print("-"*66)
    deltas = [r[2] for r in risultati]
    nn_med = sum(r[0] for r in risultati)/len(risultati)
    opt_med = sum(r[1] for r in risultati)/len(risultati)
    dmin, dmax = min(deltas), max(deltas)
    dmed = sum(deltas)/len(deltas)
    print(f"RANGE DELTA su 3 ripetizioni:  da {dmin:.1f}%  a  {dmax:.1f}%   (media {dmed:.1f}%)")
    print(f"Distanza media NN:          {nn_med:,.0f} km / settimana")
    print(f"Distanza media ottimizzata: {opt_med:,.0f} km / settimana")
    print(f"Risparmio medio in km:      {nn_med-opt_med:,.0f} km / settimana")
    print(f"Tempo totale (3 ripetizioni, OSRM reale): {wall:.1f} s")
    print("-"*66)
    print(">>> DATO PER IL SITO (onesto):")
    print(f">>> Simulazione 10 mezzi / 250 consegne / Umbria / rete stradale reale:")
    print(f">>> l'ottimizzazione riduce la percorrenza del {dmin:.0f}-{dmax:.0f}% rispetto")
    print(f">>> a un giro Nearest-Neighbor. Fermate verosimili, non un cliente reale.")
    print("-"*66)
    print("SIMULAZIONE: fermate verosimili, non indirizzi reali di un cliente.")
    print("Sul giro vero il risultato può essere maggiore, minore o nullo.")
    print("Nessun file del repository, del SaaS, di storage o Git è stato modificato.")

    out = "/root/nigin-simulazione-umbria-results.json"
    try:
        open(out, "a").close()
    except Exception:
        out = "nigin-simulazione-umbria-results.json"
    with open(out, "w") as h:
        json.dump({"mezzi": N_MEZZI, "consegne": N_CONSEGNE,
                   "ripetizioni": risultati,
                   "delta_min": dmin, "delta_max": dmax, "delta_media": dmed,
                   "km_nn_medio": nn_med, "km_opt_medio": opt_med,
                   "tempo_s": wall, "osrm": OSRM}, h, indent=2)
    print(f"Risultati grezzi: {out}")

if __name__ == "__main__":
    main()
