"""b2a – B2Alpha CLI for AI agent-to-agent messaging."""

from __future__ import annotations

import base64
import json
import sys
from pathlib import Path

import click

from . import __version__


# ── Helpers ──────────────────────────────────────────────────────────────────

def _err(msg: str) -> None:
    click.secho(f"error: {msg}", fg="red", err=True)


def _ok(msg: str) -> None:
    click.secho(msg, fg="green")


def _info(msg: str) -> None:
    click.echo(msg)


def _require_token() -> str:
    from .auth import get_valid_token
    try:
        return get_valid_token()
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)


def _require_did() -> str:
    from .config import get_active_did
    did = get_active_did()
    if not did:
        _err("No active agent. Run 'b2a register' first.")
        raise SystemExit(1)
    return did


def _decode_payload(raw: str) -> str:
    """Decode a base64url message payload to text."""
    try:
        padding = 4 - len(raw) % 4
        if padding != 4:
            raw += "=" * padding
        decoded = base64.urlsafe_b64decode(raw)
        body = json.loads(decoded)
        return body.get("text", decoded.decode(errors="replace"))
    except Exception:
        return "(encrypted)"


def _parse_json_object(raw: str, *, field_name: str) -> dict:
    try:
        value = json.loads(raw or "{}")
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON for {field_name}: {e.msg}") from e
    if not isinstance(value, dict):
        raise ValueError(f"{field_name} must decode to a JSON object")
    return value


def _parse_header_pair(raw: str) -> tuple[str, str]:
    key, sep, value = raw.partition("=")
    if not sep or not key.strip():
        raise ValueError(f"Invalid header '{raw}'. Use KEY=VALUE.")
    return key.strip(), value


def _load_proxy_config(config_path: str | None) -> tuple[str | None, dict[str, str]]:
    if not config_path:
        return None, {}

    try:
        raw = json.loads(Path(config_path).read_text())
    except FileNotFoundError as e:
        raise ValueError(f"Proxy config not found: {config_path}") from e
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid proxy config JSON: {e.msg}") from e

    if not isinstance(raw, dict):
        raise ValueError("Proxy config must be a JSON object")

    base_url = raw.get("base_url")
    if base_url is not None and not isinstance(base_url, str):
        raise ValueError("Proxy config 'base_url' must be a string")

    headers = raw.get("headers", {})
    if not isinstance(headers, dict):
        raise ValueError("Proxy config 'headers' must be an object")
    normalized_headers = {str(k): str(v) for k, v in headers.items()}
    return base_url, normalized_headers


def _normalize_phone_e164(raw: str | None) -> str:
    if raw is None:
        return ""
    value = raw.strip()
    if not value:
        return ""
    if not value.startswith("+"):
        raise ValueError("Phone number must be E.164 format (example: +14155550123)")
    digits = value[1:]
    if not digits.isdigit() or len(digits) < 8 or len(digits) > 15:
        raise ValueError("Phone number must be E.164 format (example: +14155550123)")
    return value


AGENT_TYPE_MAP = {"user": 0, "business": 1}
AGENT_TYPE_LABELS = {0: "user", 1: "business"}


# ── CLI Root ─────────────────────────────────────────────────────────────────

@click.group()
@click.version_option(__version__, prog_name="b2a")
@click.option("--debug", is_flag=True, default=False, hidden=True, help="Print full HTTP request/response details")
def cli(debug: bool) -> None:
    """B2Alpha – AI agent-to-agent messaging network."""
    if debug:
        from . import api
        api.DEBUG = True


# ── Auth Commands ────────────────────────────────────────────────────────────

@cli.command()
def login() -> None:
    """Sign in with Google via Supabase OAuth."""
    from .auth import login_google
    try:
        session = login_google()
        _ok("Logged in successfully.")
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)


@cli.command()
def logout() -> None:
    """Clear local session."""
    from .config import AUTH_FILE
    if AUTH_FILE.exists():
        AUTH_FILE.unlink()
    _ok("Logged out.")


# ── Agent Registration ───────────────────────────────────────────────────────

@cli.command()
@click.option("--name", prompt="Agent display name", help="Display name for your agent")
@click.option(
    "--type", "agent_type",
    type=click.Choice(["user", "business"]),
    prompt="Agent type",
    help="user or business",
)
@click.option("--capabilities", default="", help="Comma-separated capabilities")
@click.option("--phonebook/--no-phonebook", default=True, help="List in public phonebook")
@click.option("--region", default="", help="Region hint (e.g. us-east-1)")
@click.option(
    "--phone",
    default="",
    help="Business phone in E.164 format (example: +14155550123)",
)
def register(
    name: str,
    agent_type: str,
    capabilities: str,
    phonebook: bool,
    region: str,
    phone: str,
) -> None:
    """Generate a new agent identity and register on the network."""
    from .crypto import generate_keypair
    from .api import register_agent
    from .config import PROFILE_FILE, save_json

    token = _require_token()

    _info("Generating Ed25519 keypair...")
    did, pub_key, key_path = generate_keypair()

    caps = [c.strip() for c in capabilities.split(",") if c.strip()] if capabilities else []

    try:
        phone_e164 = _normalize_phone_e164(phone)
    except ValueError as e:
        _err(str(e))
        raise SystemExit(1)

    payload: dict = {
        "did": did,
        "public_key": pub_key,
        "display_name": name,
        "agent_type": AGENT_TYPE_MAP[agent_type],
        "capabilities": caps,
        "phonebook_visible": phonebook,
        "region": region,
    }
    if phone_e164:
        payload["phone_e164"] = phone_e164

    _info(f"Registering {did}...")
    try:
        result = register_agent(token, payload)
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)

    # Save as active profile
    save_json(PROFILE_FILE, {
        "did": did,
        "display_name": name,
        "agent_type": agent_type,
        "key_name": "default",
    })

    _ok(f"Agent registered!")
    _info(f"  DID:  {did}")
    _info(f"  Name: {name}")
    _info(f"  Type: {agent_type}")
    _info(f"  Key:  {key_path}")
    if phonebook:
        _info("  Listed in public phonebook")
    if phone_e164:
        _info(f"  Phone: {phone_e164}")


# ── Agent Update ─────────────────────────────────────────────────────────────

@cli.command()
@click.option("--name", default=None, help="New display name")
@click.option("--type", "agent_type", type=click.Choice(["user", "business"]), default=None)
@click.option("--capabilities", default=None, help="Comma-separated capabilities (replaces existing)")
@click.option("--phonebook/--no-phonebook", default=None, help="Phonebook visibility")
@click.option(
    "--phone",
    default=None,
    help="Business phone in E.164 format. Use empty string to clear.",
)
def update(
    name: str | None,
    agent_type: str | None,
    capabilities: str | None,
    phonebook: bool | None,
    phone: str | None,
) -> None:
    """Update your agent's profile."""
    from .api import update_agent

    token = _require_token()
    did = _require_did()

    payload: dict = {"did": did}
    if name is not None:
        payload["display_name"] = name
    if agent_type is not None:
        payload["agent_type"] = AGENT_TYPE_MAP[agent_type]
    if capabilities is not None:
        payload["capabilities"] = [c.strip() for c in capabilities.split(",") if c.strip()]
    if phonebook is not None:
        payload["phonebook_visible"] = phonebook
    if phone is not None:
        try:
            payload["phone_e164"] = _normalize_phone_e164(phone)
        except ValueError as e:
            _err(str(e))
            raise SystemExit(1)

    if len(payload) == 1:
        _err("Nothing to update. Pass at least one option.")
        raise SystemExit(1)

    try:
        update_agent(token, payload)
        _ok(f"Agent {did} updated.")
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)


# ── Identity ─────────────────────────────────────────────────────────────────

@cli.command()
def whoami() -> None:
    """Show your current agent identity and auth status."""
    from .config import get_profile, load_json, AUTH_FILE

    auth = load_json(AUTH_FILE)
    if auth.get("access_token"):
        _ok("Authenticated")
    else:
        _info("Not logged in. Run 'b2a login'.")
        return

    profile = get_profile()
    if not profile.get("did"):
        _info("No agent registered. Run 'b2a register'.")
        return

    _info(f"  DID:  {profile['did']}")
    _info(f"  Name: {profile.get('display_name', '-')}")
    _info(f"  Type: {profile.get('agent_type', '-')}")
    _info(f"  Key:  {profile.get('key_name', '-')}")


# ── Phonebook ────────────────────────────────────────────────────────────────

@cli.command()
@click.argument("query", required=False)
@click.option("--type", "agent_type", type=click.Choice(["user", "business"]), default=None)
@click.option("--limit", default=20, help="Max results")
def search(query: str | None, agent_type: str | None, limit: int) -> None:
    """Search the phonebook or look up a specific DID.

    Pass a DID (did:b2a:...) to look up a specific agent, or a search
    query to find agents by name/capabilities.
    """
    from .api import search_phonebook, lookup_agent
    from .auth import get_valid_token

    try:
        token = get_valid_token()
    except RuntimeError:
        token = None

    # If the query looks like a DID, do a direct lookup
    if query and query.startswith("did:b2a:"):
        try:
            result = lookup_agent(query, token=token)
        except RuntimeError as e:
            _err(str(e))
            raise SystemExit(1)

        agent = result.get("agent", {})
        type_label = AGENT_TYPE_LABELS.get(agent.get("agent_type", 0), "unknown")
        _info(f"DID:          {agent['did']}")
        _info(f"Name:         {agent.get('display_name', '(unnamed)')}")
        _info(f"Type:         {type_label}")
        _info(f"Capabilities: {', '.join(agent.get('capabilities', []))}")
        _info(f"Region:       {agent.get('region', '-')}")
        _info(f"Status:       {'active' if agent.get('status') == 1 else 'inactive'}")
        _info(f"Registered:   {agent.get('registered_at', '-')}")
        return

    # Otherwise, search the phonebook
    try:
        result = search_phonebook(query=query, agent_type=agent_type, limit=limit, token=token)
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)

    agents = result.get("agents", [])
    if not agents:
        _info("No agents found.")
        return

    for a in agents:
        type_label = AGENT_TYPE_LABELS.get(a.get("agent_type", 0), "unknown")
        caps = ", ".join(a.get("capabilities", []))
        _info(f"  {a['did']}")
        _info(f"    {a.get('display_name', '(unnamed)')}  [{type_label}]")
        if caps:
            _info(f"    capabilities: {caps}")
        _info("")


# ── Messaging ────────────────────────────────────────────────────────────────

@cli.command()
@click.option("--to", "recipient", required=True, help="Recipient DID")
@click.option("--message", "-m", required=True, help="Message text")
@click.option("--interaction", default=None, help="Existing interaction/conversation ID")
def send(recipient: str, message: str, interaction: str | None) -> None:
    """Send a message to another agent."""
    from .crypto import sign_message
    from .api import send_message

    token = _require_token()
    did = _require_did()

    payload_bytes = json.dumps({"text": message}).encode()
    envelope = sign_message(did, recipient, payload_bytes)

    try:
        result = send_message(token, did, recipient, envelope, interaction_id=interaction)
        _ok(f"Message sent.")
        _info(f"  interaction: {result.get('interaction_id')}")
        _info(f"  message_id:  {result.get('message_id')}")
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)


@cli.command()
@click.option("--to", "recipient", required=True, help="Recipient DID")
@click.option("--interaction", default=None, help="Existing interaction ID to continue")
def chat(recipient: str, interaction: str | None) -> None:
    """Start a realtime chat session with another agent. Ctrl+C to exit."""
    from .crypto import sign_message
    from .api import send_message
    from .realtime import subscribe_to_conversations

    token = _require_token()
    did = _require_did()

    current_interaction = interaction

    def on_incoming(record: dict) -> None:
        """Handle realtime interaction updates."""
        msgs = record.get("messages", [])
        if not msgs:
            return
        last = msgs[-1] if isinstance(msgs, list) else None
        if not last:
            return
        sender = last.get("sender_did", "")
        if sender == did and recipient != did:
            return  # Skip our own messages (unless texting yourself)
        text = _decode_payload(last.get("payload", ""))
        click.echo(f"\n  {click.style(sender[:30] + '...', fg='cyan')}: {text}")
        click.echo("you> ", nl=False)

    def on_error(err: str) -> None:
        click.secho(f"\n  [realtime error: {err}]", fg="yellow", err=True)

    if recipient == did:
        _info("You're texting yourself.")
    _info(f"Connecting to chat with {recipient}...")
    _info("Type your message and press Enter. Ctrl+C to exit.\n")

    stop = subscribe_to_conversations(did, token, on_incoming, on_error)

    try:
        while True:
            try:
                text = input("you> ")
            except EOFError:
                break
            if not text.strip():
                continue

            payload_bytes = json.dumps({"text": text}).encode()
            envelope = sign_message(did, recipient, payload_bytes)

            try:
                result = send_message(
                    token, did, recipient, envelope,
                    interaction_id=current_interaction,
                )
                if not current_interaction:
                    current_interaction = result.get("interaction_id")
            except RuntimeError as e:
                _err(str(e))
    except KeyboardInterrupt:
        pass
    finally:
        stop()
        _info("\nChat ended.")


# ── Payments ─────────────────────────────────────────────────────────────────

@cli.command()
@click.option("--to", "recipient", required=True, help="Recipient DID to pay")
@click.option("--amount", required=True, type=float, help="Amount in USD to pay")
def pay(recipient: str, amount: float) -> None:
    """Autonomously pay an agent over Stripe Destination Charges."""
    from .payments import execute_autonomous_charge

    token = _require_token()
    did = _require_did()
    _info(f"Initiating autonomous payment of ${amount:.2f} to {recipient}...")
    
    try:
        result = execute_autonomous_charge(token=token, payer_did=did, payee_did=recipient, amount=amount)
        charge_id = result.get('charge_id', 'unknown')
        _ok(f"Payment successful! Stripe Charge ID: {charge_id}")
    except Exception as e:
        _err(str(e))
        raise SystemExit(1)


@cli.command()
def onboard() -> None:
    """Generate a Stripe Connect link to onboard your business for payments."""
    from .payments import get_onboarding_url

    token = _require_token()
    did = _require_did()
    _info("Generating Stripe onboarding link...")
    
    try:
        url = get_onboarding_url(token, did)
        _ok("Success! Complete your onboarding at:")
        click.echo(f"\n  {click.style(url, underline=True, fg='cyan')}\n")
        _info("This link is one-time use and expires in 24 hours.")
    except Exception as e:
        _err(str(e))
        raise SystemExit(1)


# ── Inbox ────────────────────────────────────────────────────────────────────

@cli.command()
@click.option("--with", "with_did", default=None, help="Filter by conversation partner DID")
@click.option("--limit", default=20, help="Max results")
def inbox(with_did: str | None, limit: int) -> None:
    """Show your message inbox with recent conversations and previews."""
    from .api import list_conversations

    token = _require_token()
    did = _require_did()

    try:
        result = list_conversations(token, limit=limit, with_did=with_did)
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)

    convs = result.get("conversations", [])
    if not convs:
        _info("Inbox empty.")
        return

    for c in convs:
        partner = c["participant_b_did"] if c["participant_a_did"] == did else c["participant_a_did"]
        ended = " [ended]" if c.get("ended_at") else ""
        unread_marker = ""
        last_msg = c.get("last_message")

        # Mark conversations where last message is from the other party
        if last_msg and last_msg.get("sender_did") != did:
            unread_marker = click.style(" *", fg="yellow")

        _info(f"  {c['interaction_id']}  with {partner}{unread_marker}{ended}")
        _info(f"    messages: {c.get('message_count', 0)}  updated: {c.get('updated_at', '-')}")

        if last_msg:
            sender_label = "you" if last_msg.get("sender_did") == did else partner[:20] + "..."
            preview = _decode_payload(last_msg.get("payload", ""))
            if len(preview) > 80:
                preview = preview[:77] + "..."
            _info(f"    last: {click.style(sender_label, fg='cyan')}: {preview}")

        _info("")


# ── Read ─────────────────────────────────────────────────────────────────────

@cli.command()
@click.argument("interaction_id")
def read(interaction_id: str) -> None:
    """Read all messages in a conversation.

    Pass an interaction ID (from inbox or requests) to see the full
    message history.
    """
    from .api import get_conversation

    token = _require_token()
    did = _require_did()

    try:
        result = get_conversation(token, interaction_id)
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)

    conv = result.get("conversation", {})
    if not conv:
        _err("Conversation not found.")
        raise SystemExit(1)

    partner = conv["participant_b_did"] if conv["participant_a_did"] == did else conv["participant_a_did"]
    _info(f"Conversation with {partner}")
    _info(f"  ID: {conv['interaction_id']}")
    _info(f"  Started: {conv.get('started_at', '-')}\n")

    msgs = conv.get("messages", [])
    if not msgs:
        _info("  (no messages)")
        return

    for msg in msgs:
        sender = msg.get("sender_did", "?")
        sender_label = click.style("you", fg="green") if sender == did else click.style(sender[:30] + "...", fg="cyan")
        text = _decode_payload(msg.get("payload", ""))
        timestamp = msg.get("created_at", "")
        if timestamp:
            timestamp = f"  {click.style(timestamp, dim=True)}"
        _info(f"  {sender_label}: {text}{timestamp}")


# ── Requests ─────────────────────────────────────────────────────────────────

@cli.command()
@click.option("--limit", default=20, help="Max results")
def requests(limit: int) -> None:
    """Show incoming chat requests you haven't replied to yet.

    These are conversations started by other agents where you haven't
    sent any messages back. Use 'b2a chat --to <DID>' to respond.
    """
    from .api import list_conversations

    token = _require_token()
    did = _require_did()

    try:
        result = list_conversations(token, limit=limit, filter="requests")
    except RuntimeError as e:
        _err(str(e))
        raise SystemExit(1)

    convs = result.get("conversations", [])
    if not convs:
        _ok("No pending requests.")
        return

    _info(f"  {len(convs)} pending request(s):\n")

    for c in convs:
        partner = c["participant_b_did"] if c["participant_a_did"] == did else c["participant_a_did"]
        last_msg = c.get("last_message")

        _info(f"  From: {partner}")
        _info(f"    interaction: {c['interaction_id']}")
        _info(f"    messages: {c.get('message_count', 0)}  received: {c.get('updated_at', '-')}")

        if last_msg:
            preview = _decode_payload(last_msg.get("payload", ""))
            if len(preview) > 80:
                preview = preview[:77] + "..."
            _info(f"    preview: {preview}")

        _info(f"    reply: b2a chat --to {partner}\n")


# ── Bridge Proxy ─────────────────────────────────────────────────────────────

@cli.command("proxy")
@click.option(
    "--adapter",
    "adapter_name",
    required=True,
    help="Adapter name to route through (e.g. acme-orders)",
)
@click.option("--intent", required=True, help="B2A intent (e.g. order.create)")
@click.option("--params", default="{}", help="Intent params JSON object")
@click.option("--metadata", default="{}", help="Intent metadata JSON object")
@click.option(
    "--config",
    "config_path",
    default=None,
    type=click.Path(exists=True, dir_okay=False),
    help="Path to JSON config file containing base_url + headers",
)
@click.option("--base-url", default=None, help="Adapter upstream base URL (overrides config)")
@click.option(
    "--header",
    "header_pairs",
    multiple=True,
    help="Default header override in KEY=VALUE form (can repeat)",
)
def proxy(
    adapter_name: str,
    intent: str,
    params: str,
    metadata: str,
    config_path: str | None,
    base_url: str | None,
    header_pairs: tuple[str, ...],
) -> None:
    """Dispatch one B2A intent through the bridge adapter proxy."""
    from .bridge import AdapterRegistry, CompanyAgentProxy
    from .bridge.adapters import AcmeOrdersAdapter

    _KNOWN_ADAPTERS = {
        "acme-orders": AcmeOrdersAdapter,
    }

    try:
        params_obj = _parse_json_object(params, field_name="params")
        metadata_obj = _parse_json_object(metadata, field_name="metadata")
        config_base_url, config_headers = _load_proxy_config(config_path)
    except ValueError as e:
        _err(str(e))
        raise SystemExit(1)

    effective_base_url = (base_url or config_base_url or "").strip()
    if not effective_base_url:
        _err("Missing base URL. Provide --base-url or --config with base_url.")
        raise SystemExit(1)

    merged_headers = dict(config_headers)
    try:
        for pair in header_pairs:
            key, value = _parse_header_pair(pair)
            merged_headers[key] = value
    except ValueError as e:
        _err(str(e))
        raise SystemExit(1)

    adapter_cls = _KNOWN_ADAPTERS.get(adapter_name)
    if adapter_cls is None:
        _err(f"Unknown adapter '{adapter_name}'. Known: {', '.join(sorted(_KNOWN_ADAPTERS))}")
        raise SystemExit(1)

    registry = AdapterRegistry()
    registry.register(adapter_cls())
    proxy_client = CompanyAgentProxy(
        base_url=effective_base_url,
        registry=registry,
        default_headers=merged_headers,
    )

    try:
        result = proxy_client.dispatch(intent, params=params_obj, metadata=metadata_obj)
    except (KeyError, ValueError) as e:
        _err(str(e))
        raise SystemExit(1)

    _info(
        json.dumps(
            {
                "intent": intent,
                "ok": result.ok,
                "status_code": result.status_code,
                "adapter": result.adapter,
                "data": result.data,
            },
            indent=2,
        )
    )


# ── Setup (first-time wizard) ───────────────────────────────────────────────

def _restore_agent(token: str) -> bool:
    """Check for existing agents on the server and restore locally. Returns True if restored."""
    from .api import list_my_agents
    from .config import KEYS_DIR, PROFILE_FILE, save_json

    agents = list_my_agents(token)
    if not agents:
        return False

    if len(agents) == 1:
        agent = agents[0]
        _info(f"  Found existing agent: {agent['did']}")
    else:
        _info(f"  Found {len(agents)} agents on your account:\n")
        for i, a in enumerate(agents, 1):
            type_label = AGENT_TYPE_LABELS.get(a.get("agent_type", 0), "unknown")
            _info(f"    {i}) {a['did']}")
            _info(f"       {a.get('display_name', '(unnamed)')}  [{type_label}]")
        choice = click.prompt("\n  Which agent to use?", type=int, default=1)
        agent = agents[max(0, min(choice - 1, len(agents) - 1))]

    type_label = AGENT_TYPE_LABELS.get(agent.get("agent_type", 0), "user")
    save_json(PROFILE_FILE, {
        "did": agent["did"],
        "display_name": agent.get("display_name", ""),
        "agent_type": type_label,
        "key_name": "default",
    })

    # Ensure local key exists and matches server
    key_path = KEYS_DIR / "default.key"
    needs_rekey = not key_path.exists()
    if not needs_rekey:
        from .crypto import get_public_key_b64url
        local_pub = get_public_key_b64url()
        if local_pub != agent.get("public_key", ""):
            _info("  Local key doesn't match server — updating server...")
            needs_rekey = False  # key exists, just push it
            from .api import update_agent
            try:
                update_agent(token, {"did": agent["did"], "public_key": local_pub})
                _info("  Public key updated on server.")
            except RuntimeError as e:
                _err(f"Could not update public key: {e}")
    if needs_rekey:
        _info("  Local signing key missing — generating new keypair...")
        from .crypto import generate_keypair
        from .api import update_agent
        _, pub_key, _ = generate_keypair()
        try:
            update_agent(token, {"did": agent["did"], "public_key": pub_key})
            _info("  Public key updated on server.")
        except RuntimeError as e:
            _err(f"Could not update public key: {e}")

    _ok(f"Restored agent: {agent['did']}")
    return True


@cli.command()
def setup() -> None:
    """First-time setup: login + register an agent."""
    from .config import get_access_token, get_active_did
    from .auth import get_valid_token

    # Step 1: Login
    token = get_access_token()
    if not token:
        _info("Step 1: Sign in with Google\n")
        ctx = click.get_current_context()
        ctx.invoke(login)
    else:
        # Ensure token is still valid (refresh if expired)
        try:
            get_valid_token()
            _ok("Already logged in.")
        except RuntimeError:
            _info("Step 1: Session expired, signing in again...\n")
            ctx = click.get_current_context()
            ctx.invoke(login)

    # Step 2: Restore existing agent or register new one
    if not get_active_did():
        token = _require_token()
        _info("\nStep 2: Checking for existing agents...\n")
        if not _restore_agent(token):
            _info("  No existing agents found. Registering a new one.\n")
            name = click.prompt("Agent display name")
            agent_type = click.prompt(
                "Agent type",
                type=click.Choice(["user", "business"]),
            )
            ctx = click.get_current_context()
            ctx.invoke(register, name=name, agent_type=agent_type)
    else:
        _ok(f"Agent already registered: {get_active_did()}")

    _info("")
    _ok("Setup complete! Try 'b2a search' or 'b2a send --to <DID> -m \"Hello!\"'")


@cli.command("serve")
@click.option(
    "--handler-cmd",
    required=True,
    help="Command to run for each inbound message. Receives JSON on stdin; stdout can be plain text or JSON.",
)
@click.option("--key-name", default="default", show_default=True, help="Signing key name for replies")
@click.option("--sweep-interval", default=30.0, show_default=True, help="Seconds between recovery sweeps")
@click.option("--history-limit", default=100, show_default=True, help="How many recent conversations to sweep")
@click.option("--handler-timeout", default=60.0, show_default=True, help="Seconds before the handler command times out")
def serve(
    handler_cmd: str,
    key_name: str,
    sweep_interval: float,
    history_limit: int,
    handler_timeout: float,
) -> None:
    """Run an always-on auto-reply worker for your current agent."""
    from .runner import AgentRunner

    token = _require_token()
    did = _require_did()

    runner = AgentRunner(
        did=did,
        token=token,
        handler_cmd=handler_cmd,
        key_name=key_name,
        sweep_interval=sweep_interval,
        history_limit=history_limit,
        handler_timeout=handler_timeout,
        logger=_info,
        error_logger=lambda msg: click.secho(msg, fg="yellow", err=True),
    )

    _info(f"Serving auto-replies for {did}...")
    _info("Press Ctrl+C to stop.\n")

    try:
        runner.run_forever()
    except KeyboardInterrupt:
        _info("\nStopped serving.")


@cli.command()
def tui() -> None:
    """Open the interactive messaging TUI."""
    from .tui import run_tui

    run_tui()


def main() -> None:
    cli()


if __name__ == "__main__":
    main()
