from __future__ import annotations

import asyncio
from typing import List, Optional

import typer

from remotivelabs.broker.client import BrokerClient
from remotivelabs.broker.restbus import RestbusFrameConfig, RestbusSignalConfig
from remotivelabs.cli.broker.typer import ApiKeyOption, BrokerUrlOption, FrameSpec, FrameSpecType, SignalSpec, SignalSpecType, get_auth
from remotivelabs.cli.typer_ext import typer_utils
from remotivelabs.cli.utils.console import print_generic_error, print_generic_message

app = typer_utils.create_typer(
    help=(
        "Control periodic frame transmission on a broker. "
        "Restbus lets you inject frames onto a bus at a fixed cycle time, "
        "optionally overriding signal values. Typical workflow: add → start → update → stop/reset."
    )
)


@app.command()
def add(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    frame: List[FrameSpec] = typer.Option(..., click_type=FrameSpecType, help="Frame(s) to add (namespace:frame_name)"),
    cycle_time: float | None = typer.Option(None, help="Cycle time override in ms"),
    start: bool = typer.Option(False, help="Start frames after adding"),
) -> None:
    """Register one or more frames (with optional cycle time override)."""

    async def _run() -> None:
        by_ns: dict[str, list[RestbusFrameConfig]] = {}
        for spec in frame:
            by_ns.setdefault(spec.namespace, []).append(RestbusFrameConfig(name=spec.name, cycle_time=cycle_time))
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            for ns, configs in by_ns.items():
                await client.restbus.add((ns, configs), start=start)
            print_generic_message(f"Added {len(frame)} frame(s)")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)


@app.command()
def start(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    namespace: List[str] = typer.Option(..., help="Namespace(s) to start"),
) -> None:
    """Begin transmitting registered frames on the specified namespace(s)."""

    async def _run() -> None:
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            await client.restbus.start(*namespace)
            print_generic_message(f"Started restbus for namespace(s): {', '.join(namespace)}")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)


@app.command()
def stop(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    namespace: List[str] = typer.Option(..., help="Namespace(s) to stop"),
) -> None:
    """Pause transmission without removing configuration."""

    async def _run() -> None:
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            await client.restbus.stop(*namespace)
            print_generic_message(f"Stopped restbus for namespace(s): {', '.join(namespace)}")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)


@app.command()
def update(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    signal: List[SignalSpec] = typer.Option(..., click_type=SignalSpecType, help="Signal(s) to update (loop values)"),
    initial: Optional[List[SignalSpec]] = typer.Option(
        None, click_type=SignalSpecType, help="Signal(s) initial values (emitted once before loop)"
    ),
) -> None:
    """Change signal values to be emitted by the restbus."""

    async def _run() -> None:
        initial_by_key = {(s.namespace, s.name): s.value for s in (initial or [])}
        by_ns: dict[str, list[RestbusSignalConfig]] = {}
        for spec in signal:
            key = (spec.namespace, spec.name)
            by_ns.setdefault(spec.namespace, []).append(
                RestbusSignalConfig(name=spec.name, loop=spec.value, initial=initial_by_key.get(key, []))
            )
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            await client.restbus.update_signals(*by_ns.items())
            print_generic_message(f"Updated {len(signal)} signal(s)")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)


@app.command()
def remove(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    frame: List[FrameSpec] = typer.Option(..., click_type=FrameSpecType, help="Frame(s) to remove (namespace:frame_name)"),
) -> None:
    """Remove specific frames from the restbus.

    Unregisters the named frames from the given namespace. Unlike 'reset',
    this only removes the specified frames and leaves others intact.
    """

    async def _run() -> None:
        by_ns: dict[str, list[str]] = {}
        for spec in frame:
            by_ns.setdefault(spec.namespace, []).append(spec.name)
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            for ns, names in by_ns.items():
                await client.restbus.remove((ns, names))
            print_generic_message(f"Removed {len(frame)} frame(s)")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)


@app.command()
def reset(
    url: str = BrokerUrlOption,
    api_key: str = ApiKeyOption,
    namespace: List[str] = typer.Option(..., help="Namespace(s) to reset"),
) -> None:
    """Clear all configuration for a namespace."""

    async def _run() -> None:
        async with BrokerClient(url, auth=get_auth(url, api_key)) as client:
            await client.restbus.reset_namespaces(*namespace)
            print_generic_message(f"Reset namespace(s): {', '.join(namespace)}")

    try:
        asyncio.run(_run())
    except Exception as e:
        print_generic_error(str(e))
        raise typer.Exit(1)
