#!/usr/bin/env python3
"""Publish one camera view to a Farsight chamber over SRT. Requires FFmpeg."""

import argparse
import fcntl
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import Request, urlopen

API_BASE = os.environ.get("FARSIGHT_BASE_URL", "https://farsight.observer").rstrip("/")
KEY_FILE = Path(os.environ.get("FARSIGHT_KEY_FILE", "~/.config/farsight/api_key")).expanduser()


def camera_input():
    if sys.platform == "darwin":
        cameras = {
            "top": ["-f", "avfoundation", "-i", "0:none"],
            # "side": ["-f", "avfoundation", "-i", "1:none"],
            # "front": ["-f", "avfoundation", "-i", "2:none"],
        }
    elif sys.platform.startswith("linux"):
        cameras = {
            "top": ["-f", "v4l2", "-i", "/dev/video0"],
            # "side": ["-f", "v4l2", "-i", "/dev/video2"],
            # "front": ["-f", "v4l2", "-i", "/dev/video4"],
        }
    else:
        raise RuntimeError("Camera capture supports Linux and macOS")
    return cameras


def api_key():
    key = os.environ.get("FARSIGHT_API_KEY", "").strip()
    if not key and KEY_FILE.is_file():
        key = KEY_FILE.read_text(encoding="utf-8").strip()
    if not key:
        raise RuntimeError(f"Set FARSIGHT_API_KEY or save the key in {KEY_FILE}")
    return key


def stream_url(chamber_id, view_name, key):
    base = f"{API_BASE}/v1/chambers/{quote(chamber_id, safe='')}"
    headers = {"Authorization": f"Bearer {key}"}
    with urlopen(Request(base, headers=headers), timeout=30) as response:
        chamber = json.load(response)
    view = next((item for item in chamber.get("views", []) if item["name"].casefold() == view_name.casefold()), None)
    if view is None:
        with urlopen(Request(base + "/views", data=json.dumps({"name": view_name}).encode(),
                             headers={**headers, "Content-Type": "application/json"}, method="POST"), timeout=30) as response:
            view = json.load(response)
    if view["stream"]["enabled"]:
        url = view["stream"]["srt_url"]
        if url:
            return url
    with urlopen(Request(base + "/views/" + quote(view["id"], safe="") + "/stream",
                         data=b"", headers=headers, method="POST"), timeout=30) as response:
        stream = json.load(response)
    url = stream.get("srt_url")
    if not url:
        raise RuntimeError("Farsight returned no SRT URL")
    return url


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--chamber", default=os.environ.get("FARSIGHT_CHAMBER_ID"),
                        help="Chamber ID (or set FARSIGHT_CHAMBER_ID)")
    parser.add_argument("--view", default="top", help="Configured camera name")
    args = parser.parse_args()
    if not args.chamber:
        parser.error("set --chamber or FARSIGHT_CHAMBER_ID")
    cameras = camera_input()
    if args.view not in cameras:
        parser.error(f"unknown view {args.view!r}; configure it in camera_input()")

    # flock is released when this process exits. A second run cannot publish twice.
    lock_dir = Path(os.environ.get("XDG_RUNTIME_DIR", "~/.cache/farsight")).expanduser()
    lock_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
    lock_id = hashlib.sha256(f"{API_BASE}/{args.chamber}/{args.view.casefold()}".encode()).hexdigest()[:20]
    lock_path = lock_dir / f"stream-{lock_id}.lock"
    with lock_path.open("w") as lock:
        try:
            fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            raise RuntimeError(f"A stream for {args.chamber}/{args.view} is already running") from None
        url = stream_url(args.chamber, args.view, api_key())
        # Keep the SRT URL out of logs: it contains a publishing secret.
        command = ["ffmpeg", "-hide_banner", "-loglevel", "error", *cameras[args.view],
                   "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=48000",
                   "-map", "0:v:0", "-map", "1:a:0",
                   "-vf", "fps=10,setsar=1",
                   "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency",
                   "-b:v", "1000k", "-g", "30", "-keyint_min", "30", "-sc_threshold", "0",
                   "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "96k",
                   "-f", "mpegts", url]
        print(f"Publishing {args.view} to chamber {args.chamber}. Press Ctrl-C to stop.", flush=True)
        return subprocess.call(command)


if __name__ == "__main__":
    try:
        sys.exit(main())
    except (HTTPError, URLError, OSError, RuntimeError) as error:
        if isinstance(error, HTTPError):
            detail = error.read(1024).decode("utf-8", "replace")
            print(f"Farsight HTTP {error.code}: {detail}", file=sys.stderr)
        else:
            print(f"Error: {error}", file=sys.stderr)
        sys.exit(1)
