"""Lightweight Pi-side UI for testing the production-mode + LiveKit flow.

Run on the Pi:
    /opt/robopark-pi-client/.venv/bin/python /opt/robopark-pi-client/pi_ui.py \
        --scheduler-url http://192.168.1.241:8080 \
        --port 8081

Then open http://192.168.1.159:8081/ in any browser on the same LAN.

What it shows / does:
- Live status of this device as seen by the scheduler (online/offline, last heartbeat)
- Current scheduler settings (production_mode, LiveKit configured)
- Big "Join Conversation" button that asks the scheduler for a LiveKit token and
  opens https://meet.livekit.io/custom?livekitUrl=...&token=... in a new tab
- "Send motor command" widget that fires a JSON command into the MotorBridge
  (same wire format as the LiveKit data channel)
- "Tail logs" pane that shows the last lines of the robopark-pi-client service
"""

import argparse
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Optional

import httpx
from flask import Flask, jsonify, render_template_string, request

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
log = logging.getLogger("robopark-pi.ui")

app = Flask(__name__)

# Lazy-loaded MotorBridge import so the UI can start even if the SDK is missing.
_BRIDGE = None
def _bridge():
    global _BRIDGE
    if _BRIDGE is None:
        try:
            from motor_bridge import MotorBridge
            _BRIDGE = MotorBridge(
                motor_server_url=os.environ.get("MOTOR_SERVER_URL", "http://127.0.0.1:8001"),
                dry_run=os.environ.get("DRY_RUN_MOTORS", "0") == "1",
            )
        except Exception as e:
            log.warning(f"MotorBridge unavailable: {e}")
            _BRIDGE = None
    return _BRIDGE

STATE_FILE = Path(os.environ.get("STATE_FILE", "/etc/robopark/device.json"))


def _load_device() -> dict:
    if not STATE_FILE.exists():
        return {}
    try:
        return json.loads(STATE_FILE.read_text())
    except Exception as e:
        log.warning(f"could not read {STATE_FILE}: {e}")
        return {}


def _read_service_log(lines: int = 60) -> str:
    try:
        out = subprocess.run(
            ["journalctl", "-u", "robopark-pi-client", "-n", str(lines), "--no-pager", "-o", "cat"],
            capture_output=True, text=True, timeout=5,
        )
        return out.stdout
    except Exception as e:
        return f"(could not read journal: {e})"


HTML = r"""
<!doctype html>
<html><head>
<title>RoboPark Pi Console</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
  * { box-sizing: border-box; }
  body { font: 14px/1.4 -apple-system, system-ui, Segoe UI, sans-serif;
         background: #0f172a; color: #e2e8f0; margin: 0; padding: 24px; }
  h1 { margin: 0 0 8px; }
  h2 { margin: 24px 0 8px; font-size: 16px; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
  .card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
  .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
  .pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
  .pill.green  { background: #14532d; color: #86efac; }
  .pill.red    { background: #7f1d1d; color: #fca5a5; }
  .pill.gray   { background: #1f2937; color: #d1d5db; }
  .pill.yellow { background: #78350f; color: #fde68a; }
  .pill.blue   { background: #1e3a8a; color: #bfdbfe; }
  button { background: #2563eb; color: white; border: 0; padding: 8px 16px;
           border-radius: 6px; font-size: 14px; cursor: pointer; }
  button:hover { background: #1d4ed8; }
  button:disabled { background: #334155; color: #94a3b8; cursor: not-allowed; }
  button.danger  { background: #dc2626; }  button.danger:hover  { background: #b91c1c; }
  button.success { background: #16a34a; }  button.success:hover { background: #15803d; }
  button.muted   { background: #475569; }  button.muted:hover   { background: #334155; }
  pre { background: #020617; color: #e2e8f0; padding: 12px; border-radius: 6px;
        overflow-x: auto; font-size: 12px; line-height: 1.4; }
  input, select { background: #0f172a; color: #e2e8f0; border: 1px solid #334155;
                  padding: 6px 10px; border-radius: 4px; font: inherit; }
  .status { display: flex; gap: 24px; flex-wrap: wrap; }
  .status div { min-width: 140px; }
  .status b { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 2px; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
  .small { font-size: 12px; color: #94a3b8; }
  .mono  { font-family: ui-monospace, Menlo, Consolas, monospace; }
</style>
</head><body>
<h1>🤖 RoboPark Pi Console</h1>
<p class="small" id="hostname">ceopi1</p>

<div class="card">
  <h2>This device</h2>
  <div class="status" id="status">
    <div><b>Device ID</b><span class="mono" id="device_id">—</span></div>
    <div><b>Status</b><span id="status_pill" class="pill gray">…</span></div>
    <div><b>Last heartbeat</b><span id="last_hb" class="mono">—</span></div>
    <div><b>Motor server</b><span id="motor_url" class="mono">—</span></div>
  </div>
</div>

<div class="grid">
  <div class="card">
    <h2>Scheduler</h2>
    <div class="status">
      <div><b>URL</b><span class="mono" id="sched_url">—</span></div>
      <div><b>Production mode</b><span id="prod_pill" class="pill gray">…</span></div>
      <div><b>LiveKit</b><span id="lk_pill" class="pill gray">…</span></div>
    </div>
    <div class="row" style="margin-top:12px">
      <button id="prod_toggle" class="muted">Toggle production mode</button>
      <button id="refresh" class="muted">Refresh</button>
    </div>
  </div>

  <div class="card">
    <h2>LiveKit — join conversation</h2>
    <p class="small">Asks the scheduler for a short-lived token, then opens
       <code>meet.livekit.io</code> in a new tab. You can publish mic + cam and
       hear the agent; the Pi's audio hardware is forwarded by the main client.</p>
    <div class="row">
      <label>Room <input id="room" value="robopark-pi-test" class="mono" style="width:200px"></label>
      <label>Identity <input id="identity" value="" class="mono" style="width:200px"
                              placeholder="auto"></label>
    </div>
    <div class="row" style="margin-top:12px">
      <button id="join" class="success">Join conversation</button>
    </div>
    <p class="small" id="join_msg" style="margin-top:8px"></p>
  </div>
</div>

<div class="card">
  <h2>Motor test</h2>
  <p class="small">Fires the same JSON payload the LiveKit data channel would send
     into the local motor bridge. No wiring needed.</p>
  <div class="row">
    <label>Motor <input id="motor_name" value="arm" class="mono" style="width:120px"></label>
    <label>Seconds <input id="motor_secs" value="2" type="number" min="1" max="30" style="width:80px"></label>
    <button id="motor_go" class="success">Move</button>
    <button id="motor_stop" class="danger">Stop all</button>
  </div>
  <p class="small" id="motor_msg" style="margin-top:8px"></p>
</div>

<div class="card">
  <h2>Service log (last 60 lines)</h2>
  <pre id="log">(loading…)</pre>
  <div class="row" style="margin-top:8px">
    <button id="log_refresh" class="muted">Refresh log</button>
  </div>
</div>

<script>
async function fetchJSON(url, opts) {
  const r = await fetch(url, opts);
  if (!r.ok) throw new Error("HTTP " + r.status + " " + (await r.text()).slice(0, 200));
  return r.json();
}

function fmtTime(iso) {
  if (!iso) return "—";
  try { return new Date(iso).toLocaleString(); } catch { return iso; }
}

async function refresh() {
  try {
    const s = await fetchJSON("/api/status");
    document.getElementById("device_id").textContent = s.device_id || "(not enrolled)";
    const pill = document.getElementById("status_pill");
    pill.textContent = s.status || "unknown";
    pill.className = "pill " + (s.status === "online" ? "green"
      : s.status === "offline" ? "red" : "gray");
    document.getElementById("last_hb").textContent = fmtTime(s.last_heartbeat);
    document.getElementById("motor_url").textContent = s.motor_server_url || "—";
    document.getElementById("sched_url").textContent = s.scheduler_url;
    const pp = document.getElementById("prod_pill");
    pp.textContent = s.production_mode ? "ON" : "OFF";
    pp.className = "pill " + (s.production_mode ? "green" : "gray");
    const lp = document.getElementById("lk_pill");
    if (s.livekit_configured) { lp.textContent = s.livekit_url || "set"; lp.className = "pill blue"; }
    else { lp.textContent = "NOT SET"; lp.className = "pill yellow"; }
    if (!document.getElementById("identity").value) {
      document.getElementById("identity").placeholder = "pi-ui-" + (s.device_id || "anon");
    }
  } catch (e) { console.error(e); }
}

async function toggleProd() {
  const s = await fetchJSON("/api/status");
  await fetchJSON("/api/production_mode", { method: "PUT",
    headers: {"Content-Type":"application/json"},
    body: JSON.stringify({ production_mode: !s.production_mode }) });
  await refresh();
}

async function join() {
  const room = document.getElementById("room").value.trim() || "robopark-pi-test";
  const identity = document.getElementById("identity").value.trim()
                || "pi-ui-" + Math.random().toString(36).slice(2,8);
  const msg = document.getElementById("join_msg");
  msg.textContent = "requesting token...";
  try {
    const t = await fetchJSON("/api/join", { method: "POST",
      headers: {"Content-Type":"application/json"},
      body: JSON.stringify({ room, identity }) });
    const url = "https://meet.livekit.io/custom?liveKitUrl="
              + encodeURIComponent(t.url) + "&token=" + encodeURIComponent(t.token);
    window.open(url, "_blank");
    msg.textContent = "Token expires " + fmtTime(t.expires_at) + ". Opened in a new tab.";
  } catch (e) { msg.textContent = "error: " + e.message; }
}

async function motor(cmd) {
  const msg = document.getElementById("motor_msg");
  try {
    const r = await fetchJSON("/api/motor", { method: "POST",
      headers: {"Content-Type":"application/json"}, body: JSON.stringify(cmd) });
    msg.textContent = JSON.stringify(r);
  } catch (e) { msg.textContent = "error: " + e.message; }
}

async function refreshLog() {
  try {
    const r = await fetchJSON("/api/log");
    document.getElementById("log").textContent = r.lines || "(empty)";
  } catch (e) { document.getElementById("log").textContent = "error: " + e.message; }
}

document.getElementById("refresh").onclick = refresh;
document.getElementById("prod_toggle").onclick = toggleProd;
document.getElementById("join").onclick = join;
document.getElementById("motor_go").onclick = () => motor({
  op: "motor",
  name: document.getElementById("motor_name").value,
  seconds: parseInt(document.getElementById("motor_secs").value, 10) || 2,
});
document.getElementById("motor_stop").onclick = () => motor({ op: "stop" });
document.getElementById("log_refresh").onclick = refreshLog;

refresh();
refreshLog();
setInterval(refresh, 5000);
setInterval(refreshLog, 5000);
</script>
</body></html>
"""


@app.route("/")
def index():
    return render_template_string(HTML)


@app.route("/api/status")
def api_status():
    dev = _load_device()
    out = {
        "scheduler_url": app.config["SCHEDULER_URL"],
        "device_id": dev.get("device_id"),
        "status": None,
        "last_heartbeat": None,
        "motor_server_url": dev.get("motor_server_url"),
        "production_mode": None,
        "livekit_configured": False,
        "livekit_url": None,
    }
    try:
        with httpx.Client(timeout=5.0) as c:
            r = c.get(f"{app.config['SCHEDULER_URL']}/api/settings")
            if r.status_code == 200:
                s = r.json()
                out["production_mode"] = bool(s.get("production_mode"))
            r2 = c.get(f"{app.config['SCHEDULER_URL']}/api/livekit/config")
            if r2.status_code == 200:
                lk = r2.json()
                out["livekit_configured"] = bool(lk.get("url") and lk.get("has_secret"))
                out["livekit_url"] = lk.get("url")
            if dev.get("device_id"):
                r3 = c.get(f"{app.config['SCHEDULER_URL']}/api/devices/{dev['device_id']}")
                if r3.status_code == 200:
                    d = r3.json()
                    out["status"] = d.get("status")
                    out["last_heartbeat"] = d.get("last_heartbeat")
    except Exception as e:
        log.warning(f"status fetch failed: {e}")
    return jsonify(out)


@app.route("/api/production_mode", methods=["PUT"])
def api_prod_mode():
    payload = request.get_json(force=True) or {}
    new = bool(payload.get("production_mode", False))
    with httpx.Client(timeout=5.0) as c:
        r = c.put(f"{app.config['SCHEDULER_URL']}/api/settings",
                  json={"production_mode": new})
        r.raise_for_status()
    return jsonify({"status": "ok", "production_mode": new})


@app.route("/api/join", methods=["POST"])
def api_join():
    payload = request.get_json(force=True) or {}
    room = payload.get("room") or "robopark-pi-test"
    identity = payload.get("identity") or ("pi-ui-" + os.urandom(3).hex())
    with httpx.Client(timeout=5.0) as c:
        r = c.post(f"{app.config['SCHEDULER_URL']}/api/livekit/token",
                   json={"room": room, "identity": identity,
                         "name": "Pi UI", "ttl_seconds": 3600})
        if r.status_code != 200:
            return jsonify({"error": r.text}), r.status_code
        return jsonify(r.json())


@app.route("/api/motor", methods=["POST"])
def api_motor():
    payload = request.get_json(force=True) or {}
    bridge = _bridge()
    if bridge is None:
        return jsonify({"error": "motor bridge not loaded (import failed)"}), 503
    import asyncio
    result = asyncio.run(bridge.handle_command(json.dumps(payload)))
    return jsonify({"result": result})


@app.route("/api/log")
def api_log():
    return jsonify({"lines": _read_service_log(60)})


def parse_args():
    p = argparse.ArgumentParser()
    p.add_argument("--scheduler-url", required=True)
    p.add_argument("--host", default="0.0.0.0")
    p.add_argument("--port", type=int, default=8081)
    p.add_argument("--debug", action="store_true")
    return p.parse_args()


def main():
    args = parse_args()
    app.config["SCHEDULER_URL"] = args.scheduler_url.rstrip("/")
    log.info(f"Pi UI on http://{args.host}:{args.port}/ -> scheduler {app.config['SCHEDULER_URL']}")
    app.run(host=args.host, port=args.port, debug=args.debug, threaded=True)


if __name__ == "__main__":
    main()