"""Command line entry point for a2a-agent."""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path

import typer

from a2a_agent import __version__
from a2a_agent import autostart
from a2a_agent.config import AgentConfig
from a2a_agent.config import BrokerConfig
from a2a_agent.config import DEFAULT_BROKER_HOST
from a2a_agent.config import DEFAULT_BROKER_PORT
from a2a_agent.config import DEFAULT_EXECUTOR
from a2a_agent.config import DEFAULT_ROOM
from a2a_agent.config import DeviceRole
from a2a_agent.config import load_config
from a2a_agent.config import save_config
from a2a_agent.executor import selected_executor
from a2a_agent.pairing import add_pending_pairing
from a2a_agent.runtime import join_pairing
from a2a_agent.runtime import resolve_device_name
from a2a_agent.runtime import run_commander
from a2a_agent.runtime import run_worker
from a2a_agent.runtime import SendResult
from a2a_agent.runtime import send_task
from a2a_agent.updater import maybe_schedule_auto_update


app = typer.Typer(
    no_args_is_help=True,
    invoke_without_command=True,
    help="A2A messages over MQTT.",
)


def _print_send_result(result: SendResult) -> None:
    typer.echo(f"task: {result.task_id}")
    typer.echo(f"status: {result.status}")
    typer.echo("response:")
    typer.echo(result.text)
    if result.downloaded_files:
        typer.echo("downloaded files:")
        for path in result.downloaded_files:
            typer.echo(f"  - {path}")
    if result.file_errors:
        typer.echo("file warnings:", err=True)
        for error in result.file_errors:
            typer.echo(f"  - {error}", err=True)


@app.callback()
def main(
    version: bool = typer.Option(False, "--version", help="Show package version."),
) -> None:
    if version:
        typer.echo(__version__)
        raise typer.Exit()


@app.command()
def init(
    room: str = typer.Option(DEFAULT_ROOM, help="A2A room name."),
    device_name: str = typer.Option("commander", help="Local device name."),
    broker_host: str = typer.Option(DEFAULT_BROKER_HOST, help="MQTT broker host."),
    broker_port: int = typer.Option(DEFAULT_BROKER_PORT, help="MQTT broker port."),
    tls: bool = typer.Option(False, "--tls/--no-tls", help="Use TLS for MQTT."),
    executor: str = typer.Option(DEFAULT_EXECUTOR, help="Worker executor."),
) -> None:
    path = save_config(
        AgentConfig(
            room=room,
            device_name=device_name,
            broker=BrokerConfig(host=broker_host, port=broker_port, tls=tls),
            role="commander",
            executor=executor,
        )
    )
    typer.echo(f"saved commander config: {path}")


@app.command()
def join(invite_code: str = typer.Argument(..., help="6-character pairing code.")) -> None:
    try:
        path = join_pairing(invite_code)
    except (RuntimeError, TimeoutError, ValueError) as exc:
        typer.echo(f"join failed: {exc}", err=True)
        raise typer.Exit(1) from exc
    typer.echo(f"saved worker config: {path}")


@app.command()
def invite(device_name: str = typer.Argument(..., help="Device name to invite.")) -> None:
    config = load_config()
    if config.role != "commander":
        raise typer.BadParameter("invite requires commander config")

    code, updated = add_pending_pairing(config, device_name)
    save_config(updated)
    typer.echo(code)


@app.command("set-role")
def set_role(
    device_name: str = typer.Argument(..., help="Worker device name."),
    role: str = typer.Argument(..., help="Role name managed by commander."),
    description: str = typer.Argument(..., help="Role description."),
) -> None:
    config = load_config()
    if config.role != "commander":
        raise typer.BadParameter("set-role requires commander config")

    devices = dict(config.devices)
    devices[device_name] = DeviceRole(role=role, description=description)
    path = save_config(replace(config, devices=devices))
    typer.echo(f"saved role for {device_name}: {path}")


@app.command("set-alias")
def set_alias(
    alias: str = typer.Argument(..., help="Human-friendly alias managed by commander."),
    device_name: str = typer.Argument(..., help="Registered worker device name."),
) -> None:
    config = load_config()
    if config.role != "commander":
        raise typer.BadParameter("set-alias requires commander config")
    if config.devices and device_name not in config.devices:
        raise typer.BadParameter(f"unknown device: {device_name}")

    aliases = dict(config.aliases)
    aliases[alias] = device_name
    path = save_config(replace(config, aliases=aliases))
    typer.echo(f"saved alias {alias} -> {device_name}: {path}")


@app.command("set-executor")
def set_executor(
    executor: str = typer.Argument(..., help="Executor name to save in local config."),
) -> None:
    """Persist the executor used when A2A_EXECUTOR is not set."""

    normalized_executor = executor.strip().lower()
    if not normalized_executor:
        raise typer.BadParameter("executor must not be empty")

    config = load_config()
    path = save_config(replace(config, executor=normalized_executor))
    typer.echo(f"saved executor: {normalized_executor}")
    typer.echo(f"saved config: {path}")


@app.command("set-cursor-key")
def set_cursor_key(
    cursor_api_key: str = typer.Argument(
        ...,
        help="Cursor API key to save in local config.",
    ),
) -> None:
    """Persist a Cursor API key without printing it."""

    if not cursor_api_key or any(
        character.isspace() for character in cursor_api_key
    ):
        raise typer.BadParameter(
            "cursor api key must not be empty or contain whitespace"
        )

    config = load_config()
    path = save_config(replace(config, cursor_api_key=cursor_api_key))
    typer.echo(f"saved config: {path}")


@app.command("set-ollama-key")
def set_ollama_key(
    ollama_api_key: str = typer.Argument(
        ...,
        help="Ollama API key to save in local config.",
    ),
) -> None:
    """Persist an Ollama API key without printing it."""

    if not ollama_api_key or any(
        character.isspace() for character in ollama_api_key
    ):
        raise typer.BadParameter(
            "ollama api key must not be empty or contain whitespace"
        )

    config = load_config()
    path = save_config(replace(config, ollama_api_key=ollama_api_key))
    typer.echo(f"saved config: {path}")


@app.command("set-ollama-model")
def set_ollama_model(
    ollama_model: str = typer.Argument(
        ...,
        help="Ollama model to save in local config.",
    ),
) -> None:
    """Persist an Ollama model without printing it."""

    normalized_model = ollama_model.strip()
    if not normalized_model:
        raise typer.BadParameter("ollama model must not be empty")

    config = load_config()
    path = save_config(replace(config, ollama_model=normalized_model))
    typer.echo(f"saved config: {path}")


@app.command()
def send(
    device_name: str = typer.Argument(..., help="Worker device name."),
    text: str = typer.Argument(..., help="Message text."),
    files: list[Path] | None = typer.Option(
        None,
        "--file",
        "-f",
        exists=True,
        dir_okay=False,
        readable=True,
        help="Attach a local file. Can be used multiple times.",
    ),
) -> None:
    try:
        result = send_task(device_name, text, files=files)
    except TimeoutError as exc:
        typer.echo(f"send timed out: {exc}", err=True)
        raise typer.Exit(1) from exc
    except RuntimeError as exc:
        typer.echo(f"send failed: {exc}", err=True)
        raise typer.Exit(1) from exc

    _print_send_result(result)


@app.command()
def control(
    device_name: str = typer.Argument(..., help="Worker device name."),
    control_spec: str = typer.Argument(
        ...,
        help="Built-in worker control name[:argument].",
    ),
) -> None:
    """Send a fixed worker control task without an AI executor prompt."""

    try:
        result = send_task(device_name, "", control=control_spec)
    except TimeoutError as exc:
        typer.echo(f"control timed out: {exc}", err=True)
        raise typer.Exit(1) from exc
    except RuntimeError as exc:
        typer.echo(f"control failed: {exc}", err=True)
        raise typer.Exit(1) from exc

    _print_send_result(result)
    if result.status != "completed":
        raise typer.Exit(1)


@app.command()
def run() -> None:
    config = load_config()
    if maybe_schedule_auto_update():
        raise typer.Exit()
    try:
        if config.role == "commander":
            run_commander()
            return
        run_worker()
    except RuntimeError as exc:
        typer.echo(f"run failed: {exc}", err=True)
        raise typer.Exit(1) from exc


@app.command("install-autostart")
def install_autostart() -> None:
    """Register this worker to start at the next user login."""

    config = load_config()
    if config.role != "worker":
        typer.echo("autostart install failed: worker config required", err=True)
        raise typer.Exit(1)
    executor = selected_executor(config)
    try:
        result = autostart.install_autostart(executor)
    except autostart.AutostartError as exc:
        typer.echo(f"autostart install failed: {exc}", err=True)
        raise typer.Exit(1) from exc

    typer.echo(f"registered path: {result.path}")
    typer.echo(f"executor: {executor}")
    if result.backup_path is not None:
        typer.echo(f"backup: {result.backup_path}")
    if result.manual_command is not None:
        typer.echo(f"manual enable required: {result.manual_command}")
    else:
        typer.echo("effective: next login or boot")
    typer.echo("current worker: not started")


@app.command("uninstall-autostart")
def uninstall_autostart() -> None:
    """Remove this worker's login registration without stopping it."""

    try:
        result = autostart.uninstall_autostart()
    except autostart.AutostartError as exc:
        typer.echo(f"autostart uninstall failed: {exc}", err=True)
        raise typer.Exit(1) from exc

    if result.manual_command is not None:
        typer.echo(f"registration removal incomplete: {result.path}")
        typer.echo(f"manual disable required: {result.manual_command}")
    else:
        typer.echo(f"removed path: {result.path}")
    typer.echo("current worker: not stopped")


@app.command()
def status() -> None:
    config = load_config()
    typer.echo(f"role: {config.role}")
    typer.echo(f"room: {config.room}")
    typer.echo(f"device_name: {config.device_name}")
    typer.echo(f"executor: {config.executor}")
    typer.echo(
        "broker: "
        f"{config.broker.host}:{config.broker.port} "
        f"tls={str(config.broker.tls).lower()}"
    )

    if not config.devices:
        typer.echo("registered devices: none")
    else:
        typer.echo("registered devices:")
        for device_name, role in sorted(config.devices.items()):
            aliases = [
                alias
                for alias, target_device_name in sorted(config.aliases.items())
                if target_device_name == device_name
            ]
            alias_text = f" aliases={', '.join(aliases)}" if aliases else ""
            typer.echo(
                f"  - {device_name}: role={role.role} "
                f"description={role.description}{alias_text}"
            )

    if config.aliases:
        typer.echo("aliases:")
        for alias, device_name in sorted(config.aliases.items()):
            typer.echo(f"  - {alias} -> {resolve_device_name(config, alias)}")
    else:
        typer.echo("aliases: none")

    if config.pending_pairings:
        typer.echo(f"pending pairings: {len(config.pending_pairings)}")
    else:
        typer.echo("pending pairings: none")


if __name__ == "__main__":
    app()
