from __future__ import annotations

import asyncio
import json
from pathlib import Path
from typing import Annotated, Any, cast

import click
import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.schema import actor_role_warnings
from cleveragents.application.container import get_container
from cleveragents.cli.commands._resolve_actor import (
    resolve_config_files as _resolve_config_files,
)
from cleveragents.cli.commands.actor_context import app as actor_context_app
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import (
    BusinessRuleViolation,
    CleverAgentsError,
    NotFoundError,
    UnsafeConfigurationError,
    ValidationError,
)
from cleveragents.domain.models.core import Actor
from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.context_manager import ContextManager

app = typer.Typer(
    help=(
        "Manage actor configurations and actor-scoped execution. "
        "Built-ins use <provider>/<model>; customs use local/<id>. "
        "Default or built-in actors cannot be removed; "
        "use --unsafe when configs are marked unsafe."
    )
)
console = Console()

# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"


@app.command()
def run(
    name: Annotated[
        str,
        typer.Argument(help="Actor name to run (resolved from actor registry)"),
    ],
    prompt: Annotated[
        str,
        typer.Argument(help="Prompt text to send to the actor"),
    ],
    config: Annotated[
        list[Path] | None,
        typer.Option(
            "--config",
            "-c",
            exists=True,
            file_okay=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="YAML config paths (overrides registry-based name resolution)",
        ),
    ] = None,
    output: Annotated[
        Path | None,
        typer.Option(
            "--output",
            "-o",
            file_okay=True,
            dir_okay=False,
            writable=True,
            resolve_path=True,
        ),
    ] = None,
    verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0,
    unsafe: Annotated[
        bool, typer.Option("--unsafe", "-u", help="Confirm unsafe configs")
    ] = False,
    context: Annotated[
        str | None, typer.Option("--context", help="Name for saving/reusing context")
    ] = None,
    context_dir: Annotated[
        Path | None,
        typer.Option(
            "--context-dir", help="Directory to store context data", resolve_path=True
        ),
    ] = None,
    load_context: Annotated[
        Path | None,
        typer.Option(
            "--load-context",
            exists=True,
            file_okay=True,
            dir_okay=False,
            resolve_path=True,
            help="Load context from JSON; imports into named context when provided",
        ),
    ] = None,
    temperature: Annotated[
        float | None,
        typer.Option("--temperature", "-t", help="Override temperature"),
    ] = None,
    allow_rxpy_in_run_mode: Annotated[
        bool,
        typer.Option(
            "--allow-rxpy-in-run-mode",
            help="Allow RxPy stream routes in run mode (bypass validation)",
        ),
    ] = False,
    skill: Annotated[
        list[str] | None,
        typer.Option(
            "--skill",
            metavar="NAME",
            help="Skill to attach; only augments tool-bearing agents (repeatable)",
        ),
    ] = None,
) -> None:
    """Run the reactive network once with actor-first configs."""
    try:
        resolved_config = _resolve_config_files(name, list(config or []))
        app_exec = ReactiveCleverAgentsApp(
            config_files=resolved_config,
            verbose=verbose,
            unsafe=unsafe,
            temperature_override=temperature,
            skill_names=skill,
        )

        async def _execute() -> str:
            if load_context and context:
                ctx_mgr = ContextManager(context, context_dir)
                ctx_mgr.import_context(load_context)
                if ctx_mgr.global_context and app_exec.config:
                    app_exec.config.global_context.update(ctx_mgr.global_context)
                ctx_mgr.add_message("user", prompt)
                result = await app_exec.run_single_shot(
                    prompt,
                    context_manager=ctx_mgr,
                    allow_rxpy_in_run_mode=allow_rxpy_in_run_mode,
                )
                ctx_mgr.add_message("assistant", result)
                if app_exec.config:
                    ctx_mgr.save_global_context(app_exec.config.global_context)
                return result
            if load_context:
                with open(load_context, encoding="utf-8") as f:
                    data = json.load(f)
                if data.get("global_context") and app_exec.config:
                    app_exec.config.global_context.update(data["global_context"])
                return await app_exec.run_single_shot(
                    prompt,
                    context_manager=None,
                    allow_rxpy_in_run_mode=allow_rxpy_in_run_mode,
                )
            if context:
                ctx_mgr = ContextManager(context, context_dir)
                if ctx_mgr.exists() and ctx_mgr.global_context and app_exec.config:
                    app_exec.config.global_context.update(ctx_mgr.global_context)
                ctx_mgr.add_message("user", prompt)
                result = await app_exec.run_single_shot(
                    prompt,
                    context_manager=ctx_mgr,
                    allow_rxpy_in_run_mode=allow_rxpy_in_run_mode,
                )
                ctx_mgr.add_message("assistant", result)
                if app_exec.config:
                    ctx_mgr.save_global_context(app_exec.config.global_context)
                return result
            return await app_exec.run_single_shot(
                prompt,
                context_manager=None,
                allow_rxpy_in_run_mode=allow_rxpy_in_run_mode,
            )

        result = asyncio.run(_execute())
    except UnsafeConfigurationError as exc:
        typer.echo(f"Error: {exc}", err=True)
        raise typer.Exit(code=1) from exc
    except click.exceptions.Exit:
        raise
    except CleverAgentsError as exc:
        typer.echo(f"Error: {exc}", err=True)
        raise typer.Exit(code=2) from exc
    except Exception as exc:  # pragma: no cover
        typer.echo(f"Unexpected error: {exc}", err=True)
        raise typer.Exit(code=3) from exc

    if output:
        output.write_text(result)
        typer.echo(f"Output written to {output}")
    else:
        typer.echo(result)


def _get_services():
    container = get_container()
    actor_service = container.actor_service()
    actor_registry = (
        container.actor_registry() if hasattr(container, "actor_registry") else None
    )
    return actor_service, actor_registry


def _compute_actor_impact(actor_name: str) -> tuple[int, int, int]:
    """Compute the real impact counts for removing an actor.

    Queries the database to count:
    - Sessions that reference the actor via ``actor_name``
    - Active plans (queued or processing) that reference the actor via
      ``strategy_actor`` or ``execution_actor``
    - Actions configured to use the actor via any actor field

    Args:
        actor_name: The namespaced actor name (e.g. ``local/my-actor``).

    Returns:
        A 3-tuple of ``(session_count, active_plan_count, action_count)``.
        Returns ``(0, 0, 0)`` if any query fails, to avoid blocking removal.
    """
    container = get_container()

    # --- Sessions referencing this actor ---
    session_count = 0
    try:
        session_service = container.session_service()
        sessions = session_service.list()
        session_count = sum(1 for s in sessions if s.actor_name == actor_name)
    except Exception:  # pragma: no cover - defensive; DB may be unavailable
        pass

    # --- Active lifecycle plans referencing this actor ---
    active_plan_count = 0
    try:
        uow = container.unit_of_work()
        with uow.transaction() as ctx:
            plans = ctx.lifecycle_plans.list_plans(limit=10000)
        active_states = {"queued", "processing"}
        active_plan_count = sum(
            1
            for p in plans
            if p.processing_state in active_states
            and (p.strategy_actor == actor_name or p.execution_actor == actor_name)
        )
    except Exception:  # pragma: no cover - defensive; DB may be unavailable
        pass

    # --- Actions configured to use this actor ---
    action_count = 0
    try:
        lifecycle_service = container.plan_lifecycle_service()
        actions = lifecycle_service.list_actions()
        action_count = sum(
            1
            for a in actions
            if actor_name
            in (
                a.strategy_actor,
                a.execution_actor,
                getattr(a, "review_actor", None),
                getattr(a, "apply_actor", None),
                getattr(a, "estimation_actor", None),
                getattr(a, "invariant_actor", None),
            )
        )
    except Exception:  # pragma: no cover - defensive; DB may be unavailable
        pass

    return session_count, active_plan_count, action_count


def _load_config_text(config_path: Path | None) -> tuple[str, dict[str, Any]] | None:
    """Load a JSON or YAML config file and return both raw text and parsed dict.

    Returns a ``(raw_text, parsed_dict)`` tuple so callers can pass the
    original YAML source to the YAML-first persistence path.
    """
    if config_path is None:
        return None

    path = config_path.expanduser()
    if not path.exists():
        raise typer.BadParameter(f"Config file not found: {path}")

    text = path.read_text()
    try:
        data = json.loads(text)
    except json.JSONDecodeError:
        try:
            data = yaml.safe_load(text)
        except Exception as exc:  # pragma: no cover - defensive
            raise typer.BadParameter(f"Failed to parse config: {exc}") from exc

    if data is None:
        empty_config: dict[str, Any] = {}
        return text, empty_config
    if not isinstance(data, dict):
        raise typer.BadParameter("Config must be a JSON/YAML object.")

    return text, cast(dict[str, Any], data)


def _load_config(config_path: Path | None) -> dict[str, Any] | None:
    """Load a JSON or YAML config file if provided.

    Delegates to :func:`_load_config_text` and discards the raw text.
    """
    loaded = _load_config_text(config_path)
    if loaded is None:
        return None
    _, data = loaded
    return data


def _parse_option_value(raw_value: str) -> Any:
    try:
        return json.loads(raw_value)
    except Exception:
        lowered = raw_value.lower()
        if lowered in {"true", "false"}:
            return lowered == "true"
        return raw_value


def _parse_option_overrides(option_values: list[str] | None) -> dict[str, Any]:
    overrides: dict[str, Any] = {}
    if not option_values:
        return overrides

    for item in option_values:
        if "=" not in item:
            raise typer.BadParameter("Options must be provided as key=value pairs")
        key, value = item.split("=", 1)
        key = key.strip()
        if not key:
            raise typer.BadParameter("Option keys cannot be empty")
        overrides[key] = _parse_option_value(value.strip())
    return overrides


def _canonicalize_actor_config(
    *,
    name: str,
    config_blob: dict[str, Any] | None,
    provider_override: str | None = None,
    model_override: str | None = None,
    graph_descriptor: dict[str, Any] | None = None,
    unsafe: bool = False,
    option_overrides: dict[str, Any] | None = None,
) -> tuple[ActorConfiguration, dict[str, Any], bool]:
    try:
        resolved = ActorConfiguration.from_blob(
            blob=config_blob,
            name=name,
            provider=provider_override,
            model=model_override,
            graph_descriptor=graph_descriptor,
            unsafe=unsafe,
            option_overrides=option_overrides,
        )
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc

    canonical_blob = dict(config_blob or {})
    canonical_blob.setdefault("provider", resolved.provider)
    canonical_blob.setdefault("model", resolved.model)
    if (
        resolved.graph_descriptor is not None
        and "graph_descriptor" not in canonical_blob
    ):
        canonical_blob["graph_descriptor"] = resolved.graph_descriptor

    options_blob: dict[str, Any] = {}
    if isinstance(canonical_blob.get("options"), dict):
        options_blob.update(cast(dict[str, Any], canonical_blob["options"]))
    if resolved.options:
        options_blob = {**options_blob, **resolved.options}
    if option_overrides:
        options_blob.update(option_overrides)
    if options_blob:
        canonical_blob["options"] = options_blob

    canonical_blob.setdefault("unsafe", resolved.unsafe)

    requires_confirmation = resolved.unsafe and not unsafe

    return resolved, canonical_blob, requires_confirmation


def _actor_spec_dict(actor: Actor) -> dict[str, object]:
    """Return actor data using spec field names for format_output.

    Keys: name, provider, model, unsafe, is_default, is_built_in,
    config_hash, schema_version, updated_at.
    """
    result: dict[str, object] = {
        "name": actor.name,
        "provider": actor.provider,
        "model": actor.model,
        "unsafe": actor.unsafe,
        "is_default": actor.is_default,
        "is_built_in": actor.is_built_in,
        "config_hash": actor.config_hash,
        "schema_version": actor.schema_version,
        "updated_at": actor.updated_at.isoformat(),
    }
    if actor.graph_descriptor is not None:
        result["graph_descriptor"] = actor.graph_descriptor
    if actor.config_blob:
        result["config_blob"] = actor.config_blob
    return result


def _print_actor(
    actor: Actor,
    title: str = "Actor",
    fmt: str = OutputFormat.RICH.value,
    config_path: Path | None = None,
    show_add_panels: bool = False,
) -> None:
    """Print actor details in the requested format.

    When ``show_add_panels`` is True (used by the ``add`` command), renders
    the full spec-required output: Actor Added panel (with Type field), Config
    panel, Capabilities panel, Tools panel, and a success status line.
    """
    if fmt != OutputFormat.RICH.value:
        data = _actor_spec_dict(actor)
        console.print(format_output(data, fmt))
        return

    blob = actor.config_blob or {}
    actor_type = str(blob.get("type", ""))

    details = (
        f"[bold]Name:[/bold] {actor.name}\n"
        f"[bold]Provider:[/bold] {actor.provider}\n"
        f"[bold]Model:[/bold] {actor.model}\n"
        f"[bold]Default:[/bold] {'yes' if actor.is_default else 'no'}\n"
        f"[bold]Unsafe:[/bold] {'yes' if actor.unsafe else 'no'}\n"
        f"[bold]Type:[/bold] {actor_type}"
    )
    console.print(Panel(details, title=title, expand=False))

    if not show_add_panels:
        return

    # ── Config panel ──────────────────────────────────────────────────────────
    path_str = str(config_path) if config_path is not None else ""
    options_count = len(blob.get("options", {}) or {})

    graph_descriptor = actor.graph_descriptor or {}
    nodes_count = len(graph_descriptor.get("nodes", []) or [])
    edges_count = len(graph_descriptor.get("edges", []) or [])

    config_details = (
        f"[bold]Path:[/bold] {path_str}\n"
        f"[bold]Hash:[/bold] {actor.config_hash}\n"
        f"[bold]Options:[/bold] {options_count}\n"
        f"[bold]Nodes:[/bold] {nodes_count}\n"
        f"[bold]Edges:[/bold] {edges_count}"
    )
    console.print(Panel(config_details, title="Config", expand=False))

    # ── Capabilities panel ────────────────────────────────────────────────────
    capabilities: list[str] = list(blob.get("capabilities", []) or [])
    if capabilities:
        cap_lines = "\n".join(f"- {cap}" for cap in capabilities)
        console.print(Panel(cap_lines, title="Capabilities", expand=False))

    # ── Tools panel ───────────────────────────────────────────────────────────
    raw_tools: list[Any] = list(blob.get("tools", []) or [])
    if raw_tools:
        tools_table = Table(show_header=True, header_style="bold", box=None)
        tools_table.add_column("Tool")
        tools_table.add_column("Read-Only", justify="center")
        tools_table.add_column("Safe", justify="center")
        for tool_entry in raw_tools:
            if isinstance(tool_entry, dict):
                tool_name = str(tool_entry.get("name", ""))
                read_only = "yes" if tool_entry.get("read_only", False) else "no"
                safe = "yes" if tool_entry.get("safe", False) else "no"
            else:
                tool_name = str(tool_entry)
                read_only = "yes"
                safe = "yes"
            tools_table.add_row(tool_name, read_only, safe)
        console.print(Panel(tools_table, title="Tools", expand=False))

    # ── Success status line ───────────────────────────────────────────────────
    console.print("[green bold]✓ OK[/green bold] Actor added")


def _print_role_warnings(config_blob: dict[str, Any]) -> None:
    """Print non-fatal role compatibility warnings for actor configs."""
    for warning in actor_role_warnings(config_blob):
        console.print(f"[yellow]Warning:[/yellow] {warning}")


@app.command()
def add(
    name: Annotated[
        str,
        typer.Argument(
            help="Namespaced actor name (e.g. local/my-actor)",
            metavar="NAME",
        ),
    ],
    config: Annotated[
        Path | None,
        typer.Option("--config", "-c", help="Path to JSON/YAML actor config"),
    ] = None,
    unsafe: Annotated[
        bool, typer.Option("--unsafe", help="Mark the actor as unsafe")
    ] = False,
    set_default: Annotated[
        bool, typer.Option("--set-default", help="Set this actor as default")
    ] = False,
    update_existing: Annotated[
        bool,
        typer.Option("--update", help="Update actor if it already exists"),
    ] = False,
    option: Annotated[
        list[str] | None,
        typer.Option(
            "--option",
            "-o",
            help="Override or add actor option (key=value). Repeat for multiple.",
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Add a new actor configuration.

    The actor name is provided as a required positional argument. The YAML/JSON
    configuration file specified with ``--config`` supplies the actor settings.
    The positional ``NAME`` takes precedence over any ``name`` field in the
    config file.

    Signature:
    ``agents actor add <NAME> --config <FILE> [--update] [--unsafe]
    [--set-default] [--option key=value] [--format FORMAT]``

    Examples:
        agents actor add local/my-actor --config ./actors/my-actor.yaml
        agents actor add local/my-actor --config ./actors/my-actor.yaml --update
        agents actor add local/my-actor --config actor.yaml --format json
    """
    service, registry = _get_services()
    option_overrides = _parse_option_overrides(option)
    if not option_overrides:
        option_overrides = None

    if config is None:
        raise typer.BadParameter("Config file is required for actor add")

    loaded = _load_config_text(config)
    # _load_config_text() only returns None when config_path is None,
    # which is already handled above.
    assert loaded is not None, "unreachable: config is not None"
    yaml_text, config_blob = loaded

    resolved, canonical_blob, requires_confirmation = _canonicalize_actor_config(
        name=name,
        config_blob=config_blob,
        unsafe=unsafe,
        option_overrides=option_overrides,
    )

    if requires_confirmation:
        raise typer.BadParameter(
            "Actor config is marked unsafe; re-run with --unsafe to confirm."
        )

    _print_role_warnings(canonical_blob)

    # ── Enforce --update flag: reject re-adding an existing actor ─────────────
    if not update_existing:
        try:
            existing = registry.get_actor(name) if registry else service.get_actor(name)
            # Actor already exists and --update was not provided — reject with error
            registered_ts = existing.updated_at.strftime("%Y-%m-%d %H:%M")
            error_details = (
                f"Actor already exists: {name}\n"
                f"Registered: {registered_ts}\n"
                f"Use --update to replace the existing actor definition."
            )
            console.print(Panel(error_details, title="Error", border_style="red"))
            console.print(
                "[red bold]✗ ERROR[/red bold] Actor already registered"
                " \u2014 use --update to replace"
            )
            raise typer.Exit(code=1)
        except typer.Exit:
            raise
        except NotFoundError:
            pass  # Actor does not exist yet — proceed with add

    # Extract schema_version from the parsed config blob if present.
    schema_version: str | None = config_blob.get("schema_version")  # type: ignore[assignment]
    if schema_version and not isinstance(schema_version, str):
        schema_version = str(schema_version)

    try:
        if registry:
            # Use the YAML-first path via registry.upsert_actor() with
            # yaml_text threaded through.  This preserves the original YAML
            # text, schema_version, and compiled_metadata in the database
            # while also honouring all CLI flags (--set-default, --option,
            # --unsafe) that registry.add() does not accept.
            actor = registry.upsert_actor(
                name=name,
                config_blob=canonical_blob,
                unsafe=resolved.unsafe,
                set_default=set_default,
                allow_unsafe=unsafe,
                option_overrides=option_overrides,
                yaml_text=yaml_text,
                schema_version=schema_version,
            )
        else:
            if resolved.unsafe and not unsafe:
                raise ValidationError(
                    "Actor config is marked unsafe; re-run with --unsafe to confirm."
                )
            actor = service.upsert_actor(
                name=name,
                provider=resolved.provider,
                model=resolved.model,
                config_blob=canonical_blob,
                graph_descriptor=resolved.graph_descriptor,
                unsafe=resolved.unsafe,
                set_default=set_default,
            )
        title = "Actor updated" if update_existing else "Actor added"
        _print_actor(
            actor,
            title=title,
            fmt=fmt,
            config_path=config,
            show_add_panels=(not update_existing),
        )
    except (ValidationError, BusinessRuleViolation) as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command()
def update(
    name: Annotated[str, typer.Argument(help="Actor name to update")],
    config: Annotated[
        Path | None,
        typer.Option("--config", "-c", help="Path to JSON/YAML actor config"),
    ] = None,
    unsafe: Annotated[
        bool, typer.Option("--unsafe", help="Mark the actor as unsafe")
    ] = False,
    safe: Annotated[
        bool, typer.Option("--safe", help="Mark the actor as safe")
    ] = False,
    set_default: Annotated[
        bool, typer.Option("--set-default", help="Set this actor as default")
    ] = False,
    option: Annotated[
        list[str] | None,
        typer.Option(
            "--option",
            "-o",
            help="Override or add actor option (key=value). Repeat for multiple.",
        ),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Update an existing actor."""
    if unsafe and safe:
        raise typer.BadParameter("Choose only one of --unsafe or --safe")

    service, registry = _get_services()

    try:
        current = registry.get_actor(name) if registry else service.get_actor(name)
    except NotFoundError as exc:
        console.print(f"[red]Actor not found:[/red] {exc}")
        raise typer.Abort() from exc

    raw_config = _load_config(config)
    new_config = (
        dict(raw_config) if raw_config is not None else dict(current.config_blob)
    )
    provider_override = None if raw_config is not None else current.provider
    model_override = None if raw_config is not None else current.model

    if safe:
        new_config["unsafe"] = False
    if unsafe:
        new_config["unsafe"] = True

    new_unsafe = bool(new_config.get("unsafe", current.unsafe))
    option_overrides = _parse_option_overrides(option)
    if not option_overrides:
        option_overrides = None

    resolved, canonical_blob, requires_confirmation = _canonicalize_actor_config(
        name=name,
        config_blob=new_config,
        provider_override=provider_override,
        model_override=model_override,
        graph_descriptor=current.graph_descriptor,
        unsafe=new_unsafe,
        option_overrides=option_overrides,
    )

    if requires_confirmation and not unsafe:
        raise typer.BadParameter(
            "Actor config is marked unsafe; re-run with --unsafe to confirm."
        )

    _print_role_warnings(canonical_blob)

    try:
        if registry:
            if option_overrides is None:
                actor = registry.upsert_actor(
                    name=name,
                    provider=provider_override,
                    model=model_override,
                    config_blob=canonical_blob,
                    graph_descriptor=current.graph_descriptor,
                    unsafe=new_unsafe,
                    set_default=set_default,
                    is_built_in=current.is_built_in,
                    allow_unsafe=unsafe,
                )
            else:
                actor = registry.upsert_actor(
                    name=name,
                    provider=provider_override,
                    model=model_override,
                    config_blob=canonical_blob,
                    graph_descriptor=current.graph_descriptor,
                    unsafe=new_unsafe,
                    set_default=set_default,
                    is_built_in=current.is_built_in,
                    allow_unsafe=unsafe,
                    option_overrides=option_overrides,
                )
        else:
            if resolved.unsafe and not unsafe:
                raise ValidationError(
                    "Actor config is marked unsafe; re-run with --unsafe to confirm."
                )
            actor = service.upsert_actor(
                name=name,
                provider=resolved.provider,
                model=resolved.model,
                config_blob=canonical_blob,
                graph_descriptor=resolved.graph_descriptor,
                unsafe=resolved.unsafe,
                set_default=set_default,
                is_built_in=current.is_built_in,
            )
        _print_actor(actor, title="Actor updated", fmt=fmt)
    except (ValidationError, BusinessRuleViolation) as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command()
def remove(name: Annotated[str, typer.Argument(help="Actor name to remove")]) -> None:
    """Remove a custom actor.

    Specify the namespaced name (e.g. ``local/my-actor``).
    """

    service, registry = _get_services()
    try:
        # Get actor details before removal for display
        try:
            actor = registry.get_actor(name) if registry else service.get_actor(name)
            actor_provider = actor.provider
            actor_model = actor.model
        except (NotFoundError, AttributeError):
            # If we can't get the actor details, use placeholder values
            actor_provider = "unknown"
            actor_model = "unknown"

        # Compute impact counts before removal so the display reflects
        # what was actually affected at the time of removal.
        session_count, active_plan_count, action_count = _compute_actor_impact(name)

        # Perform the removal
        if registry:
            registry.remove_actor(name)
        else:
            service.remove_actor(name)

        # Display Actor Removed panel
        actor_info = (
            f"[cyan bold]Name:[/cyan bold] {name}\n"
            f"[magenta bold]Provider:[/magenta bold] {actor_provider}\n"
            f"[magenta bold]Model:[/magenta bold] {actor_model}"
        )
        actor_panel = Panel(actor_info, title="Actor Removed", border_style="green")
        console.print(actor_panel)

        # Display Impact panel with real computed counts
        impact_info = (
            f"[yellow]Sessions:[/yellow] {session_count} affected\n"
            f"[yellow]Active Plans:[/yellow] {active_plan_count} affected\n"
            f"[yellow]Actions Referencing:[/yellow] {action_count}"
        )
        impact_panel = Panel(impact_info, title="Impact", border_style="yellow")
        console.print(impact_panel)

        # Display Cleanup panel
        cleanup_info = (
            "[blue]Config:[/blue] kept on disk\n[blue]Contexts:[/blue] 0 orphaned"
        )
        cleanup_panel = Panel(cleanup_info, title="Cleanup", border_style="dim")
        console.print(cleanup_panel)

        # Print success message
        console.print("[green bold]✓ OK[/green bold] Actor removed")

    except (ValidationError, BusinessRuleViolation, NotFoundError) as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("list")
def list_actors(
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """List all actors.

    Shows actors with optional output format selection.

    Examples:
        agents actor list
        agents actor list --format json
        agents actor list --format yaml
        agents actor list --format plain
    """

    service, registry = _get_services()
    actors = registry.list_actors() if registry else service.list_actors()
    if not actors:
        console.print("[yellow]No actors configured.[/yellow]")
        return

    # Non-rich formats use the formatting helper
    if fmt != OutputFormat.RICH.value:
        data = [_actor_spec_dict(a) for a in actors]
        console.print(format_output(data, fmt))
        return

    table = Table(title=f"Actors ({len(actors)} total)")
    table.add_column("Name", style="cyan")
    table.add_column("Provider", style="magenta")
    table.add_column("Model", style="magenta")
    table.add_column("Default", justify="center")
    table.add_column("Built-in", justify="center")
    table.add_column("Unsafe", justify="center")
    table.add_column("Updated", style="green")

    for actor in actors:
        table.add_row(
            actor.name,
            actor.provider,
            actor.model,
            "✓" if actor.is_default else "",
            "✓" if actor.is_built_in else "",
            "yes" if actor.unsafe else "no",
            str(actor.updated_at),
        )

    console.print(table)

    # Calculate summary statistics
    total_actors = len(actors)
    builtin_count = sum(1 for a in actors if a.is_built_in)
    custom_count = total_actors - builtin_count
    unsafe_count = sum(1 for a in actors if a.unsafe)
    providers_used = len(set(a.provider for a in actors))

    # Create Summary panel
    summary_text = (
        f"[yellow bold]Total:[/yellow bold] {total_actors}\n"
        f"[green bold]Built-in:[/green bold] {builtin_count}\n"
        f"[blue bold]Custom:[/blue bold] {custom_count}\n"
        f"[red bold]Unsafe:[/red bold] {unsafe_count}\n"
        f"[blue bold]Providers Used:[/blue bold] {providers_used}"
    )
    summary_panel = Panel(summary_text, title="Summary", border_style="dim")
    console.print(summary_panel)

    # Print success message
    console.print(f"[green bold]✓ OK[/green bold] {total_actors} actors listed")


@app.command()
def show(
    name: Annotated[str, typer.Argument(help="Actor name to show")],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show details for an actor.

    Specify the namespaced name (e.g. ``local/my-actor``).

    Examples:
        agents actor show local/my-actor
        agents actor show local/my-actor --format json
        agents actor show local/my-actor --format yaml
    """

    service, registry = _get_services()
    try:
        actor = registry.get_actor(name) if registry else service.get_actor(name)
        _print_actor(actor, title="Actor details", fmt=fmt)
    except (ValidationError, NotFoundError) as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Abort() from exc


@app.command("set-default")
def set_default(
    name: Annotated[str, typer.Argument(help="Actor to set as default")],
) -> None:
    """Set the default actor."""

    service, registry = _get_services()
    try:
        actor = (
            registry.set_default_actor(name)
            if registry
            else service.set_default_actor(name)
        )
        _print_actor(actor, title="Default actor updated")
    except (ValidationError, NotFoundError, BusinessRuleViolation) as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Abort() from exc


# ---------------------------------------------------------------------------
# Sub-command groups
# ---------------------------------------------------------------------------
app.add_typer(actor_context_app, name="context")
