"""Session management commands for CleverAgents CLI.

The ``agents session`` command group manages interactive sessions — persistent
conversation threads tied to orchestrator actors.

Session operations are routed through the :class:`A2aLocalFacade` to comply
with the A2A protocol data flow::

    CLI ──▸ A2aLocalFacade.dispatch() ──▸ SessionService ──▸ Domain

Based on specification.md ``agents session`` section and implementation_plan.md
task A7.cli.
"""

from __future__ import annotations

import json
import logging
import sys
from collections import OrderedDict
from pathlib import Path
from typing import Annotated, Any, cast

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

from cleveragents.a2a.models import A2aRequest
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.session import (
    MessageRole,
    Session,
    SessionExportError,
    SessionImportError,
    SessionMessage,
    SessionNotFoundError,
    SessionService,
)

# Create sub-app for session commands
app = typer.Typer(help="Manage interactive sessions.")
console = Console()

_log = logging.getLogger(__name__)

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

# ---------------------------------------------------------------------------
# Module-level service accessor (patchable in tests)
# ---------------------------------------------------------------------------
_service: SessionService | None = None


def _get_session_service() -> SessionService:
    """Get or create the SessionService instance.

    Production usage goes through the DI container's ``session_service``
    provider; tests can patch ``_service`` or this function directly.
    """
    global _service
    if _service is not None:
        return _service

    from cleveragents.application.container import get_container

    container = get_container()
    svc = cast(SessionService, container.session_service())
    _service = svc
    return _service


def _reset_session_service() -> None:
    """Reset the module-level service (used by tests)."""
    global _service
    _service = None


def _facade_dispatch(operation: str, params: dict[str, Any]) -> dict[str, Any]:
    """Route an operation through the A2A local facade.

    Returns the response data dict on success.  Raises on error
    so that CLI commands can render the appropriate error message.

    stdout/stderr are redirected during facade construction to prevent
    structlog or Rich output from polluting CLI output captured by
    test runners.
    """
    import contextlib
    import io

    with (
        contextlib.redirect_stdout(io.StringIO()),
        contextlib.redirect_stderr(io.StringIO()),
    ):
        from cleveragents.a2a.cli_bootstrap import get_facade

        facade = get_facade()
        request = A2aRequest(method=operation, params=params)
        response = facade.dispatch(request)
    if response.error is not None:
        raise RuntimeError(response.error.message)
    return dict(response.result or {})


# ---------------------------------------------------------------------------
# Helpers — dict builders and rich printing
# ---------------------------------------------------------------------------


def _session_summary_dict(session: Session) -> OrderedDict[str, Any]:
    """Build a stable-ordered summary dict for a session."""
    result: OrderedDict[str, Any] = OrderedDict()
    result["session_id"] = session.session_id
    result["actor"] = session.actor_name or "(none)"
    result["namespace"] = session.namespace
    result["messages"] = session.message_count
    result["created"] = session.created_at.isoformat()
    result["updated"] = session.updated_at.isoformat()
    return result


def _session_list_dict(sessions: list[Session]) -> dict[str, Any]:
    """Build the list output with summary stats per spec."""
    items = []
    for s in sessions:
        items.append(
            {
                "id": s.session_id,
                "name": s.name or None,
                "actor": s.actor_name or "(none)",
                "messages": s.message_count,
                "updated": s.updated_at.isoformat(),
            }
        )

    # Build summary section per spec
    total_messages = sum(s.message_count for s in sessions)

    # Find most recent and oldest sessions
    if sessions:
        sorted_sessions = sorted(sessions, key=lambda x: x.updated_at, reverse=True)
        most_recent = sorted_sessions[0].name or sorted_sessions[0].session_id[:8]
        oldest = sorted_sessions[-1].name or sorted_sessions[-1].session_id[:8]
    else:
        most_recent = None
        oldest = None

    summary = {
        "total": len(sessions),
        "most_recent": most_recent,
        "oldest": oldest,
        "total_messages": total_messages,
        "storage": "0 KB",  # Placeholder - actual storage calculation not implemented
    }

    return {
        "sessions": items,
        "summary": summary,
    }


# ---------------------------------------------------------------------------
# CLI Commands
# ---------------------------------------------------------------------------


@app.command()
def create(
    actor: Annotated[
        str | None,
        typer.Option("--actor", help="Orchestrator actor name (namespace/name)"),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Create a new interactive session.

    Optionally bind the session to an orchestrator actor via ``--actor``.

    Examples:
        agents session create
        agents session create --actor openai/gpt-4
        agents session create --format json
    """
    try:
        # Create session through the service, then notify the A2A
        # facade for protocol compliance and telemetry.
        service = _get_session_service()
        session = service.create(actor_name=actor)

        # Notify the facade layer for A2A protocol bookkeeping.
        # Pass session_id so the facade handler acknowledges the already-
        # persisted session instead of creating a duplicate (#1141).
        import contextlib

        with contextlib.suppress(Exception):
            _facade_dispatch(
                "session.create",
                {"actor_name": actor or "", "session_id": session.session_id},
            )

        data = _session_summary_dict(session)

        if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
            typer.echo(format_output(dict(data), fmt))
            return

        details = (
            f"[bold]Session ID:[/bold] {session.session_id}\n"
            f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n"
            f"[bold]Namespace:[/bold] {session.namespace}\n"
            f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}"
        )
        console.print(Panel(details, title="Session", expand=False))

        # Settings panel
        settings_text = (
            "[yellow]Automation:[/yellow] default\n"
            "[yellow]Streaming:[/yellow] off\n"
            "[yellow]Context:[/yellow] default\n"
            "[yellow]Memory:[/yellow] enabled\n"
            "[yellow]Max History:[/yellow] 50 turns"
        )
        console.print(Panel(settings_text, title="Settings", expand=False))

        # Actor Details panel (if actor is bound)
        if session.actor_name:
            try:
                from cleveragents.application.container import get_container

                container = get_container()
                registry = container.actor_registry()
                actor_obj = registry.get_actor(session.actor_name)
                actor_details = (
                    f"[blue]Provider:[/blue] {actor_obj.provider}\n"
                    f"[blue]Model:[/blue] {actor_obj.model}\n"
                    f"[blue]Temperature:[/blue] "
                    f"{getattr(actor_obj, 'temperature', 0.7)}\n"
                    "[blue]Context Window:[/blue] 200K tokens"
                )
                console.print(Panel(actor_details, title="Actor Details", expand=False))
            except Exception:
                pass  # Actor details unavailable

        console.print("[green]✓ OK[/green] Session created")

    except SessionNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session create failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc


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

    Displays session IDs, bound actors, message counts, and last update times.

    Examples:
        agents session list
        agents session list --format json
        agents session list --format table
    """
    try:
        service = _get_session_service()
        sessions = service.list()
    except DatabaseError as exc:
        _log.debug("session list failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc

    if not sessions:
        # For machine-readable formats, always emit a structured empty list
        # so that callers parsing JSON/YAML receive valid output.
        if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
            typer.echo(format_output({"sessions": [], "total": 0}, fmt))
            return
        console.print("[yellow]No sessions found.[/yellow]")
        console.print("Create one with 'agents session create'")
        return

    data = _session_list_dict(sessions)

    if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
        typer.echo(format_output(data, fmt))
        return

    # Rich table
    table = Table(title="Sessions", show_header=True, border_style="blue")
    table.add_column("ID", style="cyan")
    table.add_column("Name", style="magenta")
    table.add_column("Actor", style="blue")
    table.add_column("Messages", justify="right")
    table.add_column("Updated", style="green")

    for s in sessions:
        table.add_row(
            s.session_id[:8],  # Truncate ID for readability
            s.name or "(unnamed)",
            s.actor_name or "(none)",
            str(s.message_count),
            s.updated_at.strftime("%Y-%m-%d %H:%M"),
        )

    console.print(table)
    console.print()

    # Reuse the summary dict already computed by _session_list_dict to avoid
    # duplicating the total_msgs / sorted_sessions / most_recent / oldest logic.
    summary = data["summary"]

    summary_table = Table.grid(padding=(0, 1))
    summary_table.add_column(style="cyan bold", justify="left")
    summary_table.add_column(style="white", justify="left")
    summary_table.add_row("Total:", str(summary["total"]))
    summary_table.add_row("Most Recent:", summary["most_recent"] or "")
    summary_table.add_row("Oldest:", summary["oldest"] or "")
    summary_table.add_row("Total Messages:", str(summary["total_messages"]))
    summary_table.add_row("Storage:", summary["storage"])

    console.print(Panel(summary_table, title="Summary", border_style="blue"))
    console.print()
    console.print(f"[green]✓ OK[/green] {len(sessions)} sessions listed")


@app.command()
def show(
    session_id: Annotated[
        str,
        typer.Argument(help="Session ULID to show"),
    ],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show session details and recent messages.

    Displays session metadata, recent messages, linked plans, and token usage.

    Examples:
        agents session show 01HXYZ...
        agents session show 01HXYZ... --format json
    """
    try:
        service = _get_session_service()
        session = service.get(session_id)
        data = session.as_cli_dict()

        if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
            typer.echo(format_output(dict(data), fmt))
            return

        # Session summary panel — field order per spec: ID, Actor, Messages,
        # Created, Updated, Automation (docs/specification.md §agents session show)
        details = (
            f"[bold]ID:[/bold] {session.session_id}\n"
            f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n"
            f"[bold]Messages:[/bold] {session.message_count}\n"
            f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}\n"
            f"[bold]Updated:[/bold] {session.updated_at.strftime('%Y-%m-%d %H:%M')}\n"
            f"[bold]Automation:[/bold] {session.automation or '(none)'}"
        )
        console.print(Panel(details, title="Session Summary", expand=False))

        # Recent messages
        if session.messages:
            recent = session.messages[-5:]
            msg_table = Table(title="Recent Messages", show_header=True)
            msg_table.add_column("Role", style="cyan")
            msg_table.add_column("Text")
            for msg in recent:
                text = msg.content
                if len(text) > 80:
                    text = text[:77] + "..."
                msg_table.add_row(msg.role.value, text)
            console.print(msg_table)

        # Linked plans — spec requires Plan ID / Phase / State columns
        if session.linked_plans:
            plan_table = Table(title="Linked Plans", show_header=True)
            plan_table.add_column("Plan ID", style="cyan")
            plan_table.add_column("Phase")
            plan_table.add_column("State")
            for lp in session.linked_plans:
                plan_table.add_row(lp.plan_id, lp.phase, lp.state)
            console.print(Panel(plan_table, title="Linked Plans", expand=False))
        elif session.linked_plan_ids:
            # Fallback: only flat IDs available
            plan_text = "\n".join(f"  • {pid}" for pid in session.linked_plan_ids)
            console.print(Panel(plan_text, title="Linked Plans", expand=False))

        # Token usage
        tu = session.token_usage
        usage_text = (
            f"[blue]Input Tokens:[/blue] {tu.input_tokens:,}\n"
            f"[blue]Output Tokens:[/blue] {tu.output_tokens:,}\n"
            f"[yellow]Estimated Cost:[/yellow] ${tu.estimated_cost:.4f}"
        )
        console.print(Panel(usage_text, title="Token Usage", expand=False))

        # Cost budget (per-session budget cap, #584)
        if session.cost_budget is not None:
            cb = session.cost_budget
            util = cb.utilization()
            util_display = f"{util * 100:.1f}%" if util is not None else "N/A"
            remaining = cb.remaining()
            remaining_display = (
                f"${remaining:.4f}" if remaining is not None else "unlimited"
            )
            max_display = (
                f"${cb.max_cost_usd:.4f}"
                if cb.max_cost_usd is not None
                else "unlimited"
            )
            budget_text = (
                f"[blue]Total Cost:[/blue] ${cb.total_cost:.4f}\n"
                f"[blue]Max Cost:[/blue] {max_display}\n"
                f"[yellow]Utilization:[/yellow] {util_display}\n"
                f"[green]Remaining:[/green] {remaining_display}"
            )
            console.print(Panel(budget_text, title="Cost Budget", expand=False))

        console.print("[green bold]✓ OK[/green bold] Session details loaded")

    except SessionNotFoundError as exc:
        console.print(f"[red]Session not found:[/red] {session_id}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session show failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc


@app.command()
def delete(
    session_id: Annotated[
        str,
        typer.Argument(help="Session ULID to delete"),
    ],
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
    fmt: Annotated[
        OutputFormat,
        typer.Option("--format", help=_FORMAT_HELP),
    ] = OutputFormat.RICH,
) -> None:
    """Delete a session permanently.

    A confirmation prompt is shown unless ``--yes`` is provided.

    Examples:
        agents session delete 01HXYZ...
        agents session delete 01HXYZ... --yes
    """
    try:
        service = _get_session_service()

        # Verify session exists before prompting and capture message count
        session_to_delete = service.get(session_id)
        message_count = session_to_delete.message_count

        if not yes:
            confirm = typer.confirm(f"Delete session {session_id}?", default=False)
            if not confirm:
                console.print("[yellow]Aborted.[/yellow]")
                raise typer.Abort()

        service.delete(session_id)

        # Rich output: render Deletion Summary and Cleanup panels
        if fmt == OutputFormat.RICH:
            # Deletion Summary panel
            summary_table = Table.grid(padding=(0, 1))
            summary_table.add_column(style="cyan bold", justify="left")
            summary_table.add_column(style="white", justify="left")
            summary_table.add_row("Session:", session_id)
            summary_table.add_row("ID:", session_id)
            summary_table.add_row("Messages:", f"{message_count} removed")
            summary_table.add_row("Storage:", "0 KB freed")
            summary_table.add_row("Plans Orphaned:", "0")

            console.print(
                Panel(summary_table, title="Deletion Summary", border_style="blue")
            )
            console.print()

            # Cleanup panel
            cleanup_table = Table.grid(padding=(0, 1))
            cleanup_table.add_column(style="cyan bold", justify="left")
            cleanup_table.add_column(style="white", justify="left")
            cleanup_table.add_row("Backups:", "none")
            cleanup_table.add_row("Logs:", "preserved")
            cleanup_table.add_row("Context:", "cleared")
            cleanup_table.add_row("Checkpoints:", "none")

            console.print(Panel(cleanup_table, title="Cleanup", border_style="blue"))
            console.print()
            console.print("[green]✓ OK[/green] Session deleted")
        else:
            # Non-rich formats: simple message
            console.print(f"[green]✓ OK[/green] Session {session_id} deleted")

    except SessionNotFoundError as exc:
        console.print(f"[red]Session not found:[/red] {session_id}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session delete failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc


@app.command("export")
def export_session(
    session_id: Annotated[
        str,
        typer.Argument(help="Session ULID to export"),
    ],
    output: Annotated[
        Path | None,
        typer.Option("--output", "-o", help="Output file path (default: stdout)"),
    ] = None,
    force: Annotated[
        bool,
        typer.Option("--force", help="Overwrite existing output file"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option(
            "--format",
            help="Export format: json (default) or md (Markdown transcript)",
        ),
    ] = "json",
) -> None:
    """Export a session as JSON or Markdown.

    Writes the full session data to a file or stdout.  The default format is
    JSON (canonical, importable).  Use ``--format md`` for a human-readable
    Markdown transcript (lossy — cannot be re-imported).

    On success, three Rich panels are displayed: "Session Export" (session ID,
    output path, message count, file size, format), "Contents" (messages, plan
    references, metadata keys, actor config, schema version), and "Integrity"
    (checksum, encrypted flag).

    Examples:
        agents session export 01HXYZ...
        agents session export 01HXYZ... -o session.json
        agents session export 01HXYZ... -o session.json --force
        agents session export 01HXYZ... --format md -o session.md
    """
    if fmt not in ("json", "md"):
        console.print(f"[red]Invalid format:[/red] {fmt!r}. Use 'json' or 'md'.")
        raise typer.Exit(1)

    try:
        service = _get_session_service()
        json_data: dict[str, Any] = {}

        if fmt == "md":
            # Markdown export: load full session with messages
            session = service.get(session_id)
            # Load messages via export_session to populate session.messages
            json_data = service.export_session(session_id)
            messages = [
                SessionMessage(
                    message_id=m["message_id"],
                    role=MessageRole(m["role"]),
                    content=m["content"],
                    sequence=m["sequence"],
                    timestamp=m["timestamp"],
                    metadata=m.get("metadata", {}),
                    tool_call_id=m.get("tool_call_id"),
                )
                for m in json_data.get("messages", [])
            ]
            session.messages = messages
            content = session.as_export_markdown()
        else:
            json_data = service.export_session(session_id)
            content = json.dumps(json_data, indent=2, default=str)

        if output is not None:
            if output.exists() and not force:
                console.print(
                    f"[red]File already exists:[/red] {output}\n"
                    "Use --force to overwrite."
                )
                raise typer.Exit(1)
            # Create parent directories if needed
            output.parent.mkdir(parents=True, exist_ok=True)
            output.write_text(content, encoding="utf-8")
        else:
            typer.echo(content)

        # Render Rich panels for both file and stdout export paths
        _render_export_panels(
            session_id=session_id,
            output=output,
            content=content,
            export_data=json_data,
            fmt=fmt,
        )

    except SessionNotFoundError as exc:
        console.print(f"[red]Session not found:[/red] {session_id}")
        raise typer.Exit(1) from exc
    except SessionExportError as exc:
        console.print(f"[red]Export error:[/red] {exc}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session export failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc


def _render_export_panels(
    *,
    session_id: str,
    output: Path | None,
    content: str,
    export_data: dict[str, Any],
    fmt: str,
) -> None:
    """Render the three spec-required Rich panels for ``agents session export``.

    Displays:
    - "Session Export" panel: session ID, output path, message count, size, format
    - "Contents" panel: messages, plan references, metadata keys, actor config,
      schema version
    - "Integrity" panel: checksum, encrypted flag
    - ``✓ OK Export completed`` success line
    """
    # Compute file size from content
    size_bytes = len(content.encode("utf-8"))
    if size_bytes < 1024:
        size_str = f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        size_str = f"{size_bytes // 1024} KB"
    else:
        size_str = f"{size_bytes // (1024 * 1024)} MB"

    # Derive display values from export data (JSON format) or defaults (md)
    message_count = len(export_data.get("messages", []))
    plan_refs = len(export_data.get("linked_plan_ids", []))
    metadata_keys = len(export_data.get("metadata", {}))
    actor_config = "included" if export_data.get("actor_name") else "none"
    schema_version = export_data.get("schema_version", "v1")
    checksum_raw = export_data.get("checksum", "")
    checksum_display = (
        f"sha256:{checksum_raw[:4]}...{checksum_raw[-4:]}"
        if len(checksum_raw) >= 8
        else checksum_raw or "n/a"
    )
    output_display = str(output) if output is not None else "(stdout)"
    format_display = "JSON" if fmt == "json" else "Markdown"

    # Session Export panel
    export_table = Table.grid(padding=(0, 1))
    export_table.add_column(style="cyan bold", justify="left")
    export_table.add_column(style="white", justify="left")
    export_table.add_row("Session:", session_id)
    export_table.add_row("Output:", output_display)
    export_table.add_row("Messages:", str(message_count))
    export_table.add_row("Size:", size_str)
    export_table.add_row("Format:", format_display)
    console.print(Panel(export_table, title="Session Export", border_style="blue"))
    console.print()

    # Contents panel
    contents_table = Table.grid(padding=(0, 1))
    contents_table.add_column(style="blue bold", justify="left")
    contents_table.add_column(style="white", justify="left")
    contents_table.add_row("Messages:", str(message_count))
    contents_table.add_row("Plan References:", str(plan_refs))
    contents_table.add_row("Metadata Keys:", str(metadata_keys))
    contents_table.add_row("Actor Config:", actor_config)
    contents_table.add_row("Schema Version:", str(schema_version))
    console.print(Panel(contents_table, title="Contents", border_style="blue"))
    console.print()

    # Integrity panel
    integrity_table = Table.grid(padding=(0, 1))
    integrity_table.add_column(style="magenta bold", justify="left")
    integrity_table.add_column(style="white", justify="left")
    integrity_table.add_row("Checksum:", checksum_display)
    integrity_table.add_row("Encrypted:", "no")
    console.print(Panel(integrity_table, title="Integrity", border_style="blue"))
    console.print()

    console.print("[green]✓ OK[/green] Export completed")


@app.command("import")
def import_session(
    input_file: Annotated[
        Path,
        typer.Option("--input", "-i", help="Input JSON file path"),
    ],
) -> None:
    """Import a session from a JSON file.

    The file must have been produced by ``agents session export`` and must
    contain a valid schema version and checksum.

    Examples:
        agents session import -i session.json
    """
    if not input_file.exists():
        console.print(f"[red]File not found:[/red] {input_file}")
        raise typer.Exit(1)

    try:
        raw = input_file.read_text(encoding="utf-8")
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        console.print(f"[red]Invalid JSON:[/red] {exc}")
        raise typer.Exit(1) from exc

    try:
        service = _get_session_service()
        schema_version = data.get("schema_version", "unknown")
        actor_name = data.get("actor_name")
        session = service.import_session(data)

        # Session Import panel
        session_details = (
            f"[bold]Input:[/bold] {input_file}\n"
            f"[bold]Session ID:[/bold] {session.session_id}\n"
            f"[bold]Messages:[/bold] {session.message_count}\n"
            f"[bold]Schema:[/bold] {schema_version}"
        )
        console.print(Panel(session_details, title="Session Import", expand=False))

        # Validation panel
        actor_ref_status = "resolved" if actor_name else "none"
        validation_details = (
            f"[bold]Checksum:[/bold] verified\n"
            f"[bold]Schema:[/bold] compatible\n"
            f"[bold]Actor Ref:[/bold] {actor_ref_status}"
        )
        console.print(Panel(validation_details, title="Validation", expand=False))

        # Merge panel
        merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new"
        console.print(Panel(merge_details, title="Merge", expand=False))

        console.print("[green]✓ OK[/green] Import completed")

    except SessionImportError as exc:
        console.print(f"[red]Import error:[/red] {exc}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session import failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc


@app.command()
def tell(
    prompt: Annotated[
        str,
        typer.Argument(help="Message to send to the session"),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session", help="Session ULID to send message to"),
    ],
    actor: Annotated[
        str | None,
        typer.Option("--actor", help="Override actor for this message"),
    ] = None,
    stream: Annotated[
        bool,
        typer.Option("--stream", help="Stream response in real-time"),
    ] = False,
) -> None:
    """Send a message to a session.

    Appends a user message and generates an assistant response. For M3, the
    actor execution is stubbed — the assistant echoes an acknowledgement.

    Examples:
        agents session tell --session 01HXYZ... "Hello, world"
        agents session tell --session 01HXYZ... --actor openai/gpt-4 "Plan a feature"
        agents session tell --session 01HXYZ... --stream "Build tests"
    """
    try:
        service = _get_session_service()

        # Append user message
        service.append_message(
            session_id=session_id,
            role=MessageRole.USER,
            content=prompt,
        )

        # Stub actor execution: generate simple assistant response
        assistant_content = (
            f"Acknowledged: {prompt[:100]}"
            if not actor
            else f"[{actor}] Acknowledged: {prompt[:100]}"
        )
        service.append_message(
            session_id=session_id,
            role=MessageRole.ASSISTANT,
            content=assistant_content,
        )

        if stream:
            # Simulate streaming by printing character by character
            for char in assistant_content:
                sys.stdout.write(char)
                sys.stdout.flush()
            sys.stdout.write("\n")
        else:
            from rich.markup import escape

            console.print(f"[dim]user:[/dim] {escape(prompt)}")
            console.print(f"[cyan]assistant:[/cyan] {escape(assistant_content)}")

    except SessionNotFoundError as exc:
        console.print(f"[red]Session not found:[/red] {session_id}")
        raise typer.Exit(1) from exc
    except DatabaseError as exc:
        _log.debug("session tell failed", exc_info=True)
        console.print(
            f"[red]Error:[/red] Database unavailable: {exc}\n"
            "Hint: run 'agents init' to initialise the database."
        )
        raise typer.Exit(1) from exc
