"""
Broadcast director agent  —  the autonomous co-pilot that closes the loop between the commentary AI and
the OBS/YouTube control the MCP servers expose.

The MCP servers (obs/youtube/broadcast_mcp_server) make the show agent-callable for a HUMAN driving Claude
Code. This runs the same tools under Claude PROGRAMMATICALLY: a bounded Tool Runner loop that, each pass,
reads `broadcast_health` (+ the OBS/YouTube reads) and reports — and, only on a VR-owned / unmanned box
with BROADCAST_ALLOW_CONTROL set, may take a clearly-correct action (cut to the holding card on an encode
stall, fix a wrong RTMP binding). It reuses the EXACT tool functions the MCP servers register (single
source of truth for behaviour), the process-wide Anthropic client from `commentary_llm`, and the one shared
control gate from `broadcast_control` — so it can never disagree with the MCP servers on what a tool does or
whether control is allowed.

Deliberately IN-PROCESS and synchronous (no second MCP transport, no cloud hop): per the TRWC compute
decision the director sits on the on-site box beside OBS, so wrapping the local functions directly is both
simpler and keeps the agent↔switch path off the network mid-show.

Read-only by default; `broadcast_health` degrades gracefully, so this is safe to leave running as a monitor
beside Johan's switcher. Run:
    .venv-demo/Scripts/python -m v3.broadcast.live.director_agent            # one read-only assessment
    BROADCAST_ALLOW_CONTROL=1 .venv-demo/Scripts/python -m v3...director_agent --loop 30   # co-pilot loop
Needs ANTHROPIC_API_KEY + the `anthropic` SDK (and OBS/YouTube reachable for real data).
"""
from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

from anthropic import beta_tool

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))                                   # obs/youtube/broadcast_control siblings
sys.path.insert(0, str(_HERE.parents[1] / "scoring"))           # commentary_llm (shared client + models)

import commentary_llm  # noqa: E402
import obs_mcp_server as _obs  # noqa: E402
import youtube_mcp_server as _yt  # noqa: E402
from broadcast_control import control_enabled  # noqa: E402
from broadcast_mcp_server import broadcast_health as _broadcast_health  # noqa: E402

MODEL_MONITOR = commentary_llm.MODEL_LIVE      # read-only glance — cheap/fast is plenty
MODEL_CONTROL = commentary_llm.MODEL_OFFLINE   # may actuate the show — better judgement

_SYSTEM = (
    "You are the broadcast director's co-pilot for a live sailing broadcast (OBS + YouTube). Each turn, "
    "call broadcast_health first for the fused OBS-upload + YouTube-ingest/audience status, and use the "
    "other read tools to confirm anything it flags. Then report, in 1-3 sentences: is the broadcast "
    "healthy, and if not, what is wrong and the most likely cause. Encode the known cross-check — OBS "
    "upload healthy but YouTube ingest 'noData'/'bad' means the RTMP binding is wrong, not the network.\n"
    "If (and only if) write tools are available to you, you may take a SINGLE clearly-correct corrective "
    "action when a problem has an obvious fix (e.g. cut to a holding card on an encode stall). Never take "
    "the broadcast public or end it unprompted, never act on ambiguity, and prefer reporting over acting. "
    "If no write tools are present you are a monitor only — report and stop."
)


def _safe(fn, *args, **kwargs) -> str:
    """Run a tool function and return its result as JSON — turning any failure (e.g. OBS unreachable) into
    an observation the agent can report, instead of an exception that would abort the runner."""
    try:
        return json.dumps(fn(*args, **kwargs))
    except Exception as e:  # noqa: BLE001 — a tool error is data for the agent, not a crash
        return json.dumps({"error": str(e)})


# --- READ tools (always available) — thin wrappers so the schema/description come from here, but the
#     behaviour is the MCP servers' own functions verbatim. ---------------------------------------------
@beta_tool
def broadcast_health() -> str:
    """Fused OBS-upload + YouTube-ingest/audience status with alert flags. Call this FIRST each turn."""
    return _safe(_broadcast_health)


@beta_tool
def obs_status() -> str:
    """OBS program scene + whether streaming/recording is live (with elapsed time)."""
    return _safe(_obs.obs_status)


@beta_tool
def stream_health() -> str:
    """OBS's own upload telemetry: bitrate, dropped-frame %, render/encoder drops, CPU."""
    return _safe(_obs.stream_health)


@beta_tool
def list_scenes() -> str:
    """All OBS scene names in the current collection (e.g. Broadcast, Holding Card, Boat Cams)."""
    return _safe(_obs.list_scenes)


@beta_tool
def youtube_broadcast_info() -> str:
    """The current/next YouTube broadcast: id, title, privacy, lifecycle status, watch URL."""
    return _safe(_yt.youtube_broadcast_info)


@beta_tool
def youtube_stream_stats() -> str:
    """YouTube-side telemetry: concurrent viewers + YouTube's own ingest-health verdict and issues."""
    return _safe(_yt.youtube_stream_stats)


# --- WRITE tools (only registered when control is enabled) ----------------------------------------------
@beta_tool
def switch_scene(scene: str) -> str:
    """Cut the OBS program output to an existing scene (see list_scenes)."""
    return _safe(_obs.switch_scene, scene)


@beta_tool
def set_broadcast_url(url: str) -> str:
    """Point the OBS 'Broadcast' Browser source at a page URL (e.g. the live page with a chosen world)."""
    return _safe(_obs.set_broadcast_url, url)


@beta_tool
def start_stream() -> str:
    """Start the RTMP stream to whatever YouTube ingest OBS has configured."""
    return _safe(_obs.start_stream)


@beta_tool
def stop_stream() -> str:
    """Stop the RTMP stream."""
    return _safe(_obs.stop_stream)


@beta_tool
def youtube_set_metadata(title: str | None = None, description: str | None = None,
                         privacy: str | None = None) -> str:
    """Update the live YouTube broadcast's title / description / privacy (only the fields you pass change)."""
    return _safe(_yt.youtube_set_metadata, title, description, privacy)


@beta_tool
def youtube_transition(status: str) -> str:
    """Move the YouTube broadcast lifecycle: 'testing' | 'live' (PUBLIC) | 'complete'. Use with care."""
    return _safe(_yt.youtube_transition, status)


_READ = [broadcast_health, obs_status, stream_health, list_scenes,
         youtube_broadcast_info, youtube_stream_stats]
_WRITE = [switch_scene, set_broadcast_url, start_stream, stop_stream,
          youtube_set_metadata, youtube_transition]


class DirectorAgent:
    """Runs one bounded assessment pass (`assess()`) or a monitor loop (`monitor()`). Read-only unless the
    shared control gate is on; write tools are only ever handed to Claude when `control` is True."""

    def __init__(self, *, control: bool | None = None, model: str | None = None, max_tokens: int = 4000):
        if not commentary_llm.available():
            raise RuntimeError("director_agent needs ANTHROPIC_API_KEY and the anthropic SDK.")
        self.control = control_enabled() if control is None else control
        self.tools = _READ + (_WRITE if self.control else [])
        self.model = model or (MODEL_CONTROL if self.control else MODEL_MONITOR)
        self.max_tokens = max_tokens

    def assess(self) -> str:
        """One pass: read health, report (and possibly act if control is on). Returns the agent's text."""
        runner = commentary_llm.client().beta.messages.tool_runner(
            model=self.model,
            max_tokens=self.max_tokens,
            system=_SYSTEM,
            tools=self.tools,
            messages=[{"role": "user", "content": "Assess the broadcast now."}],
        )
        final = runner.until_done()
        return "".join(b.text for b in final.content if getattr(b, "type", None) == "text").strip()

    def monitor(self, interval_s: float, iterations: int | None = None) -> None:
        """Assess every `interval_s` seconds, printing each report; `iterations` caps the loop (None = run
        until interrupted). Scheduling deliberately lives here, not in the agent, so a caller can drive it."""
        n = 0
        while iterations is None or n < iterations:
            print(f"[{n}] {self.assess()}", flush=True)
            n += 1
            if iterations is not None and n >= iterations:
                break
            time.sleep(interval_s)


def main() -> None:
    ap = argparse.ArgumentParser(description="Broadcast director co-pilot (read-only unless "
                                             "BROADCAST_ALLOW_CONTROL is set).")
    ap.add_argument("--loop", type=float, metavar="SECONDS",
                    help="monitor on this interval instead of a single assessment")
    ap.add_argument("--iterations", type=int, default=None, help="cap the monitor loop (default: forever)")
    ap.add_argument("--model", default=None, help="override the model id")
    args = ap.parse_args()

    agent = DirectorAgent(model=args.model)
    mode = "CONTROL (may actuate)" if agent.control else "read-only monitor"
    print(f"director_agent: {mode}, model={agent.model}", flush=True)
    if args.loop:
        agent.monitor(args.loop, args.iterations)
    else:
        print(agent.assess(), flush=True)


if __name__ == "__main__":
    main()
