"""
v2_stats_dashboard.py — Stats Dashboard 2° account (port 5511, Basic Auth)
NO template engine: tutto render server-side in Python (niente {{}} / {% %} nel template).
"""
import os
import sys
import json
import sqlite3
import hmac
import secrets
import string
from datetime import datetime, timezone
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
from collections import defaultdict

# === CONFIG ===
ROOT = Path("/opt/charter-live/live_deploy_v2")
DB_PATH = ROOT / "logs" / "webhook_queue.db"
PORT = int(os.environ.get("V2_STATS_PORT", "5511"))
LISTEN_HOST = "0.0.0.0"
LOG_DIR = ROOT / "logs"
LOG_FILE = LOG_DIR / "v2_stats_dashboard.log"

# === AUTH (caricata da .v2_stats_creds via systemd EnvironmentFile) ===
def _load_auth_from_envfile():
    creds_file = ROOT / ".v2_stats_creds"
    if creds_file.exists():
        try:
            for line in creds_file.read_text().splitlines():
                line = line.strip()
                if line.startswith("#") or "=" not in line:
                    continue
                k, v = line.split("=", 1)
                k = k.strip()
                v = v.strip().strip('"').strip("'")
                if k == "V2_STATS_USER" and not os.environ.get("V2_STATS_USER"):
                    os.environ["V2_STATS_USER"] = v
                elif k == "V2_STATS_PASS" and not os.environ.get("V2_STATS_PASS"):
                    os.environ["V2_STATS_PASS"] = v
        except Exception:
            pass

_load_auth_from_envfile()
AUTH_USER = os.environ.get("V2_STATS_USER", "mattia_v2_stats")
AUTH_PASS = os.environ.get("V2_STATS_PASS", "")


def _parse_bybit_ts(ts):
    """Bybit V5 closed-pnl restituisce createdAt/updatedTime in MILLISECONDI come stringa
    (es. "1785856148609"). Accetta anche ISO 8601 ("2026-08-05T20:39:31Z") o None/"".
    Ritorna datetime (UTC) oppure None se non parsabile.
    """
    if ts is None:
        return None
    s = str(ts).strip()
    if not s:
        return None
    # Millisecondi: tutti digits e lunghezza >= 10
    if s.isdigit() and len(s) >= 10:
        try:
            ms = int(s)
            return datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc)
        except Exception:
            return None
    # ISO 8601 fallback
    try:
        return datetime.fromisoformat(s.replace("Z", "+00:00"))
    except Exception:
        return None


def _fmt_bybit_ts(ts, fmt="%Y-%m-%d %H:%M:%S"):
    """Ritorna una stringa formattata (UTC) per ts Bybit (ms o ISO). None se non parsabile."""
    dt = _parse_bybit_ts(ts)
    if dt is None:
        return ""
    return dt.strftime(fmt)


def log(msg):
    ts = datetime.now(timezone.utc).astimezone().isoformat()
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass


# === DATA LOADERS ===
def get_bybit_positions():
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        return c.fetch_positions()
    except Exception as e:
        log(f"bybit positions err: {e}")
        return []


def get_bybit_balance():
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        b = c._request("GET", "/v5/account/wallet-balance",
                       {"accountType": "UNIFIED"}, signed=True)
        coin = b["result"]["list"][0]["coin"][0]
        return float(coin["walletBalance"])
    except Exception as e:
        log(f"bybit balance err: {e}")
        return 0.0


# Lista dei trade/posizioni di MIO TEST Pine-style da escludere dalla dashboard
MY_TEST_FILTER = {
    "symbols_qty": {
        # (symbol, qty) -> True se e' MIO test
        ("VIRTUALUSDT", 1500.0): True,   # VIRTUAL placeholder 1500 (mio test Pine-style)
        ("NEARUSDT", 599.9): True,         # NEAR mio test ma_trailing
    },
    "symbols": {
        "VIRTUALUSDT",  # symbol sospetto placeholder price=1.0
    },
}

# Data di partenza: 1° agosto 2026
SINCE_DATE_MS = int(__import__("datetime").datetime(2026, 8, 1, 0, 0, 0, tzinfo=__import__("datetime").timezone.utc).timestamp() * 1000)


def is_mio_test_trade(trade):
    """True se il trade chiuso Bybit corrisponde a un MIO test Pine-style."""
    sym = trade.get("symbol", "")
    qty = float(trade.get("closedSize", 0) or 0)
    if (sym, qty) in MY_TEST_FILTER["symbols_qty"] and (sym, qty) in MY_TEST_FILTER["symbols_qty"]:
        return True
    avg_entry = float(trade.get("avgEntryPrice", 0) or 0)
    # VIRTUAL placeholder Pine: avgEntry ~ 1.0 (placeholder Pine) o avgEntry 0.5711 (eseguito Bybit mio test)
    if sym == "VIRTUALUSDT" and abs(avg_entry - 1.0) < 1e-6 and abs(qty - 1500.0) < 0.01:
        return True
    if sym == "NEARUSDT" and abs(qty - 599.9) < 0.01:
        return True
    if sym == "BTCUSDT" and abs(qty - 0.023) < 1e-6:
        return True
    return False


def is_mio_test_position(pos):
    """True se la posizione aperta e' un MIO test Pine-style."""
    sym = pos.get("symbol", "")
    try:
        size = float(pos.get("size", 0) or 0)
    except (TypeError, ValueError):
        return False
    if sym == "NEARUSDT" and abs(size - 599.9) < 0.01:
        return True
    if sym == "VIRTUALUSDT" and abs(size - 1500.0) < 0.01:
        return True
    if sym == "BTCUSDT" and abs(size - 0.023) < 1e-6:
        return True
    return False


def get_closed_trades_bybit():
    """Legge TUTTI i trade chiusi da Bybit API (closed-pnl endpoint), filtra dal 1° agosto,
    esclude MIEI test Pine-style."""
    out = []
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        # Endpoint Bybit V5: GET /v5/position/closed-pnl
        # Parametri: category=linear, startTime (ms), endTime (ms), limit (max 50)
        now_ms = int(__import__("datetime").datetime.now(__import__("datetime").timezone.utc).timestamp() * 1000)
        cursor = None
        while True:
            params = {
                "category": "linear",
                "startTime": SINCE_DATE_MS,
                "endTime": now_ms,
                "limit": 50,
            }
            if cursor:
                params["cursor"] = cursor
            r = c._request("GET", "/v5/position/closed-pnl", params, signed=True)
            items = (r.get("result") or {}).get("list", []) or []
            for it in items:
                if is_mio_test_trade(it):
                    continue
                sym_raw = it.get("symbol", "")
                # Mapping symbol -> strategia per il 2° account (basato su chi gestisce l'asset)
                SYMBOL_STRATEGY_2ACC = {
                    "BTCUSDT": "rettangolo",      # rettangolo_runner
                    "ZECUSDT": "rettangolo",       # rettangolo_runner
                    "WIFUSDT": "rettangolo",       # rettangolo_runner
                    "RENDERUSDT": "rettangolo",    # rettangolo_runner
                    "UNIUSDT": "rettangolo",       # rettangolo_runner
                    "VIRTUALUSDT": "vptr3",        # Pine Pine Pine Pine Pine
                    "AXSUSDT": "vptr3",            # Pine Pine Pine Pine Pine
                    "ARUSDT": "rettangolo_simple", # Pine Pine Pine Pine Pine
                    "NEARUSDT": "ma_trailing",     # Pine Pine Pine Pine Pine
                    "BEATUSDT": "ma_trailing",     # Pine Pine Pine Pine Pine
                }
                strat = SYMBOL_STRATEGY_2ACC.get(sym_raw, "vptr3")
                qty_t = float(it.get("closedSize", 0) or 0)
                entry_t = float(it.get("avgEntryPrice", 0) or 0)
                exit_t = float(it.get("avgExitPrice", 0) or 0)
                net_pnl = float(it.get("closedPnl", 0) or 0)
                # Fee: Bybit V5 fornisce openFee + closeFee (somma = totalFee)
                open_fee = float(it.get("openFee", 0) or 0)
                close_fee = float(it.get("closeFee", 0) or 0)
                # Se Bybit non restituisce fee separate, prova totalFee
                total_fee = float(it.get("totalFee", 0) or 0) or (open_fee + close_fee)
                # Gross PnL = Net PnL + Total Fee (fee detratte dal lordo)
                gross_pnl = net_pnl + total_fee
                # PnL % basato su nozionale entry
                notional_entry = abs(entry_t * qty_t)
                pnl_pct_gross = (gross_pnl / notional_entry * 100) if notional_entry > 0 else 0
                pnl_pct_net = (net_pnl / notional_entry * 100) if notional_entry > 0 else 0
                out.append({
                    "symbol": sym_raw,
                    "side": it.get("side", ""),  # "Buy" o "Sell"
                    "qty": qty_t,
                    "entry_price": entry_t,
                    "exit_price": exit_t,
                    "pnl": net_pnl,  # backward compat: "pnl" = net
                    "pnl_net": net_pnl,
                    "pnl_gross": gross_pnl,
                    "fee": total_fee,
                    "pnl_pct_gross": pnl_pct_gross,
                    "pnl_pct_net": pnl_pct_net,
                    "strategy": strat,
                    "created_at": it.get("createdAt", ""),
                    "updated_at": it.get("updatedTime", ""),
                })
            cursor = (r.get("result") or {}).get("nextPageCursor")
            if not cursor or len(items) < 50:
                break
        return out
    except Exception as e:
        log(f"bybit closed trades err: {e}")
        return out


def get_open_positions_bybit_no_test():
    """Legge posizioni aperte da Bybit, ESCLUDE MIEI test Pine-style."""
    raw = get_bybit_positions()
    return [p for p in raw if not is_mio_test_position(p)]


def get_orders(limit=2000, since_date="2026-08-01"):
    """Carica ordini dal DB, filtrati per data >= since_date (default 1° agosto 2026)."""
    out = []
    if not DB_PATH.exists():
        return out
    try:
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute(
            "SELECT order_id, symbol, side, qty, price, notional, strategy, created_at "
            "FROM orders WHERE created_at >= ? ORDER BY id DESC LIMIT ?",
            (since_date, limit)
        )
        for row in cur.fetchall():
            out.append(dict(row))
        conn.close()
    except Exception as e:
        log(f"orders load err: {e}")
    return out


def get_queue_stats():
    if not DB_PATH.exists():
        return {"pending": 0, "completed": 0, "failed": 0, "total": 0}
    try:
        conn = sqlite3.connect(DB_PATH)
        cur = conn.cursor()
        cur.execute("SELECT status, COUNT(*) FROM queue GROUP BY status")
        stats = {"pending": 0, "completed": 0, "failed": 0, "total": 0}
        for status, count in cur.fetchall():
            stats[status] = count
            stats["total"] += count
        conn.close()
        return stats
    except Exception as e:
        log(f"queue stats err: {e}")
        return {"pending": 0, "completed": 0, "failed": 0, "total": 0}


MY_TEST_ORDER_IDS = {
    "31a9c2fd-540e-45a5-b44f-fe2f8fdb4495",  # VIRTUAL 1500 buy placeholder (mio test)
    "4cbb1304-8020-42bd-bcad-8e5d0f19caed",  # NEAR 599.9 buy (mio test)
    "5c53a9ef-dcab-469e-bbd1-3057fe2746d9",  # BTC 0.023 buy (mio test)
    "8a53e7b1-c7d4-4e5a-b67b-4e4fbf4c0977",  # VIRTUAL 2496 buy (mio test)
    "2d2e625b-d3b2-4d87-931a-9b378cf8db93",  # VIRTUAL 2679 buy (mio test)
    "514f5553-6aa3-4148-8e1a-82961c47310b",  # VIRTUAL 2712 buy (mio test)
    "b426dc94-b72f-427b-b2f2-7e021f5da985",  # VIRTUAL 2496 buy (mio test)
    "4c8a4478-377e-4b8c-b371-44c64090e26d",  # VIRTUAL buy qty=0 (mio test)
    "f79ab621-dc17-488e-950e-e984fc0d9e43",  # VIRTUAL sell qty=0 (mio test)
    "2cd6e92c-b82b-4814-808e-e23579f1a533",  # VIRTUAL sell Max exit (chiude mio test)
    "e45f561b-c4d8-4045-a2fe-581140fadac1",  # VIRTUAL sell Max exit (chiude mio test 1500)
    "c60d098a-1ec5-4101-9e77-966f67f4f393",  # VIRTUAL sell Pine reale (Max exit di VIRTUAL 2632 Pine reale)
    "2d2e625b-d3b2-4d87-931a-9b378cf8db93",
    "79969acc-3e4b-4101-91bc-69eede1ef6aa",  # ZEC rettangolo (non mio)
    "44e4a3ad-23ec-4b7f-b6f8-35778858e5bf",  # BTC rettangolo (non mio)
    "7ed5cc82-3a99-4978-8ae3-c295e82f4609",  # BTC rettangolo (non mio)
}


def is_mio_test(o):
    """True se l'ordine e' un MIO test Pine-style (placeholder price=1.0, NEAR 599.9, o order_id in MY_TEST_ORDER_IDS)."""
    oid = str(o.get("order_id", ""))
    if oid in MY_TEST_ORDER_IDS:
        return True
    sym = o.get("symbol", "")
    try:
        qty = float(o.get("qty", 0) or 0)
        price = float(o.get("price", 0) or 0)
    except (TypeError, ValueError):
        return False
    # VIRTUAL Pine placeholder (price=1.0, qty variabile)
    if sym == "VIRTUALUSDT" and abs(price - 1.0) < 1e-9:
        return True
    # NEAR mio test (qty=599.9)
    if sym == "NEARUSDT" and abs(qty - 599.9) < 0.01:
        return True
    # BTC mio test (qty=0.023)
    if sym == "BTCUSDT" and abs(qty - 0.023) < 1e-6:
        return True
    return False


def compute_trade_pnls(orders):
    """Matcha buy+sell FIFO per symbol. Esclude trade che coinvolgono MIEI test Pine-style
    (placeholder price=1.0 o order_id in MY_TEST_ORDER_IDS)."""
    by_symbol = defaultdict(list)
    for o in sorted(orders, key=lambda x: x.get("created_at", "")):
        sym = o.get("symbol", "")
        if not sym:
            continue
        oid = o.get("order_id", "")
        if oid in ("no_position", "anti_dup_skip", "", None):
            continue
        if o.get("qty", 0) <= 0:
            continue
        # Filtra MIEI test
        if is_mio_test(o):
            continue
        by_symbol[sym].append(o)
    trades = []
    for sym, ords in by_symbol.items():
        open_buys = []
        for o in ords:
            side = o.get("side", "").lower()
            qty = float(o.get("qty", 0) or 0)
            price = float(o.get("price", 0) or 0)
            strat = o.get("strategy", "")
            ts = o.get("created_at", "")
            if side == "buy":
                placeholder = abs(price - 1.0) < 1e-9
                open_buys.append({"qty": qty, "price": price, "strategy": strat, "ts": ts, "placeholder": placeholder})
            elif side == "sell" and open_buys:
                qty_to_close = qty
                while qty_to_close > 0 and open_buys:
                    buy = open_buys[0]
                    matched = min(qty_to_close, buy["qty"])
                    pnl = (price - buy["price"]) * matched
                    pnl_pct = ((price - buy["price"]) / buy["price"]) * 100 if buy["price"] > 0 else 0
                    # Warning se PnL% estremo (>30%) o buy era placeholder Pine
                    warn = buy.get("placeholder", False) or abs(pnl_pct) > 30
                    trades.append({
                        "symbol": sym, "side": "long",
                        "qty": matched, "entry_price": buy["price"], "exit_price": price,
                        "pnl": pnl, "pnl_pct": pnl_pct,
                        "strategy": buy["strategy"], "entry_ts": buy["ts"], "exit_ts": ts,
                        "warn": warn,
                    })
                    qty_to_close -= matched
                    buy["qty"] -= matched
                    if buy["qty"] <= 1e-9:
                        open_buys.pop(0)
    return trades


def compute_kpis_from_bybit(closed_trades, positions, balance):
    """Calcola KPI da trade chiusi Bybit (gia filtrati per data e test) + posizioni aperte (gia filtrate)."""
    n = len(closed_trades)
    if n == 0:
        return {
            "n_trades": 0, "wins": 0, "losses": 0, "winrate": 0.0,
            "total_pnl": 0.0, "total_pnl_gross": 0.0, "total_fees": 0.0,
            "avg_pnl": 0.0, "avg_pnl_gross": 0.0,
            "best_trade": 0.0, "worst_trade": 0.0,
            "open_positions": len(positions), "unrealized_pnl": 0.0,
            "balance": balance, "total_pnl_with_unrealized": 0.0,
        }
    wins = [t for t in closed_trades if t.get("pnl_net", t.get("pnl", 0)) > 0]
    losses = [t for t in closed_trades if t.get("pnl_net", t.get("pnl", 0)) <= 0]
    total_pnl_net = sum(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades)
    total_pnl_gross = sum(t.get("pnl_gross", 0) for t in closed_trades)
    total_fees = sum(t.get("fee", 0) for t in closed_trades)
    unrealized = 0.0
    for p in positions:
        try:
            unrealized += float(p.get("unrealisedPnl", 0) or 0)
        except Exception:
            pass
    return {
        "n_trades": n,
        "wins": len(wins),
        "losses": len(losses),
        "winrate": (len(wins) / n * 100) if n else 0.0,
        "total_pnl": total_pnl_net,            # alias per compatibilita
        "total_pnl_net": total_pnl_net,        # NET (dopo fee)
        "total_pnl_gross": total_pnl_gross,    # GROSS (prima delle fee)
        "total_fees": total_fees,              # TOTALE FEE
        "avg_pnl": total_pnl_net / n if n else 0.0,
        "avg_pnl_gross": total_pnl_gross / n if n else 0.0,
        "best_trade": max(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades) if closed_trades else 0.0,
        "worst_trade": min(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades) if closed_trades else 0.0,
        "open_positions": len(positions),
        "unrealized_pnl": unrealized,
        "balance": balance,
        "total_pnl_with_unrealized": total_pnl_net + unrealized,
        "total_pnl_gross_with_unrealized": total_pnl_gross + unrealized,
    }


def compute_equity_curve_from_bybit(closed_trades, balance):
    """Calcola equity curve da trade chiusi Bybit (gia filtrati)."""
    if not closed_trades:
        return {"dates": [], "equity": [], "trades_count": 0}
    closed_trades.sort(key=lambda t: t.get("updated_at") or t.get("created_at") or "")
    initial_equity = max(balance - sum(t["pnl"] for t in closed_trades), 0)
    dates, equity, cum = [], [], 0
    for t in closed_trades:
        cum += t["pnl"]
        ts = t.get("updated_at") or t.get("created_at") or ""
        dates.append(ts)
        equity.append(initial_equity + cum)
    return {"dates": dates, "equity": equity, "trades_count": len(closed_trades)}


def compute_by_strategy_from_bybit(closed_trades):
    by_strat = defaultdict(lambda: {"n": 0, "wins": 0, "pnl": 0.0, "pnl_gross": 0.0, "fees": 0.0})
    for t in closed_trades:
        s = t.get("strategy", "unknown")
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        by_strat[s]["n"] += 1
        if pnl_n > 0:
            by_strat[s]["wins"] += 1
        by_strat[s]["pnl"] += pnl_n
        by_strat[s]["pnl_gross"] += t.get("pnl_gross", 0)
        by_strat[s]["fees"] += t.get("fee", 0)
    out = []
    for s, d in by_strat.items():
        wr = (d["wins"] / d["n"] * 100) if d["n"] else 0
        out.append({"strategy": s, "n": d["n"], "wins": d["wins"], "winrate": wr,
                    "pnl": d["pnl"], "pnl_gross": d["pnl_gross"], "fees": d["fees"]})
    out.sort(key=lambda x: x["pnl"], reverse=True)
    return out


def compute_by_asset_from_bybit(closed_trades):
    by_asset = defaultdict(lambda: {"n": 0, "wins": 0, "pnl": 0.0, "pnl_gross": 0.0, "fees": 0.0, "volume": 0.0})
    for t in closed_trades:
        a = t.get("symbol", "")
        if not a:
            continue
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        by_asset[a]["n"] += 1
        if pnl_n > 0:
            by_asset[a]["wins"] += 1
        by_asset[a]["pnl"] += pnl_n
        by_asset[a]["pnl_gross"] += t.get("pnl_gross", 0)
        by_asset[a]["fees"] += t.get("fee", 0)
        by_asset[a]["volume"] += t.get("qty", 0) * t.get("entry_price", 0)
    out = []
    for a, d in by_asset.items():
        wr = (d["wins"] / d["n"] * 100) if d["n"] else 0
        out.append({"symbol": a, "n": d["n"], "wins": d["wins"], "winrate": wr,
                    "pnl": d["pnl"], "pnl_gross": d["pnl_gross"], "fees": d["fees"], "volume": d["volume"]})
    out.sort(key=lambda x: x["pnl"], reverse=True)
    return out


def compute_heatmap_from_bybit(closed_trades):
    matrix = [[0] * 24 for _ in range(7)]
    days = ["Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Dom"]
    for t in closed_trades:
        dt = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        if dt is None:
            continue
        # Converti in local time per heatmap (Europa)
        local_dt = dt.astimezone()
        matrix[local_dt.weekday()][local_dt.hour] += 1
    return {"days": days, "hours": list(range(24)), "matrix": matrix}


def compute_trades_by_day(closed_trades):
    """Raggruppa trade chiusi per giorno, calcola Gross/Fee/Net per ogni giorno."""
    by_day = defaultdict(lambda: {"n": 0, "wins": 0, "pnl_gross": 0.0, "fees": 0.0, "pnl_net": 0.0, "symbols": []})
    for t in closed_trades:
        dt = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        if dt is None:
            continue
        day_key = dt.astimezone().strftime("%Y-%m-%d")
        d = by_day[day_key]
        d["n"] += 1
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        if pnl_n > 0:
            d["wins"] += 1
        d["pnl_gross"] += t.get("pnl_gross", 0)
        d["fees"] += t.get("fee", 0)
        d["pnl_net"] += pnl_n
        sym = t.get("symbol", "")
        if sym and sym not in d["symbols"]:
            d["symbols"].append(sym)
    out = []
    for day_key in sorted(by_day.keys(), reverse=True):  # piu recenti prima
        d = by_day[day_key]
        out.append({
            "date": day_key,
            "n": d["n"],
            "wins": d["wins"],
            "winrate": (d["wins"] / d["n"] * 100) if d["n"] else 0,
            "pnl_gross": d["pnl_gross"],
            "fees": d["fees"],
            "pnl_net": d["pnl_net"],
            "symbols": ", ".join(d["symbols"]),
        })
    return out


def compute_top_bad_from_bybit(closed_trades, top_n=5):
    by_asset = compute_by_asset_from_bybit(closed_trades)
    return {"top": by_asset[:top_n], "bad": by_asset[-top_n:] if len(by_asset) > top_n else []}


def get_strategy_for_symbol(symbol):
    """Replica la logica di sltp_engine.get_strategy_for_symbol per le 4 strategie v2."""
    sym = symbol.upper()
    vptr3_csv = ROOT / "VPTR_V3_ASSETS.csv"
    rett_csv = ROOT / "rettangolo_assets.csv"
    rett_simple_csv = ROOT / "RETTANGOLO_SIMPLE_ASSETS.csv"
    ma_csv = ROOT / "MA_TRAILING_ASSETS.csv"
    import csv as csvmod
    for csv_path in (vptr3_csv, rett_csv, rett_simple_csv, ma_csv):
        if not csv_path.exists():
            continue
        try:
            with open(csv_path, "r", encoding="utf-8") as f:
                reader = csvmod.DictReader(f)
                for row in reader:
                    if not row.get("symbol"):
                        continue
                    if row["symbol"].strip().upper() == sym:
                        if str(row.get("enabled", "true")).lower() == "true":
                            s = row.get("strategy", "").strip().lower()
                            if s == "vptr_v3":
                                s = "vptr3"
                            return s
        except Exception:
            pass
    return "rettangolo"  # default legacy


def _fmt_usdt(v, decimals=2):
    try:
        return f"{float(v):,.{decimals}f}"
    except Exception:
        return "0.00"


def _fmt_pct(v, decimals=1):
    try:
        return f"{float(v):.{decimals}f}"
    except Exception:
        return "0.0"


def _safe_float(v, default=0.0):
    try:
        return float(v) if v is not None else default
    except Exception:
        return default


# === HTML RENDER (server-side, niente template engine) ===
def render_dashboard(data):
    kpis = data["kpis"]
    equity = data["equity"]
    by_strategy = data["by_strategy"]
    by_asset = data["by_asset"]
    top = data["top_bad"]["top"]
    bad = data["top_bad"]["bad"]
    heatmap = data["heatmap"]
    positions = data["positions"]
    recent = data["recent_orders"]
    generated_at = data["generated_at"]

    # KPI
    pnl_class = "good" if kpis["total_pnl_net"] >= 0 else "bad"
    pgross_class = "good" if kpis["total_pnl_gross"] >= 0 else "bad"
    fees_class = "warn" if kpis["total_fees"] > 0 else ""
    upnl_class = "good" if kpis["unrealized_pnl"] >= 0 else "bad"
    tot_class = "good" if kpis["total_pnl_with_unrealized"] >= 0 else "bad"
    wr_class = "good" if kpis["winrate"] >= 50 else ("bad" if kpis["winrate"] < 40 else "")
    avg_class = "good" if kpis["avg_pnl"] >= 0 else "bad"

    kpi_html = f"""
  <div class="kpi"><div class="kpi-label">Balance</div><div class="kpi-value">{_fmt_usdt(kpis['balance'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Trade chiusi</div><div class="kpi-value">{kpis['n_trades']}</div></div>
  <div class="kpi"><div class="kpi-label">Win rate</div><div class="kpi-value {wr_class}">{_fmt_pct(kpis['winrate'])}%</div></div>
  <div class="kpi"><div class="kpi-label">Gross PnL</div><div class="kpi-value {pgross_class}">{_fmt_usdt(kpis['total_pnl_gross'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Fee totali</div><div class="kpi-value {fees_class}">-{_fmt_usdt(kpis['total_fees'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Net PnL</div><div class="kpi-value {pnl_class}">{_fmt_usdt(kpis['total_pnl_net'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Avg trade</div><div class="kpi-value {avg_class}">{_fmt_usdt(kpis['avg_pnl'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Best trade</div><div class="kpi-value good">{_fmt_usdt(kpis['best_trade'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Worst trade</div><div class="kpi-value bad">{_fmt_usdt(kpis['worst_trade'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Unrealized PnL</div><div class="kpi-value {upnl_class}">{_fmt_usdt(kpis['unrealized_pnl'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">PnL Totale</div><div class="kpi-value {tot_class}">{_fmt_usdt(kpis['total_pnl_with_unrealized'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Posizioni aperte</div><div class="kpi-value">{kpis['open_positions']}</div></div>
"""

    # Equity chart
    if equity["trades_count"] > 0:
        # SVG inline (no plotly): polyline semplice
        eq_pts = equity["equity"]
        eq_dates = equity["dates"]
        if len(eq_pts) >= 2:
            min_v, max_v = min(eq_pts), max(eq_pts)
            rng = max(max_v - min_v, 0.01)
            # Normalizza in 0-100 per SVG
            w, h = 800, 200
            pad = 10
            points = []
            for i, v in enumerate(eq_pts):
                x = pad + (i / (len(eq_pts) - 1)) * (w - 2 * pad)
                y = h - pad - ((v - min_v) / rng) * (h - 2 * pad)
                points.append(f"{x:.1f},{y:.1f}")
            svg_line = " ".join(points)
            last_v = eq_pts[-1]
            last_change = last_v - eq_pts[0]
            change_pct = (last_change / eq_pts[0] * 100) if eq_pts[0] else 0
            color = "#3fb950" if last_change >= 0 else "#f85149"
            equity_html = f'''<div style="background:#0d1117;padding:10px;border-radius:6px;">
  <svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:200px;">
    <polyline points="{svg_line}" fill="none" stroke="{color}" stroke-width="2"/>
    <circle cx="{points[-1].split(",")[0]}" cy="{points[-1].split(",")[1]}" r="4" fill="{color}"/>
  </svg>
  <div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:4px;">
    <span>Initial: {eq_pts[0]:.2f} USDT</span>
    <span style="color:{color};font-weight:600;">Final: {last_v:.2f} USDT ({change_pct:+.2f}%)</span>
  </div>
  <div style="font-size:10px;color:#6e7681;margin-top:2px;">{len(eq_pts)} trade chiusi</div>
</div>'''
        else:
            # 1 solo trade
            v = eq_pts[0]
            equity_html = f'<div style="background:#0d1117;padding:20px;border-radius:6px;text-align:center;color:#8b949e;">1 trade chiuso: equity = {v:.2f} USDT</div>'
        equity_json = json.dumps(equity)
    else:
        equity_html = '<div class="empty">Nessun trade chiuso dal 1° agosto. Aspetto chiusure Pine (Max exit bars).</div>'
        equity_json = json.dumps({"dates": [], "equity": []})

    # Per strategia
    if by_strategy:
        rows = []
        for s in by_strategy:
            cls = "good" if s["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{s['strategy']}</td><td>{s['n']} ({s['wins']}W)</td><td>{_fmt_pct(s['winrate'])}%</td><td class='pnl good'>{_fmt_usdt(s['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(s['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(s['pnl'])} USDT</td></tr>")
        strategy_html = "<table><tr><th>Strategia</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th></tr>" + "".join(rows) + "</table>"
    else:
        strategy_html = '<div class="empty">Nessun trade chiuso ancora. Aspetto chiusure Pine (Max exit bars).</div>'

    # Per asset
    if by_asset:
        rows = []
        for a in by_asset:
            cls = "good" if a["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{a['symbol']}</td><td>{a['n']} ({a['wins']}W)</td><td>{_fmt_pct(a['winrate'])}%</td><td class='pnl good'>{_fmt_usdt(a['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(a['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(a['pnl'])} USDT</td></tr>")
        asset_html = "<table><tr><th>Symbol</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th></tr>" + "".join(rows) + "</table>"
    else:
        asset_html = '<div class="empty">Nessun trade chiuso ancora</div>'

    # Top 5 / Bad 5
    def _top_table(items, cls_color):
        if not items:
            return '<div class="empty">Nessun trade chiuso ancora</div>'
        rows = []
        for a in items:
            rows.append(f"<tr><td>{a['symbol']}</td><td>{a['n']}</td><td>{_fmt_pct(a['winrate'])}%</td><td class='pnl {cls_color}'>{_fmt_usdt(a['pnl'])} USDT</td></tr>")
        return "<table><tr><th>Symbol</th><th>Trades</th><th>WR</th><th>PnL</th></tr>" + "".join(rows) + "</table>"

    top_html = _top_table(top, "good")
    bad_html = _top_table(bad, "bad")

    # Heatmap (HTML puro, no Plotly CDN)
    if heatmap["matrix"] and any(sum(row) > 0 for row in heatmap["matrix"]):
        max_count = max(max(row) for row in heatmap["matrix"])
        heatmap_rows = []
        # Header (ore)
        cells = ["<td class='hm-corner'>Giorno \\\\ Ora</td>"]
        for h in heatmap["hours"]:
            cells.append(f"<td class='hm-h'>{h}</td>")
        heatmap_rows.append("<tr>" + "".join(cells) + "</tr>")
        for d_idx, day in enumerate(heatmap["days"]):
            cells = [f"<td class='hm-d'>{day}</td>"]
            for h in range(24):
                v = heatmap["matrix"][d_idx][h]
                if v == 0:
                    color = "#0d1117"
                else:
                    intensity = v / max_count if max_count > 0 else 0
                    r = int(13 + (63 - 13) * intensity)
                    g = int(17 + (185 - 17) * intensity)
                    b = int(23 + (80 - 23) * intensity)
                    color = f"rgb({r},{g},{b})"
                cells.append(f"<td class='hm-c' style='background:{color};'>{v if v else ''}</td>")
            heatmap_rows.append("<tr>" + "".join(cells) + "</tr>")
        heatmap_html = "<table class='hm'>" + "".join(heatmap_rows) + "</table>"
    else:
        heatmap_html = '<div class="empty">Nessun dato heatmap. Aspetto chiusure Pine.</div>'

    # Trade per Giorno
    _trades_by_day = data.get("trades_by_day", [])
    if _trades_by_day:
        rows = []
        for d in _trades_by_day:
            cls = "good" if d["pnl_net"] >= 0 else "bad"
            rows.append(f"<tr><td>{d['date']}</td><td>{d['n']} ({d['wins']}W)</td><td>{d['winrate']:.1f}%</td><td class='pnl good'>{_fmt_usdt(d['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(d['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(d['pnl_net'])} USDT</td><td><small>{d['symbols']}</small></td></tr>")
        # Totale
        tot_gross = sum(d['pnl_gross'] for d in _trades_by_day)
        tot_fees = sum(d['fees'] for d in _trades_by_day)
        tot_net = sum(d['pnl_net'] for d in _trades_by_day)
        tot_n = sum(d['n'] for d in _trades_by_day)
        tot_wins = sum(d['wins'] for d in _trades_by_day)
        tot_wr = (tot_wins / tot_n * 100) if tot_n else 0
        tot_cls = "good" if tot_net >= 0 else "bad"
        rows.append(f"<tr style='border-top:2px solid #58a6ff;font-weight:600;'><td>TOTALE</td><td>{tot_n} ({tot_wins}W)</td><td>{tot_wr:.1f}%</td><td class='pnl good'>{_fmt_usdt(tot_gross)}</td><td class='pnl warn'>-{_fmt_usdt(tot_fees)}</td><td class='pnl {tot_cls}'>{_fmt_usdt(tot_net)} USDT</td><td></td></tr>")
        trades_by_day_html = "<table><tr><th>Data</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th><th>Simboli</th></tr>" + "".join(rows) + "</table>"
    else:
        trades_by_day_html = '<div class="empty">Nessun trade chiuso dal 1° agosto.</div>'

    # Posizioni aperte
    if positions:
        rows = []
        for p in positions:
            cls = "good" if p["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{p['symbol']}</td><td><b>{p['strategy']}</b></td><td><span class='badge {p['side'].lower()}'>{p['side']}</span></td><td>{p['size_fmt']}</td><td>{p['entry_fmt']}</td><td>{p['mark_fmt']}</td><td>{p['sl_fmt']}</td><td class='pnl {cls}'>{p['pnl_fmt']} USDT</td><td>{p['lev']}x</td></tr>")
        positions_html = "<table><tr><th>Symbol</th><th>Strategia</th><th>Side</th><th>Size</th><th>Entry</th><th>Mark</th><th>SL</th><th>PnL</th><th>Lev</th></tr>" + "".join(rows) + "</table>"
    else:
        positions_html = '<div class="empty">Nessuna posizione aperta</div>'

    # Ordini recenti (trade chiusi Bybit, no miei test, con Gross/Fee/Net per riga)
    if recent:
        rows = []
        for o in recent:
            warn = " ⚠️placeholder" if o.get("placeholder") else ""
            pnl_gross = o.get("pnl_gross", 0)
            fee = o.get("fee", 0)
            pnl_net = o.get("pnl_net", o.get("pnl", 0))
            pnl_net_class = "good" if pnl_net > 0 else "bad" if pnl_net < 0 else ""
            rows.append(f"<tr><td>{o['created_at_fmt']}</td><td>{o['symbol']}</td><td><span class='badge {o['side']}'>{o['side']}</span></td><td>{o['qty_fmt']}</td><td>{o['price_fmt']}{warn}</td><td>{o['notional_fmt']}</td><td>{o['strategy']}</td><td class='pnl good'>{_fmt_usdt(pnl_gross)}</td><td class='pnl warn'>-{_fmt_usdt(fee)}</td><td class='pnl {pnl_net_class}'>{_fmt_usdt(pnl_net)}</td></tr>")
        if rows:
            orders_html = f"<table><tr><th>Quando</th><th>Symbol</th><th>Side</th><th>Qty</th><th>Price</th><th>Notional</th><th>Strategy</th><th>Gross</th><th>Fee</th><th>Net</th></tr><tr><td colspan='10' style='text-align:center;color:#6e7681;font-size:11px;padding:4px;'>Trade chiusi Bybit (no miei test). {len(rows)} trade mostrati.</td></tr>" + "".join(rows) + "</table>"
        else:
            orders_html = '<div class="empty">Nessun trade chiuso reale (solo miei test)</div>'
    else:
        orders_html = '<div class="empty">Nessun trade chiuso</div>'

    # HTML finale
    html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>v2 Stats Dashboard — 2° Account</title>
<meta http-equiv="refresh" content="30">
<!-- Plotly rimosso: tutto render server-side -->
<style>
* {{ box-sizing: border-box; }}
body {{ background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; }}
h1 {{ color: #58a6ff; margin: 0 0 8px 0; }}
h2 {{ color: #58a6ff; margin: 20px 0 10px 0; font-size: 16px; }}
.kpi-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 20px; }}
.kpi {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 14px; }}
.kpi-label {{ color: #8b949e; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; }}
.kpi-value {{ color: #f0f6fc; font-size: 22px; font-weight: 600; }}
.kpi-value.good {{ color: #3fb950; }}
.kpi-value.bad {{ color: #f85149; }}
.section {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 20px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ text-align: left; padding: 8px 12px; border-bottom: 1px solid #21262d; }}
th {{ color: #8b949e; font-size: 11px; text-transform: uppercase; font-weight: 600; }}
tr.good td.pnl {{ color: #3fb950; }}
tr.bad td.pnl {{ color: #f85149; }}
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }}
.badge.buy {{ background: rgba(63, 185, 80, 0.2); color: #3fb950; }}
.badge.sell {{ background: rgba(248, 81, 73, 0.2); color: #f85149; }}
.empty {{ color: #6e7681; text-align: center; padding: 30px; font-style: italic; }}
.footer {{ text-align: center; color: #6e7681; font-size: 12px; margin-top: 30px; }}
.grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
table.hm {{ border-collapse: collapse; font-size: 10px; width: 100%; }}
table.hm td {{ padding: 2px; text-align: center; min-width: 18px; height: 18px; color: #fff; }}
table.hm td.hm-corner, table.hm td.hm-h, table.hm td.hm-d {{ background: #161b22; color: #8b949e; font-weight: 600; }}
table.hm td.hm-c {{ color: #0d1117; font-weight: 600; }}
</style>
</head>
<body>
<h1>📊 v2 Stats Dashboard — 2° Account <span style="font-size: 14px; color: #6e7681;">(U05gSwYlB5)</span></h1>
<div class="footer">Auto-refresh 30s • Generato: {generated_at} • <a href="/json" style="color:#58a6ff;">/json</a> per API</div>
<div class="kpi-grid">{kpi_html}</div>
<div class="section">
  <h2>📈 Equity Curve</h2>
  {equity_html}
</div>
<div class="grid-2">
  <div class="section"><h2>📊 Per Strategia</h2>{strategy_html}</div>
  <div class="section"><h2>💎 Per Asset</h2>{asset_html}</div>
</div>
<div class="grid-2">
  <div class="section"><h2>🏆 Top 5 Best</h2>{top_html}</div>
  <div class="section"><h2>💀 Top 5 Worst</h2>{bad_html}</div>
</div>
<div class="section">
  <h2>🔥 Heatmap Giorno × Ora (chiusure trade)</h2>
  {heatmap_html}
</div>

<div class="section">
  <h2>📅 Trade per Giorno</h2>
  {trades_by_day_html}
</div>
<div class="section">
  <h2>📋 Posizioni Aperte (live Bybit)</h2>
  {positions_html}
</div>
<div class="section">
  <h2>📜 Ordini Recenti (ultimi 20)</h2>
  {orders_html}
</div>
<div class="footer">v2_stats_dashboard.py — 2° account Bybit (U05gSwYlB5)</div>
<script>
// Equity: render server-side in HTML (vedi sopra)
// Heatmap e Equity: render server-side in HTML (no Plotly CDN necessario)
</script>
</body>
</html>"""
    return html


# === DATA ASSEMBLY ===
def assemble_data():
    """Dati SOLO da Bybit (no webhook DB orders). Trade chiusi + posizioni aperte,
    filtrati dal 1° agosto ed escludendo MIEI test Pine-style."""
    closed_trades = get_closed_trades_bybit()
    positions = get_open_positions_bybit_no_test()
    balance = get_bybit_balance()
    queue_stats = get_queue_stats()

    kpis = compute_kpis_from_bybit(closed_trades, positions, balance)
    equity = compute_equity_curve_from_bybit(closed_trades, balance)
    by_strategy = compute_by_strategy_from_bybit(closed_trades)
    by_asset = compute_by_asset_from_bybit(closed_trades)
    top_bad = compute_top_bad_from_bybit(closed_trades, top_n=5)
    heatmap = compute_heatmap_from_bybit(closed_trades)
    trades_by_day = compute_trades_by_day(closed_trades)

    positions_fmt = []
    for p in positions:
        size = _safe_float(p.get("size"))
        entry = _safe_float(p.get("avgPrice"))
        mark = _safe_float(p.get("markPrice"))
        sl = _safe_float(p.get("stopLoss"))
        pnl = _safe_float(p.get("unrealisedPnl"))
        lev = _safe_float(p.get("leverage"), 1)
        sym = p.get("symbol", "")
        strat = get_strategy_for_symbol(sym)
        strat_labels = {
            "vptr3": "VPTR3", "rettangolo": "RETTANGOLO",
            "rettangolo_simple": "RETT_SIMPLE", "ma_trailing": "MA_TRAILING",
        }
        strat_label = strat_labels.get(strat, strat.upper())
        positions_fmt.append({
            "symbol": sym,
            "strategy": strat_label,
            "side": p.get("side", ""),
            "size": size, "size_fmt": f"{size:,.4f}",
            "entry": entry, "entry_fmt": f"{entry:,.5f}",
            "mark": mark, "mark_fmt": f"{mark:,.5f}",
            "sl": sl, "sl_fmt": f"{sl:,.5f}" if sl > 0 else "—",
            "pnl": pnl, "pnl_fmt": f"{pnl:,.2f}",
            "lev": f"{lev:.0f}" if lev > 0 else "—",
        })

    # Trade chiusi recenti (gia' filtrati per data e test) - max 20
    recent = []
    for t in closed_trades[:20]:
        _ts = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        recent.append({
            "created_at_fmt": _ts.strftime("%Y-%m-%d %H:%M") if _ts else "",
            "symbol": t.get("symbol", ""),
            "side": t.get("side", ""),
            "qty_fmt": f"{t.get('qty', 0):,.4f}",
            "price_fmt": f"{t.get('entry_price', 0):,.5f}",
            "notional_fmt": f"{t.get('qty', 0) * t.get('entry_price', 0):,.2f}",
            "strategy": t.get("strategy", ""),
            "pnl_gross": t.get("pnl_gross", 0),
            "fee": t.get("fee", 0),
            "pnl_net": t.get("pnl_net", t.get("pnl", 0)),
            "pnl": t.get("pnl_net", t.get("pnl", 0)),  # alias
            "placeholder": False,
            "is_real": True,
            "is_test": False,
        })

    return {
        "kpis": kpis,
        "equity": equity,
        "by_strategy": by_strategy,
        "by_asset": by_asset,
        "top_bad": top_bad,
        "heatmap": heatmap,
        "trades_by_day": trades_by_day,
        "positions": positions_fmt,
        "recent_orders": recent,
        "queue_stats": queue_stats,
        "generated_at": datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S"),
    }


# === HTTP HANDLER ===
class StatsHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass

    def _check_auth(self):
        if not AUTH_PASS:
            return True
        auth = self.headers.get("Authorization", "")
        if not auth.startswith("Basic "):
            return False
        try:
            import base64
            decoded = base64.b64decode(auth[6:]).decode("utf-8", errors="ignore")
            if ":" not in decoded:
                return False
            u, p = decoded.split(":", 1)
            return hmac.compare_digest(u, AUTH_USER) and hmac.compare_digest(p, AUTH_PASS)
        except Exception:
            return False

    def _send_unauthorized(self):
        self.send_response(401)
        self.send_header("WWW-Authenticate", 'Basic realm="v2-stats"')
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"401 Unauthorized")

    def _send_json(self, code, data):
        body = json.dumps(data, default=str, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _send_html(self, code, body):
        body_bytes = body.encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body_bytes)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body_bytes)

    def do_GET(self):
        if not self._check_auth():
            return self._send_unauthorized()
        path = urlparse(self.path).path.rstrip("/")
        try:
            data = assemble_data()
        except Exception as e:
            import traceback
            return self._send_html(500, f"<h1>500</h1><pre>{traceback.format_exc()}</pre>")
        if path in ("", "/", "/index"):
            return self._send_html(200, render_dashboard(data))
        elif path == "/json":
            return self._send_json(200, data)
        elif path == "/healthz":
            return self._send_json(200, {"status": "ok", "service": "v2-stats-dashboard"})
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b"404")


def main():
    log(f"v2 stats dashboard avviato su {LISTEN_HOST}:{PORT}")
    log(f"DB: {DB_PATH}")
    log(f"Auth: {'abilitata' if AUTH_PASS else 'disabilitata (no password settata)'}")
    httpd = HTTPServer((LISTEN_HOST, PORT), StatsHandler)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        httpd.server_close()


if __name__ == "__main__":
    main()
