"""
run.py — People Hub Dev Server with Live Browser Reload
========================================================
Run this file during development instead of main.py.

    python run.py

What it does:
  ✅ Starts FastAPI with hot-reload on .py changes (uvicorn --reload)
  ✅ Watches static/ for .html / .css / .js changes
  ✅ Sends a Server-Sent Events (SSE) signal to every open browser tab
  ✅ Browser auto-refreshes — no manual F5 needed
  ✅ Injects a tiny <script> into every HTML page served (dev only)
  ✅ Single terminal, single command

Stop:  Ctrl+C
"""
from __future__ import annotations

import os
import sys
import threading
import time
from pathlib import Path

# ── Run from project root ──────────────────────────────────────────────────────
ROOT = Path(__file__).parent.resolve()
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))

# ─────────────────────────────────────────────────────────────────────────────
# The live-reload <script> injected into every HTML page.
# Opens a persistent SSE connection to /livereload/ping.
# When the server fires "event: reload", the browser calls location.reload().
# ─────────────────────────────────────────────────────────────────────────────
LIVERELOAD_SCRIPT = """\
<script>
/* People Hub Live Reload — dev server only */
(function(){
  function connect() {
    var es = new EventSource('/livereload/ping');
    es.addEventListener('reload', function() {
      console.log('[livereload] change detected, reloading...');
      location.reload();
    });
    es.onerror = function() {
      es.close();
      setTimeout(connect, 1000);   // retry after server restart
    };
  }
  connect();
  console.log('[livereload] watching for file changes');
})();
</script>"""

# ─────────────────────────────────────────────────────────────────────────────
# Import the production app so we can add dev-only extras to it.
# ─────────────────────────────────────────────────────────────────────────────
import asyncio
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import StreamingResponse, Response

from main import app  # the production FastAPI instance
from config import settings

# ── Shared flag: background thread sets this; SSE generator reads and clears it
_reload_flag = threading.Event()


# ─────────────────────────────────────────────────────────────────────────────
# SSE ENDPOINT  GET /livereload/ping
# Every browser tab opens this long-lived connection.
# We send "event: reload\ndata: 1\n\n" whenever _reload_flag is set.
# ─────────────────────────────────────────────────────────────────────────────

@app.get("/livereload/ping", include_in_schema=False)
async def livereload_ping(request: Request):
    """SSE stream — browsers subscribe here for live-reload signals."""

    async def stream():
        yield "data: connected\n\n"   # immediate handshake so EventSource is happy
        keepalive_ticks = 0
        while True:
            await asyncio.sleep(0.3)

            if await request.is_disconnected():
                break

            if _reload_flag.is_set():
                _reload_flag.clear()
                yield "event: reload\ndata: 1\n\n"

            # Keep-alive every ~15 s so nginx / proxies don't drop the connection
            keepalive_ticks += 1
            if keepalive_ticks >= 50:
                keepalive_ticks = 0
                yield ": keep-alive\n\n"

    return StreamingResponse(
        stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control":     "no-cache",
            "X-Accel-Buffering": "no",
        },
    )


# ─────────────────────────────────────────────────────────────────────────────
# MIDDLEWARE — inject live-reload <script> into every HTML response
# ─────────────────────────────────────────────────────────────────────────────

class _LiveReloadMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response: Response = await call_next(request)

        # Only touch HTML
        ct = response.headers.get("content-type", "")
        if "text/html" not in ct:
            return response

        # Buffer the full body
        body_bytes = b""
        async for chunk in response.body_iterator:
            body_bytes += chunk if isinstance(chunk, bytes) else chunk.encode()

        html = body_bytes.decode("utf-8", errors="replace")

        # Inject before </body>; append as fallback
        tag = LIVERELOAD_SCRIPT + "\n</body>"
        html = html.replace("</body>", tag, 1) if "</body>" in html else html + "\n" + LIVERELOAD_SCRIPT

        return Response(
            content=html,
            status_code=response.status_code,
            media_type="text/html",
            headers={
                k: v for k, v in response.headers.items()
                if k.lower() not in ("content-length", "content-type")
            },
        )


app.add_middleware(_LiveReloadMiddleware)


# ─────────────────────────────────────────────────────────────────────────────
# BACKGROUND WATCHER — polls static/ for .html/.css/.js changes
# Signals _reload_flag when anything changes.
# Python (.py) changes are handled by uvicorn's built-in --reload.
# ─────────────────────────────────────────────────────────────────────────────

_WATCH_EXTS = {".html", ".htm", ".css", ".js"}
_WATCH_DIR  = ROOT / "static"


def _collect_mtimes(directory: Path) -> dict[str, float]:
    result: dict[str, float] = {}
    if not directory.exists():
        return result
    for f in directory.rglob("*"):
        if f.is_file() and f.suffix.lower() in _WATCH_EXTS:
            try:
                result[str(f)] = f.stat().st_mtime
            except OSError:
                pass
    return result


def _watcher_thread():
    prev = _collect_mtimes(_WATCH_DIR)
    while True:
        time.sleep(0.4)
        curr = _collect_mtimes(_WATCH_DIR)

        added    = set(curr) - set(prev)
        removed  = set(prev) - set(curr)
        modified = {k for k in curr if k in prev and curr[k] != prev[k]}
        changed  = added | removed | modified

        if changed:
            names = ", ".join(Path(p).name for p in changed)
            print(f"\n[livereload] 🔄  {names}  changed — refreshing browser...\n")
            _reload_flag.set()

        prev = curr


# ─────────────────────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import socket
    import webbrowser
    import uvicorn

    URL = f"http://localhost:{settings.APP_PORT}"

    # ── Start static file watcher ─────────────────────────────────────────────
    threading.Thread(
        target=_watcher_thread, daemon=True, name="static-watcher"
    ).start()

    # ── Open browser once server is ready ─────────────────────────────────────
    def _open_browser():
        """
        Poll localhost until the server accepts TCP connections, then open
        the default browser. Tries for up to 15 seconds before giving up.
        Runs as a daemon thread so it never blocks uvicorn starting up.
        """
        deadline = time.time() + 15
        while time.time() < deadline:
            time.sleep(0.25)
            try:
                with socket.create_connection(("localhost", settings.APP_PORT), timeout=0.5):
                    break           # server is accepting connections
            except OSError:
                continue            # not ready yet

        webbrowser.open(URL)
        print(f"[browser] ✅  Opened {URL}")

    threading.Thread(target=_open_browser, daemon=True, name="browser-opener").start()

    # ── Banner ────────────────────────────────────────────────────────────────
    print("\n" + "═" * 64)
    print(f"  {settings.APP_NAME}  ·  DEV SERVER")
    print("═" * 64)
    print(f"  🌐  URL      →  {URL}")
    print(f"  📖  Docs     →  {URL}/docs")
    print(f"  🚀  Browser opens automatically when server is ready")
    print()
    print("  Live reload")
    print("    .py files      →  server restarts automatically")
    print("    .html .css .js →  browser refreshes automatically")
    print()
    print("  Test credentials  (edit data/users.json to change stage)")
    print("    DEMO0001  →  Stage 1  Assessment")
    print("    DEMO0002  →  Stage 2  Role Play")
    print("    DEMO0003  →  Stage 3  Coding")
    print("    DEMO0004  →  Stage 4  Technical")
    print()
    print("  Stop:  Ctrl+C")
    print("═" * 64 + "\n")

    # ── Start uvicorn ─────────────────────────────────────────────────────────
    uvicorn.run(
        "run:app",               # string import so uvicorn can reload on .py changes
        host=settings.APP_HOST,
        port=settings.APP_PORT,
        reload=True,
        reload_dirs=[str(ROOT)],
        reload_includes=["*.py"],
        reload_excludes=[        # static changes handled by our own watcher thread
            "static/*",
            "data/*",
            "*.html", "*.css", "*.js", "*.json",
        ],
        log_level="info",
    )