"""
YouTube live MCP server  —  the YouTube-side of the broadcast, as agent tools.

Sibling of `obs_mcp_server.py`. That one drives OBS (the outgoing pixels + local scenes); THIS one drives
what YouTube knows and shows: the concurrent-viewer count, YouTube's own stream-health verdict, the
broadcast's title/description/privacy, and the lifecycle transition (testing → live → complete). Two
different systems — OBS `start_stream` pushes RTMP; YouTube `go_live` makes the broadcast public — so
they are deliberately two servers an agent can hold at once.

Backed by the **YouTube Data API v3 / Live Streaming API** (google-api-python-client), authorised to the
channel that owns the stream. That needs OAuth (an API key can only READ public data, never update or
transition), so auth is a one-time interactive step done via the CLI; the server itself runs headless and
just loads the stored refresh token.

    GCP project: apps-viewregatta  (enable "YouTube Data API v3"; create an OAuth 2.0 Client ID of type
    "Desktop app" and download its client_secrets.json).

One-time authorise (opens a browser, writes the token file):
    .venv-demo/Scripts/python -m v3.broadcast.live.youtube_mcp_server auth

Then run as an MCP server (stdio):
    .venv-demo/Scripts/python -m v3.broadcast.live.youtube_mcp_server

Config (env; secrets stay out of code/logs — mirror the rest of the stack, e.g. Secret Manager via
secret_refs.py in production):
    YOUTUBE_CLIENT_SECRETS   path to the OAuth client_secrets.json (needed only for `auth`)
    YOUTUBE_TOKEN_FILE       where the authorised token is stored/loaded (default: ./.youtube_token.json)
    YOUTUBE_BROADCAST_ID     optional — pin a specific broadcast instead of auto-finding the active one
    # Headless alternative to the token file (e.g. from Secret Manager):
    YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET / YOUTUBE_REFRESH_TOKEN

Deps: `pip install google-api-python-client google-auth-oauthlib "mcp[cli]"`.

Register with an MCP client (Claude Code), alongside the OBS server:
    claude mcp add youtube-live -- <python> -m v3.broadcast.live.youtube_mcp_server
"""
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 `broadcast_control` resolves either way
from broadcast_control import control_enabled as _control_enabled  # noqa: E402  (single shared gate)

# Manage scope: read + update broadcasts, transition lifecycle, and (later) live chat. `youtube.readonly`
# would NOT allow update/transition, so we take force-ssl even for the read tools to keep one token.
_SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"]

_service = None  # lazily-built googleapiclient resource


def _token_path() -> Path:
    return Path(os.environ.get("YOUTUBE_TOKEN_FILE", ".youtube_token.json")).expanduser()


def _load_credentials():
    """Load OAuth credentials from env (Secret-Manager-friendly) or the token file, refreshing if stale.
    Raises a clear, actionable error if the channel hasn't been authorised yet."""
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials

    creds = None
    rt = os.environ.get("YOUTUBE_REFRESH_TOKEN")
    if rt and os.environ.get("YOUTUBE_CLIENT_ID") and os.environ.get("YOUTUBE_CLIENT_SECRET"):
        creds = Credentials(
            token=None, refresh_token=rt,
            token_uri="https://oauth2.googleapis.com/token",
            client_id=os.environ["YOUTUBE_CLIENT_ID"],
            client_secret=os.environ["YOUTUBE_CLIENT_SECRET"],
            scopes=_SCOPES,
        )
    else:
        tp = _token_path()
        if tp.exists():
            creds = Credentials.from_authorized_user_file(str(tp), _SCOPES)
    if creds is None:
        raise RuntimeError(
            "YouTube not authorised yet. Run once:  python -m v3.broadcast.live.youtube_mcp_server auth "
            "(set YOUTUBE_CLIENT_SECRETS to your OAuth client_secrets.json first), or provide "
            "YOUTUBE_CLIENT_ID/SECRET/REFRESH_TOKEN via env."
        )
    if not creds.valid and creds.refresh_token:
        creds.refresh(Request())
        if not os.environ.get("YOUTUBE_REFRESH_TOKEN"):   # persist the refreshed token when file-backed
            _token_path().write_text(creds.to_json(), encoding="utf-8")
    return creds


def _yt():
    """Return a cached, authorised YouTube Data API v3 client."""
    global _service
    if _service is None:
        from googleapiclient.discovery import build
        _service = build("youtube", "v3", credentials=_load_credentials(), cache_discovery=False)
    return _service


def _active_broadcast(parts: str = "id,snippet,status,contentDetails") -> dict:
    """The broadcast to act on: the pinned YOUTUBE_BROADCAST_ID, else the channel's currently ACTIVE
    (on-air) broadcast, else the next UPCOMING one. Raises if the channel has neither."""
    yt = _yt()
    pinned = os.environ.get("YOUTUBE_BROADCAST_ID")
    if pinned:
        items = yt.liveBroadcasts().list(part=parts, id=pinned).execute().get("items", [])
        if items:
            return items[0]
        raise RuntimeError(f"No broadcast with id {pinned!r} on this channel.")
    for status in ("active", "upcoming"):
        items = (yt.liveBroadcasts()
                 .list(part=parts, broadcastStatus=status, broadcastType="all", maxResults=1)
                 .execute().get("items", []))
        if items:
            return items[0]
    raise RuntimeError("No active or upcoming broadcast on this channel. Create one in YouTube Studio "
                       "(or set YOUTUBE_BROADCAST_ID).")


# ---------------------------------------------------------------------------------------------------
# Read — audience + YouTube's view of stream health
# ---------------------------------------------------------------------------------------------------
def youtube_broadcast_info() -> dict:
    """The current/next broadcast's identity as YouTube sees it: id, title, privacy, and lifecycle
    status (created / ready / testing / live / complete). Use before set/transition so you act on the
    right broadcast and don't, say, re-`go_live` one already live."""
    b = _active_broadcast()
    return {
        "id": b["id"],
        "title": b["snippet"].get("title"),
        "scheduledStart": b["snippet"].get("scheduledStartTime"),
        "privacy": b["status"].get("privacyStatus"),
        "lifeCycleStatus": b["status"].get("lifeCycleStatus"),
        "watchUrl": f"https://www.youtube.com/watch?v={b['id']}",
    }


def youtube_stream_stats() -> dict:
    """YouTube-side telemetry (NOT OBS's — see obs stream_health for the upload side): live concurrent
    viewers, and YouTube's own ingest health verdict (good / ok / bad / noData) plus any configuration
    issues it reports. 'noData'/'bad' here while OBS says the upload is fine points at the RTMP binding."""
    b = _active_broadcast("id,contentDetails")
    yt = _yt()
    # concurrent viewers live in the video's liveStreamingDetails (broadcast id == video id).
    viewers = None
    vids = yt.videos().list(part="liveStreamingDetails", id=b["id"]).execute().get("items", [])
    if vids:
        viewers = vids[0].get("liveStreamingDetails", {}).get("concurrentViewers")
    # ingest health lives on the bound stream, not the broadcast.
    health, issues = "unknown", []
    bound = b.get("contentDetails", {}).get("boundStreamId")
    if bound:
        streams = yt.liveStreams().list(part="status", id=bound).execute().get("items", [])
        if streams:
            hs = streams[0].get("status", {}).get("healthStatus", {})
            health = hs.get("status", "unknown")
            issues = [i.get("reason") or i.get("description") for i in hs.get("configurationIssues", [])]
    return {
        "concurrentViewers": int(viewers) if viewers is not None else None,
        "ingestHealth": health,
        "configurationIssues": issues,
    }


# ---------------------------------------------------------------------------------------------------
# Write — metadata + lifecycle
# ---------------------------------------------------------------------------------------------------
def youtube_set_metadata(title: str | None = None, description: str | None = None,
                         privacy: str | None = None) -> str:
    """Update the live broadcast's title / description / privacy ('public' | 'unlisted' | 'private').
    Only the fields you pass change; the rest are preserved. (snippet.title and scheduledStartTime are
    required by the API on update, so we read-modify-write the current values.)"""
    if privacy and privacy not in ("public", "unlisted", "private"):
        return f"privacy must be public|unlisted|private, not {privacy!r}."
    b = _active_broadcast("id,snippet,status")
    snip = b["snippet"]
    body = {
        "id": b["id"],
        "snippet": {
            "title": title if title is not None else snip.get("title"),
            "description": description if description is not None else snip.get("description", ""),
            "scheduledStartTime": snip.get("scheduledStartTime"),   # required field, keep as-is
        },
    }
    parts = "snippet"
    if privacy:
        body["status"] = {"privacyStatus": privacy}
        parts = "snippet,status"
    _yt().liveBroadcasts().update(part=parts, body=body).execute()
    changed = [k for k, v in (("title", title), ("description", description), ("privacy", privacy)) if v]
    return f"Updated {', '.join(changed) or 'nothing'} on broadcast {b['id']}."


def youtube_transition(status: str) -> str:
    """Move the broadcast's lifecycle: 'testing' (preview, private monitor), 'live' (PUBLIC — the stream
    goes out to viewers), or 'complete' (end the broadcast). YouTube requires the bound stream to be
    receiving data (OBS already streaming) before 'live' will succeed."""
    if status not in ("testing", "live", "complete"):
        return f"status must be testing|live|complete, not {status!r}."
    b = _active_broadcast("id,status")
    cur = b["status"].get("lifeCycleStatus")
    if status == "live" and cur == "live":
        return "Broadcast is already live — no action taken."
    try:
        _yt().liveBroadcasts().transition(
            broadcastStatus=status, id=b["id"], part="id,status").execute()
    except Exception as e:   # surface YouTube's reason (e.g. redundant transition / stream inactive)
        return f"Transition to {status!r} failed: {e}"
    return f"Broadcast {b['id']} transitioned to {status!r}."


def youtube_go_live() -> str:
    """Shortcut for youtube_transition('live') — make the broadcast PUBLIC. Ensure OBS is already
    streaming (obs start_stream) and ingest health is good first, or YouTube rejects the transition."""
    return youtube_transition("live")


def _run_oauth() -> None:
    """One-time interactive OAuth: open a browser, authorise the channel, write the token file."""
    from google_auth_oauthlib.flow import InstalledAppFlow

    secrets = os.environ.get("YOUTUBE_CLIENT_SECRETS")
    if not secrets or not Path(secrets).exists():
        raise SystemExit("Set YOUTUBE_CLIENT_SECRETS to your OAuth client_secrets.json "
                         "(GCP project apps-viewregatta -> APIs & Services -> Credentials -> Desktop app).")
    flow = InstalledAppFlow.from_client_secrets_file(secrets, _SCOPES)
    creds = flow.run_local_server(port=0)
    tp = _token_path()
    tp.write_text(creds.to_json(), encoding="utf-8")
    print(f"Authorised. Token written to {tp}. You can now run the MCP server.")


#: YouTube tools split by risk. READ = safe anywhere. WRITE = changes the public broadcast (title,
#: privacy, go-live/end) — gated behind control mode so an agent can't take the broadcast public or end
#: it out from under the operator. (For TRWC these belong to a VR-owned / unmanned stream, not Johan's
#: switcher — see docs/plans/trwc-obs-production-workflow.md.)
_YT_READ = [youtube_broadcast_info, youtube_stream_stats]
_YT_WRITE = [youtube_set_metadata, youtube_transition, youtube_go_live]


def register(mcp: FastMCP, *, control: bool | None = None) -> None:
    """Add YouTube tools to `mcp`: read tools always; write tools only when control is on (defaults to
    the BROADCAST_ALLOW_CONTROL env). Shared by this standalone server and broadcast_mcp_server.py."""
    if control is None:
        control = _control_enabled()
    for fn in _YT_READ:
        mcp.tool()(fn)
    if control:
        for fn in _YT_WRITE:
            mcp.tool()(fn)


def main() -> None:
    if len(sys.argv) > 1 and sys.argv[1] == "auth":
        _run_oauth()
        return
    mcp = FastMCP("youtube-live")
    register(mcp)
    mcp.run()


if __name__ == "__main__":
    main()
