#!/usr/bin/env python3
"""Send one JSON message through a local Fleet Manager using only Python's standard library."""

from __future__ import annotations

import argparse
import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

API_PREFIX = "/ApiGateway/V2"


def request_json(
    base_url: str,
    path: str,
    token: str = "",
    body: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Make one bounded REST call; successful uploads have an empty response body."""
    headers = {"Authorization": token} if token else {}
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body, allow_nan=False).encode("utf-8")
    request = urllib.request.Request(
        base_url.rstrip("/") + API_PREFIX + path,
        data=data,
        headers=headers,
        method="POST" if body is not None else "GET",
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            content = response.read()
            return json.loads(content) if content else {}
    except urllib.error.HTTPError as error:
        detail = error.read().decode("utf-8", errors="replace")
        raise SystemExit(f"HTTP {error.code}: {detail}") from error
    except urllib.error.URLError as error:
        raise SystemExit(f"Connection failed: {error.reason}") from error


def uploadable_streams(streams: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Offer discovered component/ROS inputs; video and output streams are excluded."""
    return [
        stream for stream in streams
        if stream.get("direction", "").lower() == "input"
        and stream.get("streamSourceType") in (1, 2, 3)
        and stream.get("streamType")
    ]


def main(argv: list[str] | None = None) -> None:
    """Discover explicit targets, then publish once without retrying a robot command."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("local_user_token", help="Fleet Manager password or local user token")
    parser.add_argument("base_url", help="Fleet Manager URL, e.g. http://fleet-manager:19992")
    parser.add_argument("robot_id", nargs="?", help="omit to list robots")
    parser.add_argument("stream_name", nargs="?", help="omit to list the robot's input streams")
    parser.add_argument("--component-name", default="", help="descriptor componentName; empty for ROS")
    parser.add_argument("--source", type=int, choices=(1, 2, 3), help="disambiguate component/ROS1/ROS2")
    parser.add_argument("--message", help="one JSON object matching the selected stream's message type")
    args = parser.parse_args(argv)

    message = None
    if args.stream_name:
        if args.message is None:
            parser.error("--message is required when a stream is selected")
        try:
            message = json.loads(args.message)
            if not isinstance(message, dict):
                raise ValueError("message must be a JSON object")
            # JSON's non-finite extensions are not valid robot payloads.
            json.dumps(message, allow_nan=False)
        except ValueError as error:
            parser.error(str(error))
    elif args.message is not None:
        parser.error("select a robot and stream before supplying --message")

    login = request_json(args.base_url, "/auth/login", body={"userToken": args.local_user_token})
    token = login.get("token") or ""
    if not token:
        raise SystemExit("Login response did not include a token.")
    # The login response already includes the bearer prefix.
    robots = request_json(args.base_url, "/robots", token).get("robotDataList") or []
    if not args.robot_id:
        print(json.dumps(robots, indent=2))
        return
    if not any(robot.get("basicData", {}).get("id") == args.robot_id for robot in robots):
        raise SystemExit("Robot is not in the Fleet Manager's robot list.")

    robot_path = "/robots/" + urllib.parse.quote(args.robot_id, safe="")
    response = request_json(args.base_url, robot_path + "/streams", token)
    streams = uploadable_streams(response.get("robotStreams") or [])
    if not args.stream_name:
        print(json.dumps(streams, indent=2))
        return

    matches = [
        stream for stream in streams
        if stream.get("streamName") == args.stream_name
        and (stream.get("componentName") or "") == args.component_name
        and (args.source is None or stream["streamSourceType"] == args.source)
    ]
    if len(matches) != 1:
        raise SystemExit("Select exactly one listed input stream using its name, component, and source.")
    selected = matches[0]
    body = {
        "componentName": selected.get("componentName") or "",
        "streamName": selected["streamName"],
        "source": str(selected["streamSourceType"]),
        "dataType": selected["streamType"],
        # The public REST DTO expects the message JSON encoded inside a string field.
        "streamJson": json.dumps(message, allow_nan=False),
    }
    request_json(args.base_url, robot_path + "/upload-stream", token, body)
    print(f"Message published to robot {args.robot_id}, stream {args.stream_name}.")


if __name__ == "__main__":
    main()
