"""
Dashboard estatico del wrapper de arbitraje.

Cada 60s consulta SQLite y regenera dashboard.html con:
  - Resumen general
  - Distribucion por categoria
  - Top 10 oportunidades activas
  - Posiciones paper abiertas/cerradas
  - PnL post-mortem
"""
import sqlite3
import time
import signal
import sys
from datetime import datetime, timedelta
from pathlib import Path

DB_PATH = "/root/arbitrage_data/data/opportunities.db"
HTML_PATH = "/root/arbitrage_data/dashboard/dashboard.html"
REFRESH_SECONDS = 60
START_TIME = datetime.utcnow()
_running = True


def q(conn, sql, params=()):
    """Helper para queries que devuelven filas como lista de tuples."""
    cursor = conn.cursor()
    cursor.execute(sql, params)
    return cursor.fetchall()


def q_one(conn, sql, params=()):
    """Helper para una unica fila."""
    rows = q(conn, sql, params)
    return rows[0] if rows else None


def fmt_pct(v, decimals=2):
    """Format pct con signo y color HTML."""
    if v is None:
        return "—"
    try:
        v = float(v)
    except (ValueError, TypeError):
        return str(v)
    color = "#16a34a" if v > 0.01 else ("#dc2626" if v < -0.01 else "#6b7280")
    return f'<span style="color:{color}">{v:+.{decimals}f}%</span>'


def fmt_usd(v, decimals=4):
    """Format USD con color."""
    if v is None:
        return "—"
    try:
        v = float(v)
    except (ValueError, TypeError):
        return str(v)
    color = "#16a34a" if v > 0.001 else ("#dc2626" if v < -0.001 else "#6b7280")
    return f'<span style="color:{color}">{v:+.{decimals}f}</span>'


def fmt_dur(minutes):
    """Format duracion en formato humano."""
    if minutes is None:
        return "—"
    minutes = float(minutes)
    if minutes < 60:
        return f"{minutes:.0f}m"
    hours = minutes / 60
    if hours < 24:
        return f"{hours:.1f}h"
    return f"{hours/24:.1f}d"


def fmt_uptime():
    """Format uptime desde el arranque del generador."""
    delta = datetime.utcnow() - START_TIME
    total_sec = delta.total_seconds()
    if total_sec < 3600:
        return f"{int(total_sec/60)}m"
    if total_sec < 86400:
        return f"{int(total_sec/3600)}h{int((total_sec%3600)/60)}m"
    return f"{int(total_sec/86400)}d{int((total_sec%86400)/3600)}h"


HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="60">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polymarket Arb Dashboard</title>
<style>
* {{ box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
       margin: 0; padding: 20px; background: #f3f4f6; color: #111827;
       font-size: 14px; line-height: 1.4; }}
h1 {{ font-size: 22px; margin: 0 0 8px 0; }}
h2 {{ font-size: 16px; margin: 24px 0 10px 0; padding-bottom: 6px;
      border-bottom: 1px solid #e5e7eb; color: #374151; }}
.subtitle {{ color: #6b7280; font-size: 12px; margin-bottom: 20px; }}
.summary-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
                 gap: 12px; margin-bottom: 20px; }}
.metric {{ background: white; padding: 12px; border-radius: 6px;
           box-shadow: 0 1px 2px rgba(0,0,0,.05); }}
.metric .label {{ color: #6b7280; font-size: 11px; text-transform: uppercase;
                  letter-spacing: 0.5px; }}
.metric .value {{ font-size: 20px; font-weight: 600; margin-top: 4px; }}
table {{ width: 100%; border-collapse: collapse; background: white;
         border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,.05); }}
th, td {{ padding: 8px 12px; text-align: left; border-bottom: 1px solid #f3f4f6;
          font-size: 12px; }}
th {{ background: #f9fafb; color: #6b7280; font-weight: 500; text-transform: uppercase;
      font-size: 10px; letter-spacing: 0.5px; }}
tr:hover {{ background: #f9fafb; }}
.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
.muted {{ color: #9ca3af; }}
.tag {{ display: inline-block; padding: 2px 6px; border-radius: 3px;
        font-size: 10px; background: #e5e7eb; color: #374151; }}
.tag-finance {{ background: #dbeafe; color: #1e40af; }}
.tag-sports {{ background: #d1fae5; color: #065f46; }}
.tag-politics {{ background: #fef3c7; color: #92400e; }}
.tag-culture {{ background: #fce7f3; color: #9d174d; }}
.tag-tech {{ background: #ede9fe; color: #5b21b6; }}
@media (max-width: 640px) {{
  body {{ padding: 10px; font-size: 13px; }}
  .summary-grid {{ grid-template-columns: repeat(2, 1fr); }}
  table {{ font-size: 11px; }}
  th, td {{ padding: 6px 8px; }}
}}
</style>
</head>
<body>
<h1>Polymarket Arbitrage Dashboard</h1>
<p class="subtitle">Generated {generated_at} UTC · Dashboard uptime {uptime} · Auto-refresh 60s</p>

<h2>Summary</h2>
<div class="summary-grid">
{summary_metrics}
</div>

<h2>Distribution by Category (Active Opportunities)</h2>
{categories_table}

<h2>Top 10 Active Opportunities (by edge after fees)</h2>
{top_opps_table}

<h2>Paper Trader — Open Positions</h2>
{open_positions_table}

<h2>Paper Trader — Last 20 Closed Positions</h2>
{closed_positions_table}

<h2>PnL Post-mortem (Vanished opportunities)</h2>
{pnl_outcome_table}

</body>
</html>
"""


def render_metric(label, value):
    return f'<div class="metric"><div class="label">{label}</div><div class="value">{value}</div></div>'


def render_category_tag(cat):
    if not cat:
        return '<span class="tag">—</span>'
    cls = f"tag-{cat.lower()}"
    return f'<span class="tag {cls}">{cat}</span>'


def build_html(conn):
    """Construye el HTML completo del dashboard."""
    # --- Summary metrics ---
    n_scans = q_one(conn, "SELECT COUNT(*) FROM scans")[0]
    n_active = q_one(conn, "SELECT COUNT(*) FROM opportunities WHERE status='active'")[0]
    n_vanished = q_one(conn, "SELECT COUNT(*) FROM opportunities WHERE status='vanished'")[0]
    n_paper_open = q_one(conn, "SELECT COUNT(*) FROM paper_positions WHERE status='open'")[0]
    n_paper_closed = q_one(conn, "SELECT COUNT(*) FROM paper_positions WHERE status='closed'")[0]
    paper_pnl_row = q_one(conn, "SELECT SUM(pnl_usd) FROM paper_positions WHERE status='closed'")
    paper_pnl = paper_pnl_row[0] if paper_pnl_row and paper_pnl_row[0] else 0
    last_scan = q_one(conn, "SELECT scan_time FROM scans ORDER BY id DESC LIMIT 1")
    last_scan_str = last_scan[0][:19] if last_scan else "—"

    metrics = (
        render_metric("Total scans", n_scans) +
        render_metric("Active opps", n_active) +
        render_metric("Vanished opps", n_vanished) +
        render_metric("Paper open", n_paper_open) +
        render_metric("Paper closed", n_paper_closed) +
        render_metric("Paper PnL (USD)", fmt_usd(paper_pnl, 2)) +
        render_metric("Last scan", last_scan_str)
    )

    # --- Categories table ---
    cat_rows = q(conn, """
        SELECT COALESCE(category, '—') as cat, COUNT(*) as n,
               ROUND(AVG(last_edge_after_fees_pct), 2) as avg_edge,
               ROUND(MAX(last_edge_after_fees_pct), 2) as max_edge,
               ROUND(AVG(last_min_liquidity), 0) as avg_liq
        FROM opportunities WHERE status='active'
        GROUP BY cat ORDER BY n DESC
    """)
    cat_html = '<table><tr><th>Category</th><th class="num">Count</th><th class="num">Avg edge</th><th class="num">Max edge</th><th class="num">Avg liq USD</th></tr>'
    for cat, n, avg_e, max_e, avg_l in cat_rows:
        cat_html += f'<tr><td>{render_category_tag(cat)}</td><td class="num">{n}</td><td class="num">{avg_e}%</td><td class="num">{max_e}%</td><td class="num">${avg_l:.0f}</td></tr>'
    cat_html += '</table>'

    # --- Top 10 active opps ---
    top_rows = q(conn, """
        SELECT description, category, last_edge_after_fees_pct, last_min_liquidity,
               scans_count, high_fee_zone
        FROM opportunities WHERE status='active'
        ORDER BY last_edge_after_fees_pct DESC LIMIT 10
    """)
    top_html = '<table><tr><th>Description</th><th>Cat</th><th class="num">Edge</th><th class="num">Liq</th><th class="num">Scans</th><th>HFZ</th></tr>'
    for desc, cat, edge, liq, scans, hfz in top_rows:
        hfz_txt = "⚠️" if hfz else ""
        desc_short = (desc or "")[:65]
        top_html += f'<tr><td>{desc_short}</td><td>{render_category_tag(cat)}</td><td class="num">{edge:.2f}%</td><td class="num">${liq:.0f}</td><td class="num">{scans}</td><td>{hfz_txt}</td></tr>'
    top_html += '</table>' if top_rows else '<p class="muted">No active opportunities.</p>'

    # --- Open paper positions ---
    open_rows = q(conn, """
        SELECT position_id, description, category, entry_time, entry_edge_pct,
               entry_total_cost, entry_size_usd
        FROM paper_positions WHERE status='open' ORDER BY entry_time DESC LIMIT 20
    """)
    open_html = '<table><tr><th>ID</th><th>Description</th><th>Cat</th><th>Entry time</th><th class="num">Entry edge</th><th class="num">Size USD</th></tr>'
    for pid, desc, cat, entry_t, edge, cost, size in open_rows:
        open_html += f'<tr><td>{pid}</td><td>{(desc or "")[:55]}</td><td>{render_category_tag(cat)}</td><td>{entry_t[:19]}</td><td class="num">{edge:.2f}%</td><td class="num">${size:.0f}</td></tr>'
    open_html += '</table>' if open_rows else '<p class="muted">No open positions.</p>'

    # --- Closed paper positions ---
    closed_rows = q(conn, """
        SELECT position_id, description, category, entry_time, exit_time,
               exit_reason, pnl_usd, pnl_pct, entry_size_usd
        FROM paper_positions WHERE status='closed' ORDER BY exit_time DESC LIMIT 20
    """)
    closed_html = '<table><tr><th>ID</th><th>Description</th><th>Cat</th><th>Reason</th><th class="num">PnL USD</th><th class="num">PnL %</th></tr>'
    for pid, desc, cat, ent, ext, reason, pnl_u, pnl_p, size in closed_rows:
        closed_html += f'<tr><td>{pid}</td><td>{(desc or "")[:55]}</td><td>{render_category_tag(cat)}</td><td>{reason}</td><td class="num">{fmt_usd(pnl_u)}</td><td class="num">{fmt_pct(pnl_p)}</td></tr>'
    closed_html += '</table>' if closed_rows else '<p class="muted">No closed positions yet.</p>'

    # --- Pnl outcomes ---
    pnl_rows = q(conn, """
        SELECT outcome, COUNT(*) as n,
               ROUND(AVG(pnl_pct_on_cost), 3) as avg_pnl,
               ROUND(AVG(duration_minutes), 1) as avg_dur,
               ROUND(AVG(initial_edge_pct), 2) as avg_init_edge
        FROM pnl_simulations GROUP BY outcome ORDER BY n DESC
    """)
    pnl_html = '<table><tr><th>Outcome</th><th class="num">Count</th><th class="num">Avg PnL%</th><th class="num">Avg duration</th><th class="num">Avg initial edge</th></tr>'
    for outcome, n, avg_pnl, avg_dur, avg_init in pnl_rows:
        pnl_html += f'<tr><td>{outcome}</td><td class="num">{n}</td><td class="num">{fmt_pct(avg_pnl, 3)}</td><td class="num">{fmt_dur(avg_dur)}</td><td class="num">{avg_init}%</td></tr>'
    pnl_html += '</table>' if pnl_rows else '<p class="muted">No vanished opps processed yet.</p>'

    return HTML_TEMPLATE.format(
        generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
        uptime=fmt_uptime(),
        summary_metrics=metrics,
        categories_table=cat_html,
        top_opps_table=top_html,
        open_positions_table=open_html,
        closed_positions_table=closed_html,
        pnl_outcome_table=pnl_html,
    )


def shutdown(signum, frame):
    global _running
    print(f"[shutdown] Senal {signum}", flush=True)
    _running = False


def main():
    """Loop infinito: regenera HTML cada REFRESH_SECONDS."""
    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)

    Path(HTML_PATH).parent.mkdir(parents=True, exist_ok=True)
    print(f"Dashboard generator iniciado. Salida: {HTML_PATH}", flush=True)

    cycle = 0
    while _running:
        cycle += 1
        try:
            conn = sqlite3.connect(DB_PATH)
            html = build_html(conn)
            conn.close()
            tmp_path = HTML_PATH + ".tmp"
            with open(tmp_path, "w") as f:
                f.write(html)
            # Rename atomico para que el servidor nunca lea un HTML a medias
            Path(tmp_path).replace(HTML_PATH)
            print(f"[cycle {cycle}] {datetime.utcnow().isoformat()} dashboard regenerated",
                  flush=True)
        except Exception as e:
            print(f"[cycle {cycle}] ERROR: {e}", flush=True)
            import traceback
            traceback.print_exc()
        # Sleep responsivo a senales
        slept = 0
        while slept < REFRESH_SECONDS and _running:
            time.sleep(min(5, REFRESH_SECONDS - slept))
            slept += 5

    print("Dashboard generator detenido", flush=True)


if __name__ == "__main__":
    main()
