"""VDO.Ninja integration — stream DJ HUD output to browsers via WebSocket.

Architecture (simplified for VPN / LAN use):

    DJ HUD (Python/tkinter)
        │  JPEG frames via push_frame()
        ▼
    aiohttp server (port 7088, all interfaces)
        ├── GET /          → publisher page (canvas preview + status)
        ├── GET /viewer    → viewer page (fullscreen canvas, auto-reconnect)
        ├── GET /ws        → WebSocket endpoint (binary JPEG frames)
        └── GET /health    → health check

    Viewers open:  http://<DJ_IP>:7088/viewer
    (works over VPN, LAN, or localhost — no external services needed)

No Meshcast, no WHIP/WHEP, no STUN/TURN, no WebRTC.
Just JPEG-over-WebSocket drawn to a canvas.  Works everywhere.
"""

import asyncio
import io
import threading
import time
import webbrowser
from typing import Optional

try:
    from aiohttp import web
except ImportError:
    web = None

try:
    from PIL import Image
except ImportError:
    Image = None

# ─── Constants ────────────────────────────────────────────────────────────────

DEFAULT_PORT = 7088
JPEG_QUALITY = 75
MAX_FPS = 30


# ─── HTML: Publisher page (shown to the DJ) ──────────────────────────────────

def _build_publisher_html(port: int, stream_id: str, fps: int) -> str:
    return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>DJ HUD — Stream Control</title>
<style>
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
  body {{ background: #0f0f1a; color: #e0e0e0; font-family: system-ui, -apple-system, sans-serif;
         display: flex; flex-direction: column; min-height: 100vh; }}

  .header {{ padding: 12px 20px; background: #16213e; border-bottom: 1px solid #2a3a5e;
             display: flex; align-items: center; gap: 16px; flex-shrink: 0; }}
  .header h1 {{ font-size: 15px; font-weight: 600; color: #7ec8ff; white-space: nowrap; }}
  .badge {{ font-size: 12px; padding: 3px 10px; border-radius: 10px; font-weight: 500; }}
  .badge-off {{ background: #3d1414; color: #f87171; }}
  .badge-ok  {{ background: #143d1f; color: #4ade80; }}
  .badge-live {{ background: #14293d; color: #60a5fa; animation: pulse 2s infinite; }}
  @keyframes pulse {{ 0%,100% {{ opacity:1; }} 50% {{ opacity:0.7; }} }}

  .info {{ padding: 10px 20px; background: #121225; border-bottom: 1px solid #222;
           font-size: 13px; display: flex; flex-wrap: wrap; gap: 16px; align-items: center; }}
  .info a {{ color: #60a5fa; text-decoration: none; word-break: break-all; }}
  .info a:hover {{ text-decoration: underline; }}
  .btn-sm {{ border: none; padding: 4px 12px; font-size: 12px; background: #333;
             color: #ccc; border-radius: 4px; cursor: pointer; }}
  .btn-sm:hover {{ background: #444; }}

  .canvas-wrap {{ flex: 1; display: flex; align-items: center; justify-content: center;
                  padding: 16px; min-height: 0; }}
  canvas {{ border: 1px solid #2a2a3e; border-radius: 4px; max-width: 100%;
            max-height: 100%; background: #000; object-fit: contain; }}

  .footer {{ padding: 8px 20px; font-size: 12px; color: #555; border-top: 1px solid #1a1a2e;
             display: flex; gap: 16px; flex-shrink: 0; }}
</style>
</head>
<body>

<div class="header">
  <h1>DJ HUD Stream</h1>
  <span class="badge badge-off" id="wsBadge">Connecting...</span>
  <span class="badge badge-off" id="clientBadge">0 viewers</span>
</div>

<div class="info" id="viewerInfo">
  <span>Viewer link:</span>
  <a id="viewerLink" href="/viewer" target="_blank"></a>
  <button class="btn-sm" onclick="copyLink()">Copy</button>
</div>

<div class="canvas-wrap">
  <canvas id="c" width="1080" height="80"></canvas>
</div>

<div class="footer">
  <span id="fpsInfo">&mdash; fps</span>
  <span id="sizeInfo">&mdash;</span>
</div>

<script>
const WS_URL = "ws://" + window.location.host + "/ws";
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let frameCount = 0, fpsTimer = Date.now();

// Set viewer link to use current host (so it works over VPN IP too)
const viewerUrl = window.location.origin + "/viewer";
document.getElementById("viewerLink").href = viewerUrl;
document.getElementById("viewerLink").textContent = viewerUrl;

function copyLink() {{
  navigator.clipboard.writeText(viewerUrl).then(() => {{
    const b = event.target;
    b.textContent = "Copied!";
    setTimeout(() => b.textContent = "Copy", 1500);
  }});
}}

function connectWs() {{
  const ws = new WebSocket(WS_URL);
  ws.binaryType = "arraybuffer";

  ws.onopen = () => {{
    document.getElementById("wsBadge").textContent = "Connected";
    document.getElementById("wsBadge").className = "badge badge-ok";
  }};

  ws.onmessage = (evt) => {{
    const blob = new Blob([evt.data], {{ type: "image/jpeg" }});
    const url = URL.createObjectURL(blob);
    const img = new window.Image();
    img.onload = () => {{
      if (canvas.width !== img.width || canvas.height !== img.height) {{
        canvas.width = img.width;
        canvas.height = img.height;
        document.getElementById("sizeInfo").textContent = img.width + " × " + img.height;
      }}
      ctx.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);
      frameCount++;
      const now = Date.now();
      if (now - fpsTimer >= 1000) {{
        document.getElementById("fpsInfo").textContent = frameCount + " fps";
        frameCount = 0;
        fpsTimer = now;
      }}
    }};
    img.src = url;
  }};

  ws.onclose = () => {{
    document.getElementById("wsBadge").textContent = "Disconnected";
    document.getElementById("wsBadge").className = "badge badge-off";
    setTimeout(connectWs, 2000);
  }};

  ws.onerror = () => {{ try {{ ws.close(); }} catch(_) {{}} }};
}}

// Poll client count
setInterval(async () => {{
  try {{
    const r = await fetch("/health");
    const t = await r.text();
    const m = t.match(/clients=(\\d+)/);
    if (m) {{
      const n = parseInt(m[1]);
      const badge = document.getElementById("clientBadge");
      badge.textContent = n + " viewer" + (n !== 1 ? "s" : "");
      badge.className = "badge " + (n > 0 ? "badge-live" : "badge-off");
    }}
  }} catch(_) {{}}
}}, 2000);

connectWs();
</script>
</body>
</html>"""


# ─── HTML: Viewer page (shared with others on the VPN/LAN) ──────────────────

def _build_viewer_html() -> str:
    return """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>DJ HUD Stream</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: #000; display: flex; align-items: center; justify-content: center;
         min-height: 100vh; font-family: system-ui, sans-serif; color: #ccc; }
  canvas { max-width: 100%; max-height: 100vh; object-fit: contain; image-rendering: auto; }
  #status { position: fixed; top: 10px; left: 10px; font-size: 13px;
            background: rgba(0,0,0,0.7); padding: 6px 12px; border-radius: 6px;
            transition: opacity 0.5s; }
  .ok  { color: #4ade80; }
  .err { color: #f87171; }
</style>
</head>
<body>

<canvas id="c" width="1080" height="80"></canvas>
<div id="status">Connecting...</div>

<script>
const WS_URL = "ws://" + window.location.host + "/ws";
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
let hideTimer = null;

function connectWs() {
  statusEl.style.opacity = "1";
  statusEl.innerHTML = "Connecting...";

  const ws = new WebSocket(WS_URL);
  ws.binaryType = "arraybuffer";

  ws.onopen = () => {
    statusEl.innerHTML = '<span class="ok">Connected</span>';
    hideTimer = setTimeout(() => { statusEl.style.opacity = "0"; }, 3000);
  };

  ws.onmessage = (evt) => {
    const blob = new Blob([evt.data], { type: "image/jpeg" });
    const url = URL.createObjectURL(blob);
    const img = new window.Image();
    img.onload = () => {
      if (canvas.width !== img.width || canvas.height !== img.height) {
        canvas.width = img.width;
        canvas.height = img.height;
      }
      ctx.drawImage(img, 0, 0);
      URL.revokeObjectURL(url);
    };
    img.src = url;
  };

  ws.onclose = () => {
    if (hideTimer) clearTimeout(hideTimer);
    statusEl.style.opacity = "1";
    statusEl.innerHTML = '<span class="err">Disconnected — reconnecting...</span>';
    setTimeout(connectWs, 2000);
  };

  ws.onerror = () => { try { ws.close(); } catch(_) {} };
}

connectWs();
</script>
</body>
</html>"""


# ═══════════════════════════════════════════════════════════════════════════════
# aiohttp Server — HTTP + WebSocket on a single port
# ═══════════════════════════════════════════════════════════════════════════════

class VDONinjaServer:
    """Local HTTP + WebSocket server that streams DJ HUD frames to browsers.

    Uses aiohttp for reliable cross-platform WebSocket support.
    Runs in its own thread with a dedicated asyncio event loop,
    so it doesn't interfere with tkinter's main loop.

    Public API:
        start()          — start the server thread
        stop()           — shut down gracefully
        push_frame(img)  — send a PIL Image to all connected browsers
        is_running       — bool property
        client_count     — int property
        url              — str property
        open_browser()   — open the publisher page in default browser
    """

    def __init__(self, port: int = DEFAULT_PORT, stream_id: str = "",
                 target_fps: int = 15):
        if web is None:
            raise ImportError(
                "aiohttp is required for streaming. "
                "Install it with: pip install aiohttp"
            )
        self._port = port
        self.stream_id = stream_id
        self.target_fps = target_fps
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._loop: Optional[asyncio.AbstractEventLoop] = None
        self._ws_clients: set = set()
        self._ws_lock = threading.Lock()
        self._last_frame_time = 0.0

    # ── HTTP routes ──────────────────────────────────────────────────

    async def _handle_index(self, request):
        html = _build_publisher_html(
            port=self._port,
            stream_id=self.stream_id,
            fps=min(MAX_FPS, self.target_fps),
        )
        return web.Response(text=html, content_type="text/html",
                            headers={"Cache-Control": "no-cache"})

    async def _handle_viewer(self, request):
        html = _build_viewer_html()
        return web.Response(text=html, content_type="text/html",
                            headers={"Cache-Control": "no-cache"})

    async def _handle_health(self, request):
        with self._ws_lock:
            n = len(self._ws_clients)
        return web.Response(text=f"ok clients={n}")

    # ── WebSocket handler ────────────────────────────────────────────

    async def _handle_ws(self, request):
        """Handle a WebSocket connection from a browser.

        aiohttp manages the full upgrade handshake and frame protocol,
        so there's no manual 101/RFC-6455 work needed.
        """
        ws_response = web.WebSocketResponse(
            heartbeat=20.0,       # send ping every 20s, expect pong
            autoping=True,
        )
        await ws_response.prepare(request)

        remote = request.remote or "unknown"
        print(f"[Stream] WebSocket client connected from {remote}")
        with self._ws_lock:
            self._ws_clients.add(ws_response)

        try:
            async for msg in ws_response:
                if msg.type in (web.WSMsgType.ERROR, web.WSMsgType.CLOSE,
                                web.WSMsgType.CLOSING, web.WSMsgType.CLOSED):
                    break
        except Exception as e:
            print(f"[Stream] WebSocket error: {e}")
        finally:
            with self._ws_lock:
                self._ws_clients.discard(ws_response)
            print(f"[Stream] WebSocket client disconnected ({remote})")

        return ws_response

    # ── Server lifecycle ─────────────────────────────────────────────

    def start(self):
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(target=self._run_server, daemon=True)
        self._thread.start()

    def _run_server(self):
        """Run the aiohttp server in a dedicated asyncio event loop."""
        self._loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._loop)

        app = web.Application()
        app.router.add_get("/", self._handle_index)
        app.router.add_get("/index.html", self._handle_index)
        app.router.add_get("/viewer", self._handle_viewer)
        app.router.add_get("/health", self._handle_health)
        app.router.add_get("/ws", self._handle_ws)

        runner = web.AppRunner(app)
        self._loop.run_until_complete(runner.setup())

        site = web.TCPSite(runner, "0.0.0.0", self._port)
        self._loop.run_until_complete(site.start())
        print(f"[Stream] Server running on http://0.0.0.0:{self._port}")

        try:
            self._loop.run_forever()
        except Exception:
            pass
        finally:
            self._loop.run_until_complete(runner.cleanup())
            self._loop.close()
            self._loop = None

    def stop(self):
        self._running = False

        with self._ws_lock:
            clients = list(self._ws_clients)
            self._ws_clients.clear()

        if self._loop and not self._loop.is_closed():
            for ws_client in clients:
                try:
                    asyncio.run_coroutine_threadsafe(
                        ws_client.close(), self._loop
                    )
                except Exception:
                    pass
            self._loop.call_soon_threadsafe(self._loop.stop)

        if self._thread:
            self._thread.join(timeout=3.0)
            self._thread = None

        print("[Stream] Server stopped")

    # ── Frame push ───────────────────────────────────────────────────

    def push_frame(self, pil_image: "Image.Image", quality: int = JPEG_QUALITY):
        """Encode a PIL image as JPEG and send to all connected browsers."""
        with self._ws_lock:
            if not self._ws_clients or Image is None:
                return

        # FPS throttle
        now = time.monotonic()
        if now - self._last_frame_time < 1.0 / max(1, self.target_fps):
            return
        self._last_frame_time = now

        buf = io.BytesIO()
        pil_image.save(buf, format="JPEG", quality=quality, optimize=False)
        jpeg_bytes = buf.getvalue()

        with self._ws_lock:
            clients = list(self._ws_clients)

        loop = self._loop
        if loop is None or loop.is_closed():
            return

        for ws_client in clients:
            try:
                asyncio.run_coroutine_threadsafe(
                    self._send_frame(ws_client, jpeg_bytes), loop
                )
            except Exception:
                pass

    async def _send_frame(self, ws_client, data: bytes):
        """Send binary frame data to one WebSocket client."""
        try:
            await ws_client.send_bytes(data)
        except Exception:
            with self._ws_lock:
                self._ws_clients.discard(ws_client)

    # ── Properties ───────────────────────────────────────────────────

    @property
    def is_running(self):
        return self._running

    @property
    def client_count(self):
        with self._ws_lock:
            return len(self._ws_clients)

    @property
    def url(self):
        return f"http://127.0.0.1:{self._port}"

    def open_browser(self):
        webbrowser.open(self.url)
