"""
momentum_dashboard.py — Live momentum screener dashboard (Finviz Elite)

Screen: price $1-$20, RVOL >= 3x, 30d avg volume >= 300K, market cap
$20M-$10B, change >= 15% from prior close, float 2M-30M shares,
US exchanges only (NYSE, NASDAQ, AMEX, CBOE).

Usage:
    python3 momentum_dashboard.py            # fetch once, write HTML, print table
    python3 momentum_dashboard.py --watch    # refresh every 60s during market hours,
                                              # serve at http://localhost:8787
"""

import csv
import io
import os
import sys
import threading
import time
from datetime import datetime
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer

from dotenv import load_dotenv

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from finviz_alert import FinvizSession, _esc, is_active_session, now_et, send_telegram  # noqa: E402

load_dotenv()

FINVIZ_EMAIL = os.getenv("FINVIZ_EMAIL")
FINVIZ_PASSWORD = os.getenv("FINVIZ_PASSWORD")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")

REFRESH_SECONDS = 60
PORT = 8787
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HTML_PATH = os.path.join(SCRIPT_DIR, "momentum_dashboard.html")

# Server-side Finviz filters (coarse pre-filter; exact bounds re-checked client-side)
FILTERS = "sh_price_o1,sh_price_u20,sh_relvol_o3,sh_avgvol_o300,ta_change_u15"
COLUMNS = "1,2,6,25,63,64,65,66,67,129"  # Ticker,Company,MktCap,Float,AvgVol,RVOL,Price,Change,Volume,Exchange

US_EXCHANGES = {"NYSE", "NASD", "AMEX", "CBOE"}

PRICE_MIN, PRICE_MAX = 1.0, 20.0
RVOL_MIN = 3.0
AVGVOL_MIN = 300          # thousands of shares -> 300,000
CAP_MIN, CAP_MAX = 10, 10_000      # $ millions -> $10M-$10B
CHANGE_MIN = 15.0         # percent
FLOAT_MIN, FLOAT_MAX = 1, 20       # millions of shares -> 1M-20M


def _num(s):
    s = (s or "").strip()
    if not s:
        return None
    try:
        return float(s.replace(",", "").replace("%", ""))
    except ValueError:
        return None


def fetch_momentum(fv: FinvizSession) -> list[dict]:
    if not fv._logged_in:
        fv.login()

    for attempt in range(2):
        resp = fv.session.get(
            fv.EXPORT_URL,
            params={"v": "152", "f": FILTERS, "ft": "4", "c": COLUMNS},
            timeout=20,
        )
        if resp.status_code in (401, 403) and attempt == 0:
            fv._logged_in = False
            fv.login()
            continue
        resp.raise_for_status()
        break

    reader = csv.DictReader(io.StringIO(resp.text))
    results = []
    for row in reader:
        ticker = (row.get("Ticker") or "").strip().upper()
        if not ticker:
            continue

        price = _num(row.get("Price"))
        change = _num(row.get("Change"))
        rvol = _num(row.get("Relative Volume"))
        avgvol = _num(row.get("Average Volume"))
        cap = _num(row.get("Market Cap"))
        float_m = _num(row.get("Shares Float"))
        volume = _num(row.get("Volume"))
        exch = (row.get("Exchange") or "").strip().upper()

        if None in (price, change, rvol, avgvol, cap, float_m, volume):
            continue
        if not (PRICE_MIN <= price <= PRICE_MAX):
            continue
        if rvol < RVOL_MIN:
            continue
        if avgvol < AVGVOL_MIN:
            continue
        if change < CHANGE_MIN:
            continue
        if not (CAP_MIN <= cap <= CAP_MAX):
            continue
        if not (FLOAT_MIN <= float_m <= FLOAT_MAX):
            continue
        if exch not in US_EXCHANGES:
            continue

        results.append({
            "ticker": ticker,
            "company": (row.get("Company") or "").strip(),
            "price": price,
            "change": change,
            "rvol": rvol,
            "avgvol": avgvol,
            "cap": cap,
            "float": float_m,
            "volume": volume,
            "exch": exch,
        })

    results.sort(key=lambda r: r["rvol"], reverse=True)
    return results


# ── Rendering ─────────────────────────────────────────────────────────────────

def _fmt_millions(v: float) -> str:
    if v >= 1000:
        return f"{v / 1000:.2f}B"
    return f"{v:.1f}M"


def render_html(rows: list[dict]) -> str:
    updated = now_et().strftime("%Y-%m-%d %H:%M:%S ET")
    body_rows = "\n".join(
        f"""<tr>
            <td class="tk"><a href="https://finviz.com/quote.ashx?t={r['ticker']}" target="_blank">{r['ticker']}</a></td>
            <td class="co">{r['company']}</td>
            <td>{r['exch']}</td>
            <td>${r['price']:.2f}</td>
            <td class="pos">+{r['change']:.2f}%</td>
            <td class="rvol">{r['rvol']:.2f}x</td>
            <td>{int(r['volume']):,}</td>
            <td>{r['avgvol'] * 1000:,.0f}</td>
            <td>${_fmt_millions(r['cap'])}</td>
            <td>{_fmt_millions(r['float'])}</td>
        </tr>"""
        for r in rows
    )
    return f"""<!doctype html>
<html><head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="{REFRESH_SECONDS}">
<title>Momentum Dashboard</title>
<style>
  body {{ background:#0b0e11; color:#e6e6e6; font-family: -apple-system, Helvetica, Arial, sans-serif; margin:0; padding:24px; }}
  h1 {{ font-size:20px; margin:0 0 4px; }}
  .meta {{ color:#8a8f98; font-size:13px; margin-bottom:16px; }}
  table {{ border-collapse:collapse; width:100%; font-size:14px; }}
  th, td {{ padding:8px 12px; text-align:right; border-bottom:1px solid #1f242b; white-space:nowrap; }}
  th {{ text-align:right; color:#8a8f98; font-weight:600; position:sticky; top:0; background:#0b0e11; }}
  td.tk, th:first-child {{ text-align:left; }}
  td.co {{ text-align:left; color:#8a8f98; max-width:220px; overflow:hidden; text-overflow:ellipsis; }}
  td.tk a {{ color:#4da3ff; text-decoration:none; font-weight:700; }}
  td.pos {{ color:#3ddc84; font-weight:600; }}
  td.rvol {{ color:#ffb454; font-weight:600; }}
  tr:hover {{ background:#12161b; }}
  .empty {{ color:#8a8f98; padding:40px 0; text-align:center; }}
</style>
</head>
<body>
  <h1>Momentum Dashboard</h1>
  <div class="meta">
    Updated {updated} &nbsp;·&nbsp; {len(rows)} match(es) &nbsp;·&nbsp;
    $1-$20 · RVOL&ge;3x · AvgVol&ge;300K · Cap $10M-$10B · Change&ge;15% · Float 1M-20M · NYSE/NASDAQ/AMEX/CBOE
  </div>
  {"<table><thead><tr><th>Ticker</th><th>Company</th><th>Exch</th><th>Price</th><th>Change</th><th>RVOL</th><th>Volume</th><th>Avg Vol</th><th>Mkt Cap</th><th>Float</th></tr></thead><tbody>" + body_rows + "</tbody></table>" if rows else '<div class="empty">No matches this scan.</div>'}
</body></html>"""


def format_telegram_alert(r: dict) -> str:
    return (
        f"🚀 <b>MOMENTUM — {r['ticker']}</b>\n"
        f"{_esc(r['company'])}\n"
        f"💰 Price:   <b>${r['price']:.2f}</b>\n"
        f"📈 Change:  <b>+{r['change']:.2f}%</b>\n"
        f"⚡ RVOL:    {r['rvol']:.2f}x\n"
        f"📊 Volume:  {int(r['volume']):,}  (avg {r['avgvol'] * 1000:,.0f})\n"
        f"🏢 Cap: ${_fmt_millions(r['cap'])}   Float: {_fmt_millions(r['float'])}\n"
        f"🏦 {r['exch']}\n"
        f"🔗 <a href='https://finviz.com/quote.ashx?t={r['ticker']}'>Finviz chart</a>"
    )


def print_table(rows: list[dict]):
    if not rows:
        print("No matches.")
        return
    hdr = f"{'TICKER':<7}{'PRICE':>8}{'CHG%':>8}{'RVOL':>7}{'AVGVOL':>10}{'CAP':>9}{'FLOAT':>8}  EXCH"
    print(hdr)
    print("-" * len(hdr))
    for r in rows:
        print(
            f"{r['ticker']:<7}{r['price']:>8.2f}{r['change']:>7.2f}%{r['rvol']:>6.2f}x"
            f"{r['avgvol'] * 1000:>10,.0f}{_fmt_millions(r['cap']):>9}{_fmt_millions(r['float']):>8}  {r['exch']}"
        )


def scan_and_write(fv: FinvizSession):
    rows = fetch_momentum(fv)
    html = render_html(rows)
    with open(HTML_PATH, "w") as f:
        f.write(html)
    return rows


# ── Local server ──────────────────────────────────────────────────────────────

def serve_forever():
    handler = partial(SimpleHTTPRequestHandler, directory=SCRIPT_DIR)
    httpd = ThreadingHTTPServer(("0.0.0.0", PORT), handler)
    threading.Thread(target=httpd.serve_forever, daemon=True).start()
    print(f"Serving dashboard at http://localhost:{PORT}/momentum_dashboard.html (and on your LAN IP)")


def watch():
    if not all([FINVIZ_EMAIL, FINVIZ_PASSWORD]):
        raise EnvironmentError("FINVIZ_EMAIL and FINVIZ_PASSWORD must be set in .env")
    telegram_enabled = all([TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID])
    if not telegram_enabled:
        print("TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID not set — running without Telegram alerts.")

    fv = FinvizSession(FINVIZ_EMAIL, FINVIZ_PASSWORD)
    serve_forever()

    alerted_today: set[str] = set()  # each ticker alerted once per calendar day
    last_reset_date = None

    while True:
        today = now_et().date()
        if last_reset_date != today:
            alerted_today = set()
            last_reset_date = today

        if not is_active_session():
            print(f"[{now_et():%H:%M:%S}] Outside session — writing empty state, sleeping 5 min.")
            with open(HTML_PATH, "w") as f:
                f.write(render_html([]))
            time.sleep(300)
            continue

        try:
            rows = scan_and_write(fv)
            print(f"[{now_et():%H:%M:%S}] {len(rows)} match(es).")

            if telegram_enabled:
                new_rows = [r for r in rows if r["ticker"] not in alerted_today]
                for r in new_rows:
                    try:
                        send_telegram(TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, format_telegram_alert(r))
                        alerted_today.add(r["ticker"])
                        print(f"  ✓ Telegram alert sent: {r['ticker']}")
                    except Exception as exc:
                        print(f"  ✗ Telegram send failed for {r['ticker']}: {exc}")
        except Exception as exc:
            print(f"[{now_et():%H:%M:%S}] Scan failed: {exc}")
        time.sleep(REFRESH_SECONDS)


if __name__ == "__main__":
    if "--watch" in sys.argv:
        try:
            watch()
        except KeyboardInterrupt:
            print("Stopped.")
    else:
        if not all([FINVIZ_EMAIL, FINVIZ_PASSWORD]):
            raise EnvironmentError("FINVIZ_EMAIL and FINVIZ_PASSWORD must be set in .env")
        fv = FinvizSession(FINVIZ_EMAIL, FINVIZ_PASSWORD)
        rows = scan_and_write(fv)
        print_table(rows)
        print(f"\nWrote {HTML_PATH}")
