#!/usr/bin/env python3
"""Read-only web panel for the Hyperliquid V2 bot journal."""
from __future__ import annotations

import base64
import json
import os
import time
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlparse


APP_DIR = Path(__file__).resolve().parent
JOURNAL_PATH = Path(os.environ.get("JOURNAL_PATH", "/bot-data/journal.jsonl"))
PANEL_USER = os.environ.get("PANEL_USER", "hermes")
PANEL_PASSWORD = os.environ.get("PANEL_PASSWORD", "hermes1337")
PANEL_PORT = int(os.environ.get("PANEL_PORT", "8080"))


def load_rows(hours: float = 6.0, coin: str = "", event: str = "") -> list[dict[str, Any]]:
    if not JOURNAL_PATH.exists():
        return []
    since_ms = int((time.time() - hours * 3600) * 1000)
    coin = coin.upper().strip()
    event = event.strip()
    rows: list[dict[str, Any]] = []
    for line in JOURNAL_PATH.read_text(errors="replace").splitlines():
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if int(row.get("ts") or 0) < since_ms:
            continue
        if coin and str(row.get("coin") or "").upper() != coin:
            continue
        if event and str(row.get("event") or "") != event:
            continue
        rows.append(row)
    return rows


def fnum(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


def event_counts(rows: list[dict[str, Any]]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for row in rows:
        event = str(row.get("event") or "?")
        counts[event] = counts.get(event, 0) + 1
    return counts


def latest(rows: list[dict[str, Any]], event: str) -> dict[str, Any] | None:
    for row in reversed(rows):
        if row.get("event") == event:
            return row
    return None


def pct(part: float, whole: float) -> float:
    return part / whole * 100 if whole else 0.0


def enrich(row: dict[str, Any], *, include_market: bool = False) -> dict[str, Any]:
    out = dict(row)
    if not include_market and "market_summary" in out:
        out["market_summary_omitted"] = len(out.get("market_summary") or [])
        out.pop("market_summary", None)

    if row.get("event") == "submit":
        entry = fnum(row.get("entry"))
        stop = fnum(row.get("stop"))
        target = fnum(row.get("target"))
        stop_dist = abs(entry - stop)
        reward_dist = abs(target - entry)
        metrics = row.get("exit_plan_metrics") if isinstance(row.get("exit_plan_metrics"), dict) else {}
        noise = metrics.get("noise_state") if isinstance(metrics.get("noise_state"), dict) else {}
        dynamic_floor = fnum(noise.get("dynamic_stop_floor"))
        atr14 = fnum(noise.get("atr14"))
        rr_after_noise = fnum(metrics.get("rr_after_noise_stop"))
        verdicts: list[str] = []
        if dynamic_floor and stop_dist < dynamic_floor:
            verdicts.append("BAD: stop below dynamic noise floor")
        elif dynamic_floor and stop_dist < dynamic_floor * 1.25:
            verdicts.append("BORDERLINE: stop only slightly above dynamic noise floor")
        if atr14 and stop_dist < atr14 * 1.2:
            verdicts.append("WARN: stop below prompt ATR*1.2 rule")
        if rr_after_noise and rr_after_noise < 1.5:
            verdicts.append("BAD: rr_after_noise below 1.5")
        last_5m = noise.get("last_5m") if isinstance(noise.get("last_5m"), dict) else {}
        direction = str(last_5m.get("direction") or "")
        side = str(row.get("side") or "").lower()
        if side == "short" and direction == "up":
            verdicts.append("WARN: latest 5m candle direction is against short")
        if side == "long" and direction == "down":
            verdicts.append("WARN: latest 5m candle direction is against long")

        out["derived"] = {
            "stop_distance": stop_dist,
            "reward_distance": reward_dist,
            "stop_pct": pct(stop_dist, entry),
            "target_pct": pct(reward_dist, entry),
            "stop_to_noise_floor": stop_dist / dynamic_floor if dynamic_floor else 0,
            "stop_to_atr": stop_dist / atr14 if atr14 else 0,
            "rr_after_noise": rr_after_noise,
            "verdicts": verdicts or ["OK"],
        }
    return out


def summary_payload(hours: float, limit: int, coin: str) -> dict[str, Any]:
    rows = load_rows(hours=hours, coin=coin)
    include_market = False
    counts = event_counts(rows)
    last_ts = max((int(row.get("ts") or 0) for row in rows), default=0)
    submits = [row for row in rows if row.get("event") == "submit"]
    rejects = [row for row in rows if row.get("event") == "risk_reject"]
    llm = [row for row in rows if row.get("event") == "llm_decision"]
    be_moves = [row for row in rows if row.get("event") == "stop_to_break_even"]
    by_coin: dict[str, dict[str, Any]] = {}
    for row in submits:
        coin_name = str(row.get("coin") or "?")
        bucket = by_coin.setdefault(coin_name, {"submits": 0, "long": 0, "short": 0})
        bucket["submits"] += 1
        side = str(row.get("side") or "")
        if side in ("long", "short"):
            bucket[side] += 1

    return {
        "ok": True,
        "journal": str(JOURNAL_PATH),
        "hours": hours,
        "coin": coin or "ALL",
        "row_count": len(rows),
        "counts": counts,
        "last_event_ts": last_ts,
        "last_event_age_s": round((time.time() * 1000 - last_ts) / 1000, 1) if last_ts else None,
        "by_coin": by_coin,
        "latest_submit": enrich(latest(rows, "submit") or {}, include_market=include_market),
        "recent_submits": [enrich(row, include_market=include_market) for row in submits[-limit:]],
        "recent_rejects": [enrich(row, include_market=include_market) for row in rejects[-limit:]],
        "recent_llm_decisions": [enrich(row, include_market=include_market) for row in llm[-limit:]],
        "recent_be_moves": [enrich(row, include_market=include_market) for row in be_moves[-limit:]],
    }


def events_payload(query: dict[str, list[str]]) -> dict[str, Any]:
    hours = fnum((query.get("hours") or ["6"])[0], 6.0)
    limit = int(fnum((query.get("limit") or ["25"])[0], 25))
    coin = (query.get("coin") or [""])[0]
    event = (query.get("event") or [""])[0]
    include_market = (query.get("include_market") or ["0"])[0] == "1"
    rows = load_rows(hours=hours, coin=coin, event=event)
    selected = rows[-limit:] if limit > 0 else rows
    return {
        "ok": True,
        "hours": hours,
        "coin": coin or "ALL",
        "event": event or "ALL",
        "count": len(rows),
        "rows": [enrich(row, include_market=include_market) for row in selected],
    }


class Handler(SimpleHTTPRequestHandler):
    server_version = "HLPanel/1.0"

    def do_GET(self) -> None:
        if self.path == "/healthz":
            self.send_json({"ok": True})
            return
        if not self.authorized():
            self.send_response(HTTPStatus.UNAUTHORIZED)
            self.send_header("WWW-Authenticate", 'Basic realm="HL Trading Panel"')
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(b"auth required\n")
            return

        parsed = urlparse(self.path)
        query = parse_qs(parsed.query)
        if parsed.path == "/api/summary":
            hours = fnum((query.get("hours") or ["6"])[0], 6.0)
            limit = int(fnum((query.get("limit") or ["10"])[0], 10))
            coin = (query.get("coin") or [""])[0]
            self.send_json(summary_payload(hours, limit, coin))
            return
        if parsed.path == "/api/events":
            self.send_json(events_payload(query))
            return
        if parsed.path == "/" or parsed.path == "/index.html":
            self.serve_file(APP_DIR / "index.html", "text/html; charset=utf-8")
            return
        if parsed.path == "/app.js":
            self.serve_file(APP_DIR / "app.js", "application/javascript; charset=utf-8")
            return
        self.send_error(HTTPStatus.NOT_FOUND)

    def authorized(self) -> bool:
        header = self.headers.get("Authorization", "")
        if not header.startswith("Basic "):
            return False
        try:
            raw = base64.b64decode(header[6:]).decode("utf-8")
        except Exception:
            return False
        if ":" not in raw:
            return False
        user, password = raw.split(":", 1)
        return user == PANEL_USER and password == PANEL_PASSWORD

    def send_json(self, payload: Any) -> None:
        body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def serve_file(self, path: Path, content_type: str) -> None:
        body = path.read_bytes()
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", content_type)
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt: str, *args: Any) -> None:
        print(f"[panel] {self.address_string()} {fmt % args}", flush=True)


def main() -> int:
    httpd = ThreadingHTTPServer(("0.0.0.0", PANEL_PORT), Handler)
    print(f"HL panel listening on :{PANEL_PORT}, journal={JOURNAL_PATH}", flush=True)
    httpd.serve_forever()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
