"""
Export an interactive orderbook replay viewer for one recorded LPL/LOL market.

Example:
    python3 scripts/export_lpl_orderbook_viewer.py \
      --slug lol-tes-al-2026-05-01-game2 \
      --depth 12
"""
from __future__ import annotations

import html
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import click
from loguru import logger

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
    sys.path.insert(0, ROOT)

from analysis.safe_btc5.types import parse_dt  # noqa: E402


def _parse_time(value: Any) -> datetime | None:
    parsed = parse_dt(value)
    if parsed is None:
        return None
    return parsed.astimezone(timezone.utc)


def _levels(rows: Any, *, depth: int, reverse: bool) -> list[dict[str, float]]:
    if not isinstance(rows, list):
        return []
    levels: list[dict[str, float]] = []
    for row in rows:
        try:
            price = float(row["price"])
            size = float(row["size"])
        except (KeyError, TypeError, ValueError):
            continue
        if size <= 0:
            continue
        levels.append({"price": price, "size": size})
    levels.sort(key=lambda item: item["price"], reverse=reverse)
    return levels[:depth]


def _load_last_trades(events_path: Path) -> list[dict[str, Any]]:
    trades: list[dict[str, Any]] = []
    with events_path.open("r", encoding="utf-8") as f:
        for line in f:
            if not line.strip():
                continue
            row = json.loads(line)
            if row.get("event_type") != "last_trade_price":
                continue
            ts = _parse_time(row.get("received_at_wall"))
            raw = row.get("raw_json") or {}
            if ts is None:
                continue
            try:
                price = float(raw.get("price"))
                size = float(raw.get("size") or 0)
            except (TypeError, ValueError):
                continue
            trades.append(
                {
                    "ts": ts,
                    "asset_id": str(row.get("asset_id") or raw.get("asset_id") or ""),
                    "price": price,
                    "size": size,
                    "side": str(raw.get("side") or ""),
                }
            )
    trades.sort(key=lambda item: item["ts"])
    return trades


def export_orderbook_viewer(
    *,
    slug: str,
    data_root: Path,
    output_path: Path | None,
    depth: int,
    frame_stride: int,
) -> dict[str, Any]:
    market_dir = data_root / slug
    meta_path = market_dir / "recording_meta.json"
    snapshots_path = market_dir / "orderbook_snapshots.jsonl"
    events_path = market_dir / "orderbook_events.jsonl"
    if not meta_path.exists():
        raise FileNotFoundError(meta_path)
    if not snapshots_path.exists():
        raise FileNotFoundError(snapshots_path)
    if not events_path.exists():
        raise FileNotFoundError(events_path)

    meta = json.loads(meta_path.read_text(encoding="utf-8"))
    token_ids = [str(item) for item in meta.get("token_ids", [])]
    outcomes = [str(item) for item in meta.get("outcomes", [])]
    token_to_outcome = {
        token_id: outcomes[idx] if idx < len(outcomes) else token_id
        for idx, token_id in enumerate(token_ids)
    }

    trades = _load_last_trades(events_path)
    trade_index = 0
    last_trade_by_asset: dict[str, dict[str, Any]] = {}
    latest_book: dict[str, dict[str, Any]] = {}
    frames: list[dict[str, Any]] = []
    candidate_frame = 0

    with snapshots_path.open("r", encoding="utf-8") as f:
        for line in f:
            if not line.strip():
                continue
            row = json.loads(line)
            ts = _parse_time(row.get("snapshot_at_wall"))
            asset_id = str(row.get("asset_id") or "")
            if ts is None or not asset_id:
                continue

            while trade_index < len(trades) and trades[trade_index]["ts"] <= ts:
                trade = trades[trade_index]
                last_trade_by_asset[trade["asset_id"]] = {
                    "price": trade["price"],
                    "size": trade["size"],
                    "side": trade["side"],
                    "ts": trade["ts"].isoformat(),
                }
                trade_index += 1

            bid_levels = _levels(row.get("bid_levels"), depth=depth, reverse=True)
            ask_levels = _levels(row.get("ask_levels"), depth=depth, reverse=False)
            latest_book[asset_id] = {
                "asset_id": asset_id,
                "outcome": str(row.get("outcome") or token_to_outcome.get(asset_id, "")),
                "best_bid": row.get("best_bid"),
                "best_ask": row.get("best_ask"),
                "bids": bid_levels,
                "asks": ask_levels,
                "last_trade": last_trade_by_asset.get(asset_id),
            }

            if not token_ids or not all(token_id in latest_book for token_id in token_ids):
                continue
            candidate_frame += 1
            if candidate_frame % max(1, frame_stride) != 0:
                continue

            books = {
                token_id: json.loads(json.dumps(latest_book[token_id]))
                for token_id in token_ids
            }
            frames.append(
                {
                    "t": ts.isoformat(),
                    "message_index": row.get("message_index"),
                    "books": books,
                }
            )

    if output_path is None:
        output_path = market_dir / "orderbook_replay.html"
    output_path.parent.mkdir(parents=True, exist_ok=True)

    payload = {
        "meta": meta,
        "token_to_outcome": token_to_outcome,
        "trades": [
            {
                "ts": trade["ts"].isoformat(),
                "asset_id": trade["asset_id"],
                "outcome": token_to_outcome.get(trade["asset_id"], trade["asset_id"]),
                "price": trade["price"],
                "size": trade["size"],
                "side": trade["side"],
            }
            for trade in trades
        ],
        "frames": frames,
        "depth": depth,
        "frame_stride": frame_stride,
        "generated_at": datetime.now(timezone.utc).isoformat(),
    }
    output_path.write_text(_render_html(payload), encoding="utf-8")
    return {
        "slug": slug,
        "frames": len(frames),
        "output_path": str(output_path),
        "started_at": frames[0]["t"] if frames else "",
        "ended_at": frames[-1]["t"] if frames else "",
    }


def _render_html(payload: dict[str, Any]) -> str:
    data = (
        json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
        .replace("</", "<\\/")
        .replace("\u2028", "\\u2028")
        .replace("\u2029", "\\u2029")
    )
    title = html.escape(str(payload.get("meta", {}).get("question") or "Orderbook Replay"))
    template = """<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>__TITLE__</title>
  <style>
    :root {
      color-scheme: dark;
      --bg: #0f1419;
      --panel: #151b21;
      --line: #28313a;
      --muted: #8b98a6;
      --text: #f1f4f7;
      --red: #ef4444;
      --red-bg: rgba(239, 68, 68, 0.13);
      --green: #35c978;
      --green-bg: rgba(53, 201, 120, 0.13);
      --accent: #8ab4ff;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }
    * { box-sizing: border-box; }
    body { margin: 0; overflow: hidden; background: var(--bg); color: var(--text); }
    .app { width: min(1600px, 100vw); height: 100vh; margin: 0 auto; padding: 8px; display: grid; grid-template-rows: auto 1fr auto; gap: 8px; }
    .topbar { min-height: 32px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; }
    h1 { margin: 0; font-size: 15px; font-weight: 750; letter-spacing: 0; }
    .meta { color: var(--muted); font-size: 11px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .top-stats { display: flex; gap: 14px; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
    .teams { min-height: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    .team { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 8px; }
    .team-title { height: 30px; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 10px; padding: 0 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
    .team-title strong { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; }
    .team-title span { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
    .book, .trades { min-height: 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
    .book-head, .row, .mid { display: grid; grid-template-columns: 66px 82px 104px 116px; justify-content: end; align-items: center; }
    .book-head { height: 28px; color: var(--muted); border-bottom: 1px solid var(--line); font-size: 10px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; }
    .book-head div:first-child, .row div:first-child { padding-left: 8px; }
    .row { position: relative; min-height: 30px; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.02); }
    .bar { position: absolute; inset: 0 auto 0 0; opacity: 0.85; pointer-events: none; }
    .ask .bar { background: var(--red-bg); }
    .bid .bar { background: var(--green-bg); }
    .price { font-size: 14px; font-weight: 800; }
    .ask .price { color: var(--red); }
    .bid .price { color: var(--green); }
    .num { text-align: right; padding-right: 10px; font-variant-numeric: tabular-nums; }
    .mid { min-height: 34px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); color: var(--muted); font-size: 11px; font-weight: 750; }
    .pill { display: inline-flex; width: max-content; align-items: center; border-radius: 5px; padding: 4px 7px; color: white; font-size: 11px; font-weight: 800; }
    .pill.ask { background: #dc2626; }
    .pill.bid { background: #2ca461; }
    .side-label { padding-left: 8px; }
    .trades { display: grid; grid-template-rows: auto 1fr; }
    .trade-head { height: 28px; display: grid; grid-template-columns: 128px 44px 58px 76px; gap: 6px; align-items: center; padding: 0 8px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 10px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; }
    .trade-list { min-height: 0; overflow: hidden; }
    .trade-row { display: grid; grid-template-columns: 128px 44px 58px 76px; gap: 6px; align-items: center; min-height: 26px; padding: 0 8px; border-bottom: 1px solid rgba(255,255,255,0.025); font-size: 11px; font-variant-numeric: tabular-nums; }
    .trade-row .time { color: var(--muted); white-space: nowrap; overflow: hidden; }
    .trade-row .side { color: var(--muted); font-weight: 750; }
    .trade-row.buy .trade-price { color: var(--green); font-weight: 800; }
    .trade-row.sell .trade-price { color: var(--red); font-weight: 800; }
    .trade-row .trade-size { text-align: right; }
    .controls { display: grid; grid-template-columns: 86px 74px 74px 1fr; gap: 8px; align-items: center; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 7px; }
    button, select { background: #202832; color: var(--text); border: 1px solid #34414d; border-radius: 6px; padding: 5px 8px; font-size: 12px; height: 28px; }
    button { cursor: pointer; }
    input[type="range"] { width: 100%; accent-color: var(--accent); }
    @media (max-width: 1000px) {
      body { overflow: auto; }
      .app { height: auto; }
      .teams { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
<script id="replay-data" type="application/json">__DATA__</script>
<div class="app">
  <div class="topbar">
    <div>
      <h1>Order Book Replay</h1>
      <div class="meta" id="question"></div>
    </div>
    <div class="top-stats">
      <span id="clock"></span>
      <span id="frameNo"></span>
    </div>
  </div>
  <div class="teams" id="teams"></div>
  <div class="controls">
    <select id="speedSelect">
      <option value="0.5">0.5x</option>
      <option value="1">1x</option>
      <option value="2" selected>2x</option>
      <option value="5">5x</option>
      <option value="10">10x</option>
    </select>
    <button id="playBtn">Play</button>
    <button id="stepBtn">Step</button>
    <input id="slider" type="range" min="0" max="0" value="0">
  </div>
</div>
<script>
const payload = JSON.parse(document.getElementById('replay-data').textContent);
const frames = payload.frames || [];
const trades = payload.trades || [];
const meta = payload.meta || {};
const tokenIds = meta.token_ids || [];
let frameIndex = 0;
let timer = null;

const els = {
  question: document.getElementById('question'),
  speedSelect: document.getElementById('speedSelect'),
  playBtn: document.getElementById('playBtn'),
  clock: document.getElementById('clock'),
  frameNo: document.getElementById('frameNo'),
  slider: document.getElementById('slider'),
  stepBtn: document.getElementById('stepBtn'),
  teams: document.getElementById('teams')
};
const teamEls = new Map();

function cents(price) {
  if (price === null || price === undefined || price === '') return '-';
  return `${Math.round(Number(price) * 1000) / 10}c`;
}
function money(value) {
  return `$${Number(value || 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
}
function shares(value) {
  return Number(value || 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function clockTime(value) {
  const d = new Date(value);
  if (Number.isNaN(d.getTime())) return value || '';
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Shanghai',
    hour12: false,
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  }).formatToParts(d).reduce((acc, part) => {
    acc[part.type] = part.value;
    return acc;
  }, {});
  const ms = String(d.getMilliseconds()).padStart(3, '0');
  return `${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}.${ms}`;
}
function rowHtml(level, side, maxNotional, cumulative) {
  const notional = Number(level.price) * Number(level.size);
  const total = cumulative + notional;
  const width = Math.max(2, Math.min(100, (notional / Math.max(1, maxNotional)) * 100));
  return `<div class="row ${side}">
    <div class="bar" style="width:${width}%"></div>
    <div></div>
    <div class="price num">${cents(level.price)}</div>
    <div class="num">${shares(level.size)}</div>
    <div class="num">${money(total)}</div>
  </div>`;
}
function renderSide(levels, side) {
  const top = levels.slice(0, 5);
  const display = side === 'ask' ? [...top].reverse() : top;
  const maxNotional = Math.max(1, ...top.map(l => Number(l.price) * Number(l.size)));
  let cumulative = 0;
  const cumulativeBefore = [];
  for (const level of top) {
    cumulativeBefore.push(cumulative);
    cumulative += Number(level.price) * Number(level.size);
  }
  const rows = side === 'ask'
    ? top.map((level, index) => rowHtml(level, side, maxNotional, cumulativeBefore[index])).reverse()
    : top.map((level, index) => rowHtml(level, side, maxNotional, cumulativeBefore[index]));
  const html = rows.join('');
  return html || `<div class="row ${side}"><div></div><div class="num">-</div><div class="num">-</div><div class="num">-</div></div>`;
}
function recentTrades(assetId, frameTime) {
  const cutoff = new Date(frameTime).getTime();
  const recent = [];
  for (let i = trades.length - 1; i >= 0 && recent.length < 10; i -= 1) {
    const trade = trades[i];
    if (trade.asset_id === assetId && new Date(trade.ts).getTime() <= cutoff) recent.push(trade);
  }
  return recent;
}
function tradeHtml(assetId, frameTime) {
  const recent = recentTrades(assetId, frameTime);
  return recent.map(trade => {
    const side = String(trade.side || '').toLowerCase();
    return `<div class="trade-row ${side}">
      <div class="time">${clockTime(trade.ts)}</div>
      <div class="side">${trade.side || ''}</div>
      <div class="trade-price num">${cents(trade.price)}</div>
      <div class="trade-size">${shares(trade.size)}</div>
    </div>`;
  }).join('') || '<div class="trade-row"><div class="time">-</div><div>No trades yet</div><div></div><div></div></div>';
}
function makeTeam(tokenId) {
  const sample = frames.find(f => f.books && f.books[tokenId]);
  const outcome = sample ? sample.books[tokenId].outcome : tokenId;
  const wrap = document.createElement('section');
  wrap.className = 'team';
  wrap.innerHTML = `<div class="team-title">
      <strong>${outcome}</strong><span class="best"></span><span class="last-title"></span>
    </div>
    <div class="book">
      <div class="book-head"><div></div><div class="num">Price</div><div class="num">Shares</div><div class="num">Total</div></div>
      <div class="asks"></div>
      <div class="mid"><div class="side-label"><span class="pill ask">Asks</span></div><div class="last num"></div><div class="spread num"></div><div></div></div>
      <div class="bids"></div>
    </div>
    <div class="trades">
      <div class="trade-head"><div>Time</div><div>Side</div><div class="num">Price</div><div class="num">Shares</div></div>
      <div class="trade-list"></div>
    </div>`;
  els.teams.appendChild(wrap);
  teamEls.set(tokenId, {
    root: wrap,
    best: wrap.querySelector('.best'),
    lastTitle: wrap.querySelector('.last-title'),
    asks: wrap.querySelector('.asks'),
    bids: wrap.querySelector('.bids'),
    last: wrap.querySelector('.last'),
    spread: wrap.querySelector('.spread'),
    tradeList: wrap.querySelector('.trade-list')
  });
}
function renderTeam(tokenId, frame) {
  const book = frame.books[tokenId];
  const view = teamEls.get(tokenId);
  if (!book || !view) return;
  const bid = Number(book.best_bid);
  const ask = Number(book.best_ask);
  const last = book.last_trade ? cents(book.last_trade.price) : '-';
  view.asks.innerHTML = renderSide(book.asks || [], 'ask');
  view.bids.innerHTML = renderSide(book.bids || [], 'bid');
  view.last.textContent = `Last: ${last}`;
  view.lastTitle.textContent = `Last ${last}`;
  view.best.textContent = `${cents(book.best_bid)} / ${cents(book.best_ask)}`;
  view.spread.textContent = Number.isFinite(bid) && Number.isFinite(ask) ? `Spread: ${cents(ask - bid)}` : 'Spread: -';
  view.tradeList.innerHTML = tradeHtml(tokenId, frame.t);
}
function render() {
  if (!frames.length) return;
  const frame = frames[frameIndex];
  for (const tokenId of tokenIds) renderTeam(tokenId, frame);
  els.clock.textContent = clockTime(frame.t);
  els.frameNo.textContent = `${frameIndex + 1} / ${frames.length}`;
  els.slider.value = String(frameIndex);
}
function stop() {
  if (timer) clearInterval(timer);
  timer = null;
  els.playBtn.textContent = 'Play';
}
function play() {
  if (timer) return stop();
  els.playBtn.textContent = 'Pause';
  timer = setInterval(() => {
    frameIndex += 1;
    if (frameIndex >= frames.length) {
      frameIndex = frames.length - 1;
      stop();
    }
    render();
  }, Math.max(20, 200 / Number(els.speedSelect.value || 1)));
}

els.question.textContent = `${meta.slug || ''} | ${meta.question || ''}`;
for (const tokenId of tokenIds) makeTeam(tokenId);
els.slider.max = String(Math.max(0, frames.length - 1));
els.slider.addEventListener('input', () => { frameIndex = Number(els.slider.value); render(); });
els.speedSelect.addEventListener('change', () => { if (timer) { stop(); play(); } });
els.playBtn.addEventListener('click', play);
els.stepBtn.addEventListener('click', () => { frameIndex = Math.min(frames.length - 1, frameIndex + 1); render(); });
render();
</script>
</body>
</html>
"""
    return template.replace("__TITLE__", title).replace("__DATA__", data)


@click.command()
@click.option("--slug", required=True, help="Recorded Polymarket market slug")
@click.option(
    "--data-root",
    default="data/lpl",
    show_default=True,
    type=click.Path(file_okay=False, path_type=Path),
    help="Recording data root",
)
@click.option(
    "--output",
    "output_path",
    default=None,
    type=click.Path(dir_okay=False, path_type=Path),
    help="Output HTML path. Defaults to data/lpl/<slug>/orderbook_replay.html",
)
@click.option("--depth", default=5, show_default=True, type=int, help="Levels per side")
@click.option(
    "--frame-stride",
    default=1,
    show_default=True,
    type=int,
    help="Keep every Nth reconstructed snapshot frame",
)
def main(slug: str, data_root: Path, output_path: Path | None, depth: int, frame_stride: int) -> None:
    result = export_orderbook_viewer(
        slug=slug,
        data_root=data_root,
        output_path=output_path,
        depth=depth,
        frame_stride=frame_stride,
    )
    logger.info(
        "viewer exported: "
        f"frames={result['frames']} "
        f"started_at={result['started_at']} "
        f"ended_at={result['ended_at']} "
        f"output={result['output_path']}"
    )


if __name__ == "__main__":
    main()
