"""
OBS broadcast MCP server  —  expose the broadcast's OBS control as agent tools.

Wraps `director_obs.OBSController` (obs-websocket v5) as a small set of Model Context Protocol tools, so
an LLM agent can *direct* the live broadcast in natural language — "switch to the holding card", "start
streaming", "is it live?" — and the tool layer verifies it over obs-websocket. It is the same control
path `serve_live.py --obs` uses; this just makes it agent-callable.

The OBS side is unchanged: one Browser source shows the live page (core/web/index.html?feed=live) and OBS
holds the YouTube RTMP credentials (Settings → Stream). This server only issues the lifecycle + scene
commands. It NEVER stores the stream key.

Config (env; the password stays out of the code / logs, like the rest of the stack):
    OBS_WS_HOST      default localhost
    OBS_WS_PORT      default 4455
    OBS_WS_PASSWORD  default ""      (set to OBS's Tools → WebSocket Server Settings password)

Run (stdio transport — how MCP clients launch it):
    .venv-demo/Scripts/python -m v3.broadcast.live.obs_mcp_server
Deps: `pip install obsws-python "mcp[cli]"`, and enable OBS's WebSocket server.

Register with an MCP client, e.g. Claude Code:
    claude mcp add obs-broadcast -- <python> -m v3.broadcast.live.obs_mcp_server
(pass the password via the client's env config, not on the command line).
"""
from __future__ import annotations

import os
import sys
from pathlib import Path

from mcp.server.fastmcp import FastMCP

sys.path.insert(0, str(Path(__file__).resolve().parent))   # so `director_obs` resolves either launch way
from broadcast_control import control_enabled  # noqa: E402  (the single shared control gate)
from director_obs import OBSController  # noqa: E402

# One lazily-connected controller for the process. obs-websocket is a single local endpoint, so a shared
# connection is right; each tool reconnects transparently if OBS was restarted underneath us.
_obs: OBSController | None = None


def _client() -> OBSController:
    """Return a connected OBSController, (re)connecting on demand. Raises a clear error if OBS/websocket
    is unreachable so the agent gets actionable feedback instead of a stack trace."""
    global _obs
    if _obs is None:
        _obs = OBSController(
            host=os.environ.get("OBS_WS_HOST", "localhost"),
            port=int(os.environ.get("OBS_WS_PORT", "4455")),
            password=os.environ.get("OBS_WS_PASSWORD", ""),
        )
    try:
        _obs.connect()
    except Exception as e:
        _obs = None
        raise RuntimeError(
            f"Cannot reach OBS at {os.environ.get('OBS_WS_HOST', 'localhost')}:"
            f"{os.environ.get('OBS_WS_PORT', '4455')} ({e}). Is OBS running with Tools -> WebSocket "
            "Server Settings enabled, and OBS_WS_PASSWORD set to match?"
        ) from e
    return _obs


def _flag(obj: object, *names: str, default=None):
    """First present attribute among `names` (obs-websocket responses vary snake/camel)."""
    for n in names:
        v = getattr(obj, n, None)
        if v is not None:
            return v
    return default


# ---------------------------------------------------------------------------------------------------
# Status / query
# ---------------------------------------------------------------------------------------------------
def obs_status() -> dict:
    """Current broadcast state: which scene is on program output, and whether streaming/recording is
    live (with elapsed time). Use this before acting so you never mistake a stale state for a live one."""
    c = _client().client
    stream = c.get_stream_status()
    record = c.get_record_status()
    return {
        "programScene": _client().current_program_scene(),
        "streaming": bool(_flag(stream, "output_active", default=False)),
        "streamTimecode": _flag(stream, "output_timecode", default=""),
        "reconnecting": bool(_flag(stream, "output_reconnecting", default=False)),
        "recording": bool(_flag(record, "output_active", default=False)),
        "recordTimecode": _flag(record, "output_timecode", default=""),
    }


def stream_health() -> dict:
    """OBS's OWN view of the outgoing stream: uptime, encoded bitrate (kbps), dropped-frame % (network
    congestion), and render/encoder frame drops (machine can't keep up). This is OBS-side telemetry, NOT
    YouTube's — it answers "is my upload healthy?", not "how many are watching?" (that needs the YouTube
    Data API — see the youtube_* tools if wired). Dropped % > ~1 or congestion near 1.0 = network trouble."""
    c = _client().client
    s = c.get_stream_status()
    stats = c.get_stats()
    sent = int(_flag(s, "output_total_frames", default=0) or 0)
    skipped = int(_flag(s, "output_skipped_frames", default=0) or 0)
    bytes_out = int(_flag(s, "output_bytes", default=0) or 0)
    dur_ms = int(_flag(s, "output_duration", default=0) or 0)
    kbps = round(bytes_out * 8 / dur_ms) if dur_ms > 0 else 0     # bytes→bits over ms → kbit/s
    return {
        "streaming": bool(_flag(s, "output_active", default=False)),
        "reconnecting": bool(_flag(s, "output_reconnecting", default=False)),
        "timecode": _flag(s, "output_timecode", default=""),
        "bitrateKbps": kbps,
        "droppedFramesPct": round(100 * skipped / sent, 2) if sent else 0.0,
        "congestion": round(float(_flag(s, "output_congestion", default=0.0) or 0.0), 3),
        "renderFps": round(float(_flag(stats, "active_fps", default=0.0) or 0.0), 1),
        "renderSkippedFrames": int(_flag(stats, "render_skipped_frames", default=0) or 0),
        "cpuUsagePct": round(float(_flag(stats, "cpu_usage", default=0.0) or 0.0), 1),
    }


def list_scenes() -> list[str]:
    """All scene names in the current OBS collection (e.g. 'Broadcast', 'Holding Card', 'Boat Cams')."""
    return sorted(_client()._scene_names())


# ---------------------------------------------------------------------------------------------------
# Direction — scenes + the broadcast source
# ---------------------------------------------------------------------------------------------------
def switch_scene(scene: str) -> str:
    """Cut the program output to `scene` (must be an existing scene — see list_scenes)."""
    scenes = _client()._scene_names()
    if scene not in scenes:
        return f"No scene named {scene!r}. Available: {sorted(scenes)}"
    _client().set_scene(scene)
    return f"Program output switched to {scene!r}."


def set_broadcast_url(url: str, source_name: str = "Broadcast") -> str:
    """Point the OBS Browser source `source_name` at a page URL (e.g. the live broadcast with a chosen
    world/lang: http://localhost:8765/core/web/index.html?feed=live&world=photoreal)."""
    _client().set_browser_source_url(source_name, url)
    return f"Browser source {source_name!r} now showing {url}"


# ---------------------------------------------------------------------------------------------------
# Output lifecycle (YouTube RTMP + local recording). Credentials live in OBS, never here.
# ---------------------------------------------------------------------------------------------------
def start_stream() -> str:
    """Start the RTMP stream to whatever OBS has configured (your YouTube ingest)."""
    st = obs_status()
    if st["streaming"]:
        return "Already streaming — no action taken."
    _client().start_stream()
    return "Stream started."


def stop_stream() -> str:
    """Stop the RTMP stream."""
    if not obs_status()["streaming"]:
        return "Not streaming — no action taken."
    _client().stop_stream()
    return "Stream stopped."


def start_record() -> str:
    """Start recording the broadcast to a local file (a no-YouTube dry run / archive)."""
    if obs_status()["recording"]:
        return "Already recording — no action taken."
    _client().start_record()
    return "Recording started."


def stop_record() -> str:
    """Stop the local recording."""
    if not obs_status()["recording"]:
        return "Not recording — no action taken."
    _client().stop_record()
    return "Recording stopped."


#: OBS tools split by risk. READ = safe anywhere (a monitor beside Johan's operator switcher). WRITE =
#: actuates the switcher / stream & recording — gated behind control mode so the agent can never cut a
#: scene or start/stop the stream out from under the human operator.
_OBS_READ = [obs_status, stream_health, list_scenes]
_OBS_WRITE = [switch_scene, set_broadcast_url, start_stream, stop_stream, start_record, stop_record]


def register(mcp: FastMCP, *, control: bool | None = None) -> None:
    """Add OBS tools to `mcp`: read tools always; write tools only when control is on (defaults to
    control_enabled()). Shared by this standalone server and the merged broadcast server."""
    if control is None:
        control = control_enabled()
    for fn in _OBS_READ:
        mcp.tool()(fn)
    if control:
        for fn in _OBS_WRITE:
            mcp.tool()(fn)


def main() -> None:
    mcp = FastMCP("obs-broadcast")
    register(mcp)
    mcp.run()  # stdio transport (the MCP client owns the process lifecycle)


if __name__ == "__main__":
    main()
