# 中文注释：
# 文件：echobot/cli/commands.py
# 说明：命令行入口与运维命令实现。

"""CLI commands for echobot."""

import asyncio
import os
import sys
from datetime import datetime
from pathlib import Path

import typer
from loguru import logger
import os
from rich.console import Console

# 全局变量：标记是否已经添加过文件日志 handler
# 防止重复添加导致日志重复写入
# 使用进程ID作为key，这样 fork 后的子进程会认为是新的
_file_handler_added = {}
from rich.table import Table

# 默认静默模式（移除所有日志处理器）
logger.remove()

from echobot import __version__, __logo__

app = typer.Typer(
    name="echobot",
    help=f"{__logo__} echobot - Personal AI Assistant",
    no_args_is_help=True,
)

console = Console()


def _can_bind_port(port: int, host: str = "0.0.0.0") -> bool:
    """Return True if a TCP port can be bound on the target host."""
    import socket

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind((host, port))
        except OSError:
            return False
    return True


def _find_available_port(
    preferred_port: int,
    reserved_ports: set[int] | None = None,
    host: str = "0.0.0.0",
) -> tuple[int, bool]:
    """
    Find a bindable TCP port.

    Returns:
        (selected_port, changed)
        - selected_port: chosen port number
        - changed: True if fallback happened
    """
    if not (1 <= preferred_port <= 65535):
        raise ValueError(f"Invalid port: {preferred_port}")

    reserved = reserved_ports or set()

    if preferred_port not in reserved and _can_bind_port(preferred_port, host=host):
        return preferred_port, False

    for candidate in range(preferred_port + 1, 65536):
        if candidate in reserved:
            continue
        if _can_bind_port(candidate, host=host):
            return candidate, True

    for candidate in range(1024, preferred_port):
        if candidate in reserved:
            continue
        if _can_bind_port(candidate, host=host):
            return candidate, True

    raise RuntimeError("No available TCP port found")


def _resolve_startup_ports(
    gateway_port: int,
    host: str = "0.0.0.0",
) -> tuple[int, list[str]]:
    """
    Resolve final startup port and return user-facing warnings.

    Warnings are generated when the requested port is occupied.
    """
    warnings: list[str] = []

    resolved_gateway_port, gateway_changed = _find_available_port(gateway_port, host=host)
    if gateway_changed:
        warnings.append(
            f"Gateway port {gateway_port} is in use, automatically switched to {resolved_gateway_port}."
        )

    return resolved_gateway_port, warnings


def _resolve_instance_name(instance: str | None = None) -> str | None:
    """
    Resolve effective instance name.

    Priority:
    1. Explicit CLI argument
    2. ECHOBOT_INSTANCE environment variable
    3. NANOBOT_INSTANCE environment variable (compat)
    """
    if instance:
        return instance
    return os.environ.get("ECHOBOT_INSTANCE") or os.environ.get("NANOBOT_INSTANCE") or None


def version_callback(value: bool):
    if value:
        console.print(f"{__logo__} echobot v{__version__}")
        raise typer.Exit()


@app.callback()
def main(
    version: bool = typer.Option(None, "--version", "-v", callback=version_callback, is_eager=True),
):
    """echobot - Personal AI Assistant."""
    pass


# ============================================================================
# Onboard / Setup
# ============================================================================


def _create_workspace_templates(workspace: Path):
    """Create default workspace template files."""
    templates = {
        "AGENTS.md": """# Agent Instructions

You are a helpful AI assistant. Be concise, accurate, and friendly.

## Guidelines

- Always explain what you're doing before taking actions
- Ask for clarification when the request is ambiguous
- Use tools to help accomplish tasks
- Remember important information in your memory files
""",
        "SOUL.md": """# Soul

I am echobot, a lightweight AI assistant.

## Personality

- Helpful and friendly
- Concise and to the point
- Curious and eager to learn

## Values

- Accuracy over speed
- User privacy and safety
- Transparency in actions
""",
        "USER.md": """# User

Information about the user goes here.

## Preferences

- Communication style: (casual/formal)
- Timezone: (your timezone)
- Language: (your preferred language)
""",
    }

    for filename, content in templates.items():
        file_path = workspace / filename
        if not file_path.exists():
            file_path.write_text(content)
            console.print(f"  [dim]Created {filename}[/dim]")

    # Create memory directory and MEMORY.md
    memory_dir = workspace / "memory"
    memory_dir.mkdir(exist_ok=True)
    memory_file = memory_dir / "MEMORY.md"
    if not memory_file.exists():
        memory_file.write_text("""# Long-term Memory

This file stores important information that should persist across sessions.

## User Information

(Important facts about the user)

## Preferences

(User preferences learned over time)

## Important Notes

(Things to remember)
""")
        console.print("  [dim]Created memory/MEMORY.md[/dim]")


# ============================================================================
# Gateway / Server
# ============================================================================


@app.command()
def gateway(
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output (show logs)"),
    role: str = typer.Option(
        None, "--role", "-r", help="Role to use (e.g., default, coder, tester)"
    ),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (creates isolated environment)"
    ),
    multi: bool = typer.Option(
        False,
        "--multi",
        help="Enable multi-agent mode (requires agents.json configuration)",
    ),
    agents_config: str = typer.Option(
        None,
        "--agents-config",
        help="Path to agents.json configuration file",
    ),
    daemon: bool = typer.Option(
        True,
        "--daemon/--foreground",
        help="Run in background by default; use --foreground to run in current terminal",
    ),
    daemon_log: str = typer.Option(
        None, "--daemon-log", help="Log file path for daemon mode (default: ~/.echobot/.../logs/)"
    ),
    daemon_pid: str = typer.Option(
        None, "--daemon-pid", help="PID file path for daemon mode (default: ~/.echobot/.../*.pid)"
    ),
    stop: bool = typer.Option(
        False,
        "--stop",
        help="Stop the background gateway process and exit",
    ),
    restart: bool = typer.Option(
        False,
        "--restart",
        help="Restart the gateway process (stop first, then start with current args)",
    ),
    _daemon_worker: bool = typer.Option(
        False, "--daemon-worker", hidden=True
    ),
):
    """Start the echobot gateway."""
    import subprocess
    import sys
    from echobot.config.loader import load_config, get_data_dir
    from echobot.bus.queue import MessageBus
    from echobot.providers.litellm_provider import LiteLLMProvider
    from echobot.agent.loop import AgentLoop
    from echobot.channels.manager import ChannelManager
    from echobot.cron.service import CronService
    from echobot.cron.types import CronJob
    from echobot.heartbeat.service import HeartbeatService
    from echobot.utils.helpers import get_data_path
    from loguru import logger
    import asyncio

    instance = _resolve_instance_name(instance)

    def _resolve_daemon_paths() -> tuple[Path, Path]:
        """
        计算 daemon 日志与 PID 文件路径。

        设计说明：
        1. 默认放到实例对应的数据目录，确保多实例隔离。
        2. 支持用户通过 `--daemon-log`、`--daemon-pid` 覆盖默认路径。
        """
        data_root = get_data_path(instance) if instance else get_data_dir()
        logs_dir = data_root / "logs"
        logs_dir.mkdir(parents=True, exist_ok=True)

        today = datetime.now().strftime("%Y_%m_%d")
        prefix = "gateway-multi" if multi else "gateway"
        default_log_name = f"{prefix}_{today}.log" if not instance else f"{prefix}-{instance}_{today}.log"
        default_pid_name = f"{prefix}.pid" if not instance else f"{prefix}-{instance}.pid"

        log_path = Path(daemon_log).expanduser() if daemon_log else (logs_dir / default_log_name)
        pid_path = Path(daemon_pid).expanduser() if daemon_pid else (data_root / default_pid_name)
        return log_path, pid_path

    def _stop_gateway_process(
        *,
        exit_after_stop: bool,
        allow_missing: bool = False,
        action: str = "stop",
    ) -> None:
        """
        停止后台 Gateway 进程。

        参数:
            exit_after_stop: 停止成功后是否立即退出命令
            allow_missing: 若未找到运行进程，是否允许继续
            action: 操作名称（用于日志提示）
        """
        import signal
        import time

        _, pid_path = _resolve_daemon_paths()
        instance_str = f" (instance: {instance})" if instance else ""

        def _safe_unlink_pid_file() -> None:
            try:
                pid_path.unlink(missing_ok=True)
            except PermissionError:
                # 文件权限异常不应影响“进程已经停止”的主流程结果。
                console.print(f"[yellow]Warning:[/yellow] Unable to remove PID file: {pid_path}")

        if not pid_path.exists():
            if allow_missing:
                console.print(f"[yellow]No running gateway found{instance_str}, continue {action}[/yellow]")
                console.print(f"[dim]PID file not found: {pid_path}[/dim]")
                return
            console.print(f"[yellow]No running gateway found{instance_str}[/yellow]")
            console.print(f"[dim]PID file not found: {pid_path}[/dim]")
            raise typer.Exit(0)

        try:
            pid = int(pid_path.read_text(encoding="utf-8").strip())
        except ValueError:
            console.print(f"[red]Invalid PID file content: {pid_path}[/red]")
            # PID 文件损坏时直接清理，避免后续一直误判。
            _safe_unlink_pid_file()
            if allow_missing:
                console.print("[yellow]Continue startup after cleaning invalid PID file[/yellow]")
                return
            raise typer.Exit(1)

        stop_scope = "process"
        try:
            # 优先向 gateway worker 的进程组发送 SIGTERM，
            # 这样可以同时停止它启动的子进程。
            pgid = os.getpgid(pid)
            os.killpg(pgid, signal.SIGTERM)
            stop_scope = "process group"
        except ProcessLookupError:
            # 进程不存在，视为陈旧 PID 文件，清理后返回成功。
            _safe_unlink_pid_file()
            if allow_missing:
                console.print(f"[yellow]Stale PID file removed: {pid_path}[/yellow]")
                return
            console.print(f"[yellow]Stale PID file removed: {pid_path}[/yellow]")
            raise typer.Exit(0)
        except PermissionError:
            console.print(f"[red]Permission denied when stopping PID {pid}[/red]")
            raise typer.Exit(1)
        except OSError:
            # 某些环境下可能无法获取/信号进程组，回退为只停主进程。
            try:
                os.kill(pid, signal.SIGTERM)
            except ProcessLookupError:
                _safe_unlink_pid_file()
                if allow_missing:
                    console.print(f"[yellow]Stale PID file removed: {pid_path}[/yellow]")
                    return
                console.print(f"[yellow]Stale PID file removed: {pid_path}[/yellow]")
                raise typer.Exit(0)
            except PermissionError:
                console.print(f"[red]Permission denied when stopping PID {pid}[/red]")
                raise typer.Exit(1)

        # 轮询最多 5 秒，确认进程已退出。
        stopped = False
        for _ in range(50):
            try:
                os.kill(pid, 0)
            except OSError:
                stopped = True
                break
            time.sleep(0.1)

        if stopped:
            _safe_unlink_pid_file()
            console.print(
                f"[green]✓[/green] Gateway stopped{instance_str} (PID: {pid}, scope: {stop_scope})"
            )
            if exit_after_stop:
                raise typer.Exit(0)
            return

        console.print(f"[red]Gateway did not stop in time (PID: {pid})[/red]")
        console.print(f"[dim]You can run: kill -9 {pid}[/dim]")
        raise typer.Exit(1)

    if stop and restart:
        console.print("[red]Invalid options: --stop and --restart cannot be used together[/red]")
        raise typer.Exit(2)

    if (stop or restart) and _daemon_worker:
        # 防御性分支：worker 进程不应该带 stop/restart 参数。
        console.print("[red]Invalid daemon worker state: stop/restart flags are not allowed[/red]")
        raise typer.Exit(2)

    # 停止命令优先级最高，执行后立即退出，不进入启动流程。
    if stop:
        _stop_gateway_process(exit_after_stop=True)

    # 重启命令：先尝试停止（允许当前未运行），然后继续走启动流程。
    if restart:
        _stop_gateway_process(exit_after_stop=False, allow_missing=True, action="restart")

    # 规范实例上下文：保证后续由 Agent 触发的 `echobot cron ...` 命令默认落到当前实例。
    if instance:
        os.environ["ECHOBOT_INSTANCE"] = instance
    else:
        os.environ.pop("ECHOBOT_INSTANCE", None)

    # 后台模式（父进程）：仅负责拉起子进程并立即返回。
    # 子进程会带上隐藏参数 `--daemon-worker`，以避免重复进入该分支。
    if daemon and not _daemon_worker:
        # 为后台启动回显补充当前实例配置中的服务端口，便于运维排查。
        preview_config = load_config(instance=instance)
        preview_gateway_port = preview_config.gateway.port
        log_path, pid_path = _resolve_daemon_paths()

        # 若 PID 文件存在且进程仍存活，则直接返回，避免重复启动。
        if pid_path.exists():
            try:
                existing_pid = int(pid_path.read_text(encoding="utf-8").strip())
            except ValueError:
                existing_pid = None

            if existing_pid:
                try:
                    os.kill(existing_pid, 0)
                except OSError:
                    # 旧 PID 已失效，继续启动新进程并覆盖 PID 文件。
                    pass
                else:
                    console.print(f"[yellow]Gateway is already running (PID: {existing_pid})[/yellow]")
                    console.print(f"[dim]PID file: {pid_path}[/dim]")
                    raise typer.Exit(0)

        # 使用当前 Python 环境重新拉起同一条 gateway 命令。
        cmd = [
            sys.executable,
            "-m",
            "echobot",
            "gateway",
            "--daemon-worker",
        ]
        if verbose:
            cmd.append("--verbose")
        if role:
            cmd.extend(["--role", role])
        if instance:
            cmd.extend(["--instance", instance])
        if multi:
            cmd.append("--multi")
        if agents_config:
            cmd.extend(["--agents-config", agents_config])

        log_path.parent.mkdir(parents=True, exist_ok=True)
        pid_path.parent.mkdir(parents=True, exist_ok=True)

        # `start_new_session=True` 使子进程脱离当前终端会话，实现真正后台运行。
        with open(log_path, "a", encoding="utf-8") as log_file:
            process = subprocess.Popen(
                cmd,
                stdin=subprocess.DEVNULL,
                stdout=log_file,
                stderr=subprocess.STDOUT,
                start_new_session=True,
            )

        pid_path.write_text(f"{process.pid}\n", encoding="utf-8")

        instance_str = f" (instance: {instance})" if instance else ""
        console.print(f"[green]✓[/green] Gateway started in background{instance_str}")
        console.print(f"[cyan]PID:[/cyan] {process.pid}")
        console.print(f"[cyan]Port:[/cyan] {preview_gateway_port}")
        console.print(f"[cyan]Log:[/cyan] {log_path}")
        console.print(f"[cyan]PID file:[/cyan] {pid_path}")
        console.print(f"[cyan]Stop:[/cyan] echobot gateway --stop{f' --instance {instance}' if instance else ''}")
        console.print(f"[cyan]Restart:[/cyan] echobot gateway --restart{f' --instance {instance}' if instance else ''}")
        return

    # 配置日志输出
    if verbose:
        logger.remove()
        logger.add(
            sys.stderr,
            format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
            level="DEBUG",
        )
    elif _daemon_worker:
        # Daemon worker 模式下，默认日志输出到 stderr（会被重定向到日志文件）
        logger.remove()
        logger.add(
            sys.stderr,
            format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
            level="INFO",
        )

    instance_str = f" (instance: {instance})" if instance else ""
    console.print(f"{__logo__} Starting echobot gateway{instance_str}...")

    config = load_config(instance=instance)
    logger.info(f"Gateway configured endpoint: {config.gateway.host}:{config.gateway.port}")
    console.print(f"[dim]Endpoint:[/dim] {config.gateway.host}:{config.gateway.port}")

    # Create bus
    bus = MessageBus()

    # Create provider
    api_key = config.get_api_key()
    api_base = config.get_api_base()

    if not api_key:
        console.print("[red]Error: No API key configured.[/red]")
        console.print("Set one in ~/.echobot/config.json under providers.openrouter.apiKey")
        raise typer.Exit(1)

    provider = LiteLLMProvider(
        api_key=api_key, api_base=api_base, default_model=config.agents.defaults.model
    )

    # 判断是否使用多 Agent 模式
    if multi:
        from echobot.config.loader import load_agents_config
        from echobot.agent.multi import MultiAgentManager
        from pathlib import Path as PathLib

        # 加载 agents.json
        agents_config_path = PathLib(agents_config) if agents_config else None
        agents_config = load_agents_config(agents_config_path)

        if not agents_config.agents:
            console.print("[red]Error: No agents configured in agents.json[/red]")
            console.print("Please create ~/.echobot/agents.json with your agent configurations")
            raise typer.Exit(1)

        # 收集默认配置
        defaults_dict = {
            "model": config.agents.defaults.model,
            "max_tool_iterations": config.agents.defaults.max_tool_iterations,
            "exec_allowed_dirs": config.agents.defaults.exec_allowed_dirs,
            "default_role": config.agents.defaults.default_role,
            "context_compression_enabled": config.agents.defaults.context_compression_enabled,
            "context_history_max_messages": config.agents.defaults.context_history_max_messages,
            "context_keep_recent_messages": config.agents.defaults.context_keep_recent_messages,
            "context_summary_max_chars": config.agents.defaults.context_summary_max_chars,
            "approval_ttl_seconds": config.agents.defaults.approval_ttl_seconds,
            "approval_required_tools": config.agents.defaults.approval_required_tools,
            "denied_exec_patterns": config.agents.defaults.denied_exec_patterns,
            "webhook_secret": config.agents.defaults.webhook_secret,
        }

        # 创建多 Agent 管理器
        multi_agent_manager = MultiAgentManager(
            config=agents_config,
            provider=provider,
            defaults=defaults_dict,
            channels_config=config.channels,
        )

        # 加载并启动所有 Agent
        multi_agent_manager.load_agents()

        if not multi_agent_manager.agents:
            console.print("[red]Error: No agents could be loaded[/red]")
            raise typer.Exit(1)

        console.print(f"[green]✓[/green] Multi-agent mode enabled")
        console.print(f"[cyan]Agents:[/cyan] {', '.join(a['id'] for a in multi_agent_manager.list_agents())}")

        if agents_config.collaboration.enabled:
            console.print(f"[green]✓[/green] Collaboration: enabled")
            console.print(f"[cyan]Allowed calls:[/cyan] {', '.join(agents_config.collaboration.allow_calls)}")

        async def run_multi_agent_gateway():
            """运行多 Agent Gateway 核心服务"""
            await multi_agent_manager.start_all()
            # 保持运行
            while True:
                await asyncio.sleep(3600)

        async def main():
            await run_multi_agent_gateway()
            console.print("\n🛑 停止服务...")
            await multi_agent_manager.stop_all()

        asyncio.run(main())
        return

    # Create agent (single agent mode)
    effective_default_role = config.agents.defaults.default_role if not role else None
    agent = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        instance=instance,
        model=config.agents.defaults.model,
        max_iterations=config.agents.defaults.max_tool_iterations,
        brave_api_key=config.tools.web.search.api_key or None,
        role_name=role,
        default_role=effective_default_role,
        # 上下文压缩参数（从配置注入，便于实例级调优）
        context_compression_enabled=config.agents.defaults.context_compression_enabled,
        context_history_max_messages=config.agents.defaults.context_history_max_messages,
        context_keep_recent_messages=config.agents.defaults.context_keep_recent_messages,
        context_summary_max_chars=config.agents.defaults.context_summary_max_chars,
        # 审批与 webhook 参数
        approval_ttl_seconds=config.agents.defaults.approval_ttl_seconds,
        approval_required_tools=config.agents.defaults.approval_required_tools,
        denied_exec_patterns=config.agents.defaults.denied_exec_patterns,
        webhook_secret=config.agents.defaults.webhook_secret or None,
        exec_allowed_dirs=config.agents.defaults.exec_allowed_dirs,
    )

    # 显示角色信息
    if role:
        role_info = agent.get_role_info(role)
        if role_info:
            console.print(f"[cyan]Role:[/cyan] {role_info['display_name']}")
        else:
            console.print(f"[yellow]Warning: Role '{role}' not found[/yellow]")
    else:
        console.print(f"[cyan]Default role:[/cyan] {agent.get_current_role()}")

    # 创建 cron service
    async def on_cron_job(job: CronJob) -> str | None:
        response = await agent.process_direct(job.payload.message, session_key=f"cron:{job.id}")
        if job.payload.deliver and job.payload.to:
            from echobot.bus.events import OutboundMessage

            await bus.publish_outbound(
                OutboundMessage(
                    channel=job.payload.channel or "telegram",
                    chat_id=job.payload.to,
                    content=response or "",
                )
            )
        return response

    cron_store_path = _get_cron_store_path(instance)
    cron_store_path.parent.mkdir(parents=True, exist_ok=True)
    cron = CronService(cron_store_path, on_job=on_cron_job)
    agent.set_cron_service(cron)

    # 创建 heartbeat service
    async def on_heartbeat(prompt: str) -> str:
        return await agent.process_direct(prompt, session_key="heartbeat")

    heartbeat = HeartbeatService(
        workspace=config.workspace_path,
        on_heartbeat=on_heartbeat,
        interval_s=30 * 60,
        enabled=True,
    )

    # 创建 channel manager
    channels = ChannelManager(config, bus)

    if channels.enabled_channels:
        console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
    else:
        console.print("[yellow]Warning: No channels enabled[/yellow]")

    cron_status = cron.status()
    if cron_status["jobs"] > 0:
        console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs")

    console.print(f"[green]✓[/green] Heartbeat: every 30m")
    console.print(f"[dim]Workspace:[/dim] {config.workspace_path}")
    console.print(f"[dim]Sessions:[/dim] {agent.sessions.sessions_dir}")
    console.print(f"[dim]Cron store:[/dim] {cron_store_path}")

    async def run_gateway_core():
        """运行 Gateway 核心服务"""
        await cron.start()
        await heartbeat.start()
        await channels.start_all()
        await agent.run()

    async def main():
        """运行 Gateway 核心服务"""
        # 注意：由于 fork 导致的日志重复问题暂时无法简单解决，
        # 这里暂时不添加文件日志，让日志输出到默认位置（stderr，由 daemon 模式捕获）

        await run_gateway_core()

        console.print("\n🛑 停止服务...")

        heartbeat.stop()
        cron.stop()
        agent.stop()
        await channels.stop_all()

    asyncio.run(main())


# ============================================================================
# Agent Commands
# ============================================================================


@app.command()
def agent(
    message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
    session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"),
    role: str = typer.Option(
        None, "--role", "-r", help="Role to use (e.g., default, coder, tester)"
    ),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (creates isolated environment)"
    ),
):
    """Interact with the agent directly."""
    from echobot.config.loader import load_config
    from echobot.bus.queue import MessageBus
    from echobot.providers.litellm_provider import LiteLLMProvider
    from echobot.agent.loop import AgentLoop

    instance = _resolve_instance_name(instance)
    config = load_config(instance=instance)

    api_key = config.get_api_key()
    api_base = config.get_api_base()

    if not api_key:
        console.print("[red]Error: No API key configured.[/red]")
        raise typer.Exit(1)

    bus = MessageBus()
    provider = LiteLLMProvider(
        api_key=api_key, api_base=api_base, default_model=config.agents.defaults.model
    )

    # 传递 default_role：如果命令行指定了 role_name，则使用它；否则使用配置中的 default_role
    effective_default_role = config.agents.defaults.default_role if not role else None
    agent_loop = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        instance=instance,
        model=config.agents.defaults.model,
        max_iterations=config.agents.defaults.max_tool_iterations,
        brave_api_key=config.tools.web.search.api_key or None,
        role_name=role,
        default_role=effective_default_role,
        # 上下文压缩参数（与 gateway 保持一致）
        context_compression_enabled=config.agents.defaults.context_compression_enabled,
        context_history_max_messages=config.agents.defaults.context_history_max_messages,
        context_keep_recent_messages=config.agents.defaults.context_keep_recent_messages,
        context_summary_max_chars=config.agents.defaults.context_summary_max_chars,
        # 审批与 webhook 参数
        approval_ttl_seconds=config.agents.defaults.approval_ttl_seconds,
        approval_required_tools=config.agents.defaults.approval_required_tools,
        denied_exec_patterns=config.agents.defaults.denied_exec_patterns,
        webhook_secret=config.agents.defaults.webhook_secret or None,
        exec_allowed_dirs=config.agents.defaults.exec_allowed_dirs,
    )

    # 如果指定了角色，显示角色信息
    if role:
        role_info = agent_loop.get_role_info(role)
        if role_info:
            console.print(f"[cyan]Role:[/cyan] {role_info['display_name']}")
            if role_info["description"]:
                console.print(f"[dim]{role_info['description']}[/dim]")
        else:
            console.print(f"[yellow]Warning: Role '{role}' not found, using default[/yellow]")

    if message:
        # Single message mode
        async def run_once():
            import time

            start_time = time.time()
            response = await agent_loop.process_direct(message, session_id)
            elapsed = time.time() - start_time

            # Format elapsed time
            if elapsed < 1:
                time_str = f"{elapsed * 1000:.0f}ms"
            elif elapsed < 60:
                time_str = f"{elapsed:.1f}s"
            else:
                time_str = f"{elapsed / 60:.1f}min"

            console.print(f"\n{__logo__} {response}\n")
            console.print(f"[dim]⏱️  耗时 {time_str}[/dim]")

        asyncio.run(run_once())
    else:
        # Interactive mode
        console.print(f"{__logo__} Interactive mode (Ctrl+C to exit)\n")

        async def run_interactive():
            import time
            import sys

            loading_idx = 0

            while True:
                try:
                    user_input = console.input("[bold blue]You:[/bold blue] ")
                    if not user_input.strip():
                        continue

                    start_time = time.time()

                    # Create a task to show loading animation
                    loading_task = None

                    async def show_loading():
                        nonlocal loading_idx
                        dots = ["   ", ".  ", ".. ", "..."]
                        colors = [
                            "\033[90m",
                            "\033[93m",
                            "\033[93m",
                            "\033[90m",
                        ]  # 灰 -> 黄 -> 黄 -> 灰
                        while True:
                            loading_idx = (loading_idx + 1) % len(dots)
                            sys.stdout.write(
                                f"\r{colors[loading_idx]}思考中{dots[loading_idx]}\033[0m"
                            )
                            sys.stdout.flush()
                            await asyncio.sleep(0.3)

                    # Start loading animation
                    loading_task = asyncio.create_task(show_loading())

                    try:
                        response = await agent_loop.process_direct(user_input, session_id)
                    finally:
                        # Stop loading animation
                        if loading_task:
                            loading_task.cancel()
                            try:
                                await loading_task
                            except asyncio.CancelledError:
                                pass
                        # Clear loading line
                        sys.stdout.write("\r" + " " * 30 + "\r")
                        sys.stdout.flush()

                    elapsed = time.time() - start_time

                    # Format elapsed time
                    if elapsed < 1:
                        time_str = f"{elapsed * 1000:.0f}ms"
                    elif elapsed < 60:
                        time_str = f"{elapsed:.1f}s"
                    else:
                        time_str = f"{elapsed / 60:.1f}min"

                    console.print(f"\n{__logo__} {response}\n")
                    console.print(f"[dim]⏱️  耗时 {time_str}[/dim]")
                except KeyboardInterrupt:
                    console.print("\nGoodbye!")
                    break

        asyncio.run(run_interactive())


# ============================================================================
# Role Commands
# ============================================================================


@app.command()
def roles():
    """List all available roles."""
    from echobot.config.loader import load_config
    from echobot.bus.queue import MessageBus
    from echobot.providers.litellm_provider import LiteLLMProvider
    from echobot.agent.loop import AgentLoop

    instance = _resolve_instance_name()
    config = load_config(instance=instance)

    api_key = config.get_api_key()
    api_base = config.get_api_base()

    if not api_key:
        console.print("[red]Error: No API key configured.[/red]")
        raise typer.Exit(1)

    bus = MessageBus()
    provider = LiteLLMProvider(
        api_key=api_key, api_base=api_base, default_model=config.agents.defaults.model
    )

    agent_loop = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        instance=instance,
        model=config.agents.defaults.model,
        max_iterations=config.agents.defaults.max_tool_iterations,
        context_compression_enabled=config.agents.defaults.context_compression_enabled,
        context_history_max_messages=config.agents.defaults.context_history_max_messages,
        context_keep_recent_messages=config.agents.defaults.context_keep_recent_messages,
        context_summary_max_chars=config.agents.defaults.context_summary_max_chars,
        approval_ttl_seconds=config.agents.defaults.approval_ttl_seconds,
        approval_required_tools=config.agents.defaults.approval_required_tools,
        denied_exec_patterns=config.agents.defaults.denied_exec_patterns,
        webhook_secret=config.agents.defaults.webhook_secret or None,
        exec_allowed_dirs=config.agents.defaults.exec_allowed_dirs,
    )

    roles_list = agent_loop.list_roles()

    if not roles_list:
        console.print("No roles found.")
        return

    table = Table(title="Available Roles")
    table.add_column("Name", style="cyan")
    table.add_column("Display Name", style="green")
    table.add_column("Description")
    table.add_column("Priority")

    for role in roles_list:
        table.add_row(
            role["name"],
            role["display_name"],
            role["description"][:50] + "..."
            if role["description"] and len(role["description"]) > 50
            else role["description"] or "",
            str(role["priority"]),
        )

    console.print(table)
    console.print("\nUse [cyan]--role <name>[/cyan] to select a role")


# ============================================================================
# Channel Commands
# ============================================================================


channels_app = typer.Typer(help="Manage channels")
app.add_typer(channels_app, name="channels")


# ============================================================================
# Cron Commands
# ============================================================================
# Cron Commands
# ============================================================================

cron_app = typer.Typer(help="Manage scheduled tasks")
app.add_typer(cron_app, name="cron")


def _get_cron_store_path(instance: str | None = None) -> Path:
    """
    计算 cron 存储文件路径，保证 Gateway 与 CLI 子命令使用同一规则。

    修复点：
    - 之前 cron 子命令始终读取默认目录，导致 `--instance` 模式下
      网关创建的任务与 CLI 查看/管理的任务不一致。
    - 现在统一通过该函数分流默认实例与命名实例。
    """
    effective_instance = _resolve_instance_name(instance)
    if effective_instance:
        return Path.home() / ".echobot" / "instances" / effective_instance / "cron" / "jobs.json"

    return Path.home() / ".echobot" / "cron" / "jobs.json"


@cron_app.command("list")
def cron_list(
    all: bool = typer.Option(False, "--all", "-a", help="Include disabled jobs"),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (isolated cron store)"
    ),
):
    """List scheduled jobs."""
    from echobot.cron.service import CronService

    store_path = _get_cron_store_path(instance)
    service = CronService(store_path)

    jobs = service.list_jobs(include_disabled=all)

    if not jobs:
        console.print("No scheduled jobs.")
        return

    table = Table(title="Scheduled Jobs")
    table.add_column("ID", style="cyan")
    table.add_column("Name")
    table.add_column("Schedule")
    table.add_column("Status")
    table.add_column("Next Run")

    import time

    for job in jobs:
        # Format schedule
        if job.schedule.kind == "every":
            sched = f"every {(job.schedule.every_ms or 0) // 1000}s"
        elif job.schedule.kind == "cron":
            sched = job.schedule.expr or ""
        else:
            sched = "one-time"

        # Format next run
        next_run = ""
        if job.state.next_run_at_ms:
            next_time = time.strftime(
                "%Y-%m-%d %H:%M", time.localtime(job.state.next_run_at_ms / 1000)
            )
            next_run = next_time

        status = "[green]enabled[/green]" if job.enabled else "[dim]disabled[/dim]"

        table.add_row(job.id, job.name, sched, status, next_run)

    console.print(table)


@cron_app.command("add")
def cron_add(
    name: str = typer.Option(..., "--name", "-n", help="Job name"),
    message: str = typer.Option(..., "--message", "-m", help="Message for agent"),
    every: int = typer.Option(None, "--every", "-e", help="Run every N seconds"),
    cron_expr: str = typer.Option(None, "--cron", "-c", help="Cron expression (e.g. '0 9 * * *')"),
    at: str = typer.Option(None, "--at", help="Run once at time (ISO format)"),
    deliver: bool = typer.Option(False, "--deliver", "-d", help="Deliver response to channel"),
    to: str = typer.Option(None, "--to", help="Recipient for delivery"),
    channel: str = typer.Option(
        None, "--channel", help="Channel for delivery (e.g. 'telegram', 'whatsapp')"
    ),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (isolated cron store)"
    ),
):
    """Add a scheduled job."""
    from echobot.cron.service import CronService
    from echobot.cron.types import CronSchedule

    # Determine schedule type
    if every:
        schedule = CronSchedule(kind="every", every_ms=every * 1000)
    elif cron_expr:
        schedule = CronSchedule(kind="cron", expr=cron_expr)
    elif at:
        import datetime

        dt = datetime.datetime.fromisoformat(at)
        schedule = CronSchedule(kind="at", at_ms=int(dt.timestamp() * 1000))
    else:
        console.print("[red]Error: Must specify --every, --cron, or --at[/red]")
        raise typer.Exit(1)

    import asyncio
    store_path = _get_cron_store_path(instance)
    service = CronService(store_path)

    job = asyncio.run(service.add_job(
        name=name,
        schedule=schedule,
        message=message,
        deliver=deliver,
        to=to,
        channel=channel,
    ))

    console.print(f"[green]✓[/green] Added job '{job.name}' ({job.id})")


@cron_app.command("remove")
def cron_remove(
    job_id: str = typer.Argument(..., help="Job ID to remove"),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (isolated cron store)"
    ),
):
    """Remove a scheduled job."""
    from echobot.cron.service import CronService

    import asyncio
    store_path = _get_cron_store_path(instance)
    service = CronService(store_path)

    if asyncio.run(service.remove_job(job_id)):
        console.print(f"[green]✓[/green] Removed job {job_id}")
    else:
        console.print(f"[red]Job {job_id} not found[/red]")


@cron_app.command("enable")
def cron_enable(
    job_id: str = typer.Argument(..., help="Job ID"),
    disable: bool = typer.Option(False, "--disable", help="Disable instead of enable"),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (isolated cron store)"
    ),
):
    """Enable or disable a job."""
    import asyncio
    from echobot.cron.service import CronService

    store_path = _get_cron_store_path(instance)
    service = CronService(store_path)

    job = asyncio.run(service.enable_job(job_id, enabled=not disable))
    if job:
        status = "disabled" if disable else "enabled"
        console.print(f"[green]✓[/green] Job '{job.name}' {status}")
    else:
        console.print(f"[red]Job {job_id} not found[/red]")


@cron_app.command("run")
def cron_run(
    job_id: str = typer.Argument(..., help="Job ID to run"),
    force: bool = typer.Option(False, "--force", "-f", help="Run even if disabled"),
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use (isolated cron store)"
    ),
):
    """Manually run a job."""
    from echobot.cron.service import CronService

    store_path = _get_cron_store_path(instance)
    service = CronService(store_path)

    async def run():
        return await service.run_job(job_id, force=force)

    if asyncio.run(run()):
        console.print(f"[green]✓[/green] Job executed")
    else:
        console.print(f"[red]Failed to run job {job_id}[/red]")


# ============================================================================
# Onboard Commands
# ============================================================================


@app.command()
def onboard(
    instance: str = typer.Option(
        None, "--instance", "-i", help="Instance name to use"
    ),
    force: bool = typer.Option(
        False, "--force", "-f", help="Force reinitialize (overwrite existing config)"
    ),
):
    """Interactive configuration wizard (init + config)."""
    from echobot.config.loader import load_config, get_config_path, save_config
    from echobot.config.schema import Config
    from echobot.utils.helpers import get_workspace_path
    from echobot.utils.instance import ensure_instance_structure

    config_path = get_config_path(instance)
    instance_str = f" (instance: {instance})" if instance else ""

    # 检查是否需要初始化
    need_init = not config_path.exists() or force

    if need_init:
        # 创建实例目录结构
        if instance:
            ensure_instance_structure(instance)
            console.print(f"[green]✓[/green] Created instance structure for '{instance}'")

        # 创建默认配置
        config = Config()
        save_config(config, instance=instance)
        console.print(f"[green]✓[/green] Created default config at {config_path}")

        # 创建工作空间
        workspace = get_workspace_path(instance=instance)
        workspace.mkdir(parents=True, exist_ok=True)
        console.print(f"[green]✓[/green] Created workspace at {workspace}")

        console.print("\n[green]✓[/green] Init completed!\n")

    console.print(f"\n[bold cyan]🔧 echobot Config Wizard[/bold cyan]{instance_str}\n")
    console.print(f"[dim]Config file: {config_path}[/dim]\n")

    # 加载现有配置
    config = load_config(instance=instance)

    # 跟踪用户修改的字段
    modified = {
        "model": False,
        "providers": set(),
        "telegram": False,
    }

    # ===== 模型配置 =====
    console.print("[bold]1. Model Config[/bold]")
    model = config.agents.defaults.model
    console.print(f"   Current model: {model}")
    new_model = console.input("   Enter new model (skip with enter): ").strip()
    if new_model:
        config.agents.defaults.model = new_model
        modified["model"] = True

    # ===== API 提供商配置 =====
    console.print("\n[bold]2. Provider Config[/bold]")
    console.print("   Available: minimax, openai, anthropic, deepseek, zhipu, etc.")
    provider = console.input("   Select provider to config (skip with enter): ").strip()

    if provider:
        if provider not in config.providers.model_fields:
            console.print(f"[yellow]   Unknown provider {provider}, skipped[/yellow]")
        else:
            modified["providers"].add(provider)
            provider_config = getattr(config.providers, provider)
            api_key = getattr(provider_config, 'apiKey', None) or getattr(provider_config, 'api_key', '') or ""
            console.print(f"   Current API Key: {'*' * 8 if api_key else '(not set)'}")
            new_key = console.input("   Enter new API Key (skip with enter): ").strip()
            if new_key:
                if hasattr(provider_config, 'apiKey'):
                    provider_config.apiKey = new_key
                else:
                    provider_config.api_key = new_key

            api_base = getattr(provider_config, 'apiBase', None) or getattr(provider_config, 'api_base', '') or ""
            console.print(f"   Current API Base: {api_base or '(default)'}")
            new_base = console.input("   Enter new API Base (skip with enter): ").strip()
            if new_base:
                if hasattr(provider_config, 'apiBase'):
                    provider_config.apiBase = new_base
                else:
                    provider_config.api_base = new_base
    else:
        console.print("   [dim]Skipped provider config[/dim]")

    # ===== Telegram 配置 =====
    console.print("\n[bold]3. Telegram Config (optional)[/bold]")
    console.print("   Skip if you only use CLI.")
    console.print("   To get Bot Token: @BotFather on Telegram")
    tg_enabled = config.channels.telegram.enabled
    console.print(f"   Current enabled: {'Yes' if tg_enabled else 'No'}")
    enable_tg = console.input("   Enable Telegram? [y/N] (y=yes, n=no): ").strip().lower()
    if enable_tg == 'y':
        modified["telegram"] = True
        config.channels.telegram.enabled = True
        tg_token = config.channels.telegram.token
        console.print(f"   Current Token: {'*' * 8 if tg_token else '(not set)'}")
        console.print("   Get token from @BotFather -> /newbot")
        new_token = console.input("   Enter Bot Token (skip with enter): ").strip()
        if new_token:
            config.channels.telegram.token = new_token

        try:
            tg_allowed_list = list(config.channels.telegram.allow_from or [])
        except (TypeError, AttributeError):
            tg_allowed_list = []
        tg_allowed = ",".join(tg_allowed_list) if tg_allowed_list else ""
        console.print(f"   Allowed users: {tg_allowed or '(all) - enter your Telegram user ID'}")
        console.print("   Get your ID: @userinfobot on Telegram")
        new_allowed = console.input("   Enter allowed user IDs (comma separated, skip with enter): ").strip()
        if new_allowed:
            config.channels.telegram.allow_from = [u.strip() for u in new_allowed.split(",") if u.strip()]
    else:
        config.channels.telegram.enabled = False
        modified["telegram"] = True

    # ===== 保存配置 =====
    console.print("\n[bold]4. Save Config[/bold]")
    confirm = console.input("   Save config? [Y/n] (Y=yes, n=no): ").strip().lower()
    if confirm != 'n':
        # 清理未使用的 provider
        # 将没有配置过的 provider 设为空
        for name in config.providers.model_fields:
            if name not in modified["providers"]:
                provider = getattr(config.providers, name)
                provider.api_key = ""
                provider.api_base = None
                provider.extra_headers = None

        save_config(config, instance=instance)
        console.print(f"\n[green]✓ Config saved to {config_path}[/green]")
    else:
        console.print("\n[yellow]Config save cancelled[/yellow]")


# ============================================================================
# Status Commands
# ============================================================================


@app.command()
def status():
    """Show echobot status."""
    from echobot.config.loader import load_config, get_config_path
    from echobot.utils.helpers import get_workspace_path

    config_path = get_config_path()
    workspace = get_workspace_path()

    console.print(f"{__logo__} echobot Status\n")

    console.print(
        f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}"
    )
    console.print(
        f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}"
    )

    if config_path.exists():
        config = load_config()
        console.print(f"Model: {config.agents.defaults.model}")

        # Check API keys
        has_openrouter = bool(config.providers.openrouter.api_key)
        has_anthropic = bool(config.providers.anthropic.api_key)
        has_openai = bool(config.providers.openai.api_key)

        console.print(
            f"OpenRouter API: {'[green]✓[/green]' if has_openrouter else '[dim]not set[/dim]'}"
        )
        console.print(
            f"Anthropic API: {'[green]✓[/green]' if has_anthropic else '[dim]not set[/dim]'}"
        )
        console.print(f"OpenAI API: {'[green]✓[/green]' if has_openai else '[dim]not set[/dim]'}")


# ============================================================================
# Instance Commands
# ============================================================================

instance_app = typer.Typer(help="Manage echobot instances")
app.add_typer(instance_app, name="instance")


@instance_app.command("create")
def instance_create(
    name: str = typer.Argument(..., help="Instance name"),
):
    """Create a new echobot instance."""
    from echobot.utils.instance import ensure_instance_structure, instance_exists

    if instance_exists(name):
        console.print(f"[red]Error: Instance '{name}' already exists.[/red]")
        raise typer.Exit(1)

    instance_dir = ensure_instance_structure(name)
    console.print(f"[green]✓[/green] Created instance '{name}' at {instance_dir}")


@instance_app.command("list")
def instance_list():
    """List all echobot instances."""
    from echobot.utils.instance import list_instances

    instances = list_instances()

    if not instances:
        console.print("No instances found. Create one with: echobot instance create <name>")
        return

    table = Table(title="Echobot Instances")
    table.add_column("Name", style="cyan")
    table.add_column("Created", style="green")
    table.add_column("Path")

    for inst in instances:
        table.add_row(
            inst["name"],
            inst["created_at"][:19],  # Just the date-time part
            str(inst["path"]),
        )

    console.print(table)


@instance_app.command("delete")
def instance_delete(
    name: str = typer.Argument(..., help="Instance name to delete"),
    force: bool = typer.Option(False, "--force", "-f", help="Force delete without confirmation"),
):
    """Delete an echobot instance."""
    from echobot.utils.instance import delete_instance, instance_exists

    if not instance_exists(name):
        console.print(f"[red]Error: Instance '{name}' does not exist.[/red]")
        raise typer.Exit(1)

    if not force:
        if not typer.confirm(
            f"Are you sure you want to delete instance '{name}'? This cannot be undone."
        ):
            console.print("Cancelled.")
            raise typer.Exit()

    delete_instance(name)
    console.print(f"[green]✓[/green] Deleted instance '{name}'")


@instance_app.command("copy")
def instance_copy(
    source: str = typer.Argument(..., help="Source instance name"),
    target: str = typer.Argument(..., help="Target instance name"),
):
    """Copy an echobot instance."""
    from echobot.utils.instance import copy_instance

    try:
        copy_instance(source, target)
        console.print(f"[green]✓[/green] Copied instance '{source}' to '{target}'")
    except ValueError as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1)


# ============================================================================
# Agents Commands
# ============================================================================

agents_app = typer.Typer(help="Manage multi-agent configuration")
app.add_typer(agents_app, name="agents")


@agents_app.command("init")
def agents_init(
    force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing configuration"),
):
    """Interactive wizard to create agents.json configuration."""
    from echobot.config.loader import get_agents_config_path, load_config, save_agents_config
    from echobot.config.schema import MultiAgentsConfig, AgentInstanceConfig, AgentBinding, AgentBindingMatch, CollaborationConfig
    from echobot.roles import RoleManager

    config_path = get_agents_config_path()

    # Check if file exists
    if config_path.exists() and not force:
        console.print(f"[yellow]agents.json already exists at {config_path}[/yellow]")
        console.print("Use --force to overwrite")
        raise typer.Exit(1)

    console.print(f"\n[bold cyan]🤖 Multi-Agent Configuration Wizard[/bold cyan]\n")

    agents_list: list[AgentInstanceConfig] = []
    bindings_list: list[AgentBinding] = []

    runtime_config = load_config()
    default_channel = (
        "dingtalk"
        if runtime_config.channels.dingtalk.enabled
        and runtime_config.channels.dingtalk.client_id
        and runtime_config.channels.dingtalk.client_secret
        else "telegram"
    )
    route_channel = console.input(f"Default routing channel [{default_channel}]: ").strip().lower()
    if not route_channel:
        route_channel = default_channel
    if route_channel not in {"telegram", "dingtalk"}:
        console.print(f"[yellow]Unsupported channel '{route_channel}', fallback to {default_channel}[/yellow]")
        route_channel = default_channel

    # Step 1: Add agents
    console.print("[bold]Step 1: Add Agents[/bold]")
    console.print("How many agents do you want to create? (1-5)")

    while True:
        num_str = console.input("  Number of agents: ").strip()
        try:
            num = int(num_str)
            if 1 <= num <= 5:
                break
            console.print("  [red]Please enter a number between 1 and 5[/red]")
        except ValueError:
            console.print("  [red]Please enter a valid number[/red]")

    # 动态读取角色，避免新增角色后向导仍显示旧固定列表。
    available_roles = [role["name"] for role in RoleManager().list_roles() if role and role.get("name")]
    if not available_roles:
        available_roles = ["default"]
    if "default" in available_roles:
        available_roles = ["default"] + [r for r in available_roles if r != "default"]

    for i in range(num):
        console.print(f"\n[bold]Agent {i + 1}:[/bold]")

        # Agent ID
        agent_id = console.input("  Agent ID (e.g., coder, reviewer): ").strip()
        while not agent_id:
            console.print("  [red]Agent ID cannot be empty[/red]")
            agent_id = console.input("  Agent ID: ").strip()

        # Name
        name = console.input(f"  Display name [{agent_id.title()}]: ").strip()
        if not name:
            name = agent_id.title()

        # Token
        token = ""
        if route_channel == "telegram":
            console.print("  Get token from @BotFather on Telegram")
            token = console.input("  Bot Token: ").strip()
            while not token:
                console.print("  [red]Bot Token is required for telegram channel[/red]")
                token = console.input("  Bot Token: ").strip()
        else:
            token = console.input("  Telegram Bot Token (optional, for fallback): ").strip()

        # Role
        console.print(f"  Available roles: {', '.join(available_roles)}")
        role = console.input(f"  Role [default]: ").strip()
        if not role:
            role = "default"

        # Workspace
        default_workspace = f"~/.echobot/agents/{agent_id}"
        workspace = console.input(f"  Workspace [{default_workspace}]: ").strip()
        if not workspace:
            workspace = default_workspace

        # Enabled
        enabled_str = console.input("  Enable this agent? [Y/n]: ").strip().lower()
        enabled = enabled_str != "n"

        agent_config = AgentInstanceConfig(
            id=agent_id,
            name=name,
            token=token,
            role=role,
            workspace=workspace,
            enabled=enabled,
        )
        agents_list.append(agent_config)

    # Step 2: Collaboration
    console.print("\n[bold]Step 2: Collaboration Settings[/bold]")
    collab_enabled_str = console.input("  Enable agent collaboration? [Y/n]: ").strip().lower()
    collab_enabled = collab_enabled_str != "n"

    allow_calls = []
    if collab_enabled:
        console.print(f"  Available agents: {', '.join(a.id for a in agents_list)}")
        calls_input = console.input("  Agents that can be called (comma-separated): ").strip()
        if calls_input:
            allow_calls = [c.strip() for c in calls_input.split(",") if c.strip()]

    collaboration = CollaborationConfig(
        enabled=collab_enabled,
        allow_calls=allow_calls,
    )

    # Step 3: Bindings
    console.print("\n[bold]Step 3: Message Bindings[/bold]")
    console.print("  Routing rules for messages (private chat, @mention, etc.)")

    # Add default bindings for each agent
    for agent in agents_list:
        if agent.enabled:
            # Private chat binding
            binding = AgentBinding(
                agent_id=agent.id,
                match=AgentBindingMatch(
                    channel=route_channel,
                    chat_type="private",
                    mention=None,
                ),
            )
            bindings_list.append(binding)
            console.print(f"  [green]✓[/green] {agent.id}: private chat")

    # Ask about group chat bindings
    add_group = console.input("  Add group chat @mention bindings? [y/N]: ").strip().lower()
    if add_group == "y":
        for agent in agents_list:
            if agent.enabled:
                mention_input = console.input(f"  @mention for {agent.id} [{agent.id}]: ").strip()
                mention = mention_input or agent.id

                binding = AgentBinding(
                    agent_id=agent.id,
                    match=AgentBindingMatch(
                        channel=route_channel,
                        chat_type="group",
                        mention=mention,
                    ),
                )
                bindings_list.append(binding)
                console.print(f"  [green]✓[/green] {agent.id}: @{mention}")

    # Create config
    config = MultiAgentsConfig(
        agents=agents_list,
        collaboration=collaboration,
        bindings=bindings_list,
    )

    # Save
    config_path.parent.mkdir(parents=True, exist_ok=True)
    save_agents_config(config, config_path)

    console.print(f"\n[green]✓[/green] Configuration saved to {config_path}")
    console.print("\n[bold cyan]Next steps:[/bold cyan]")
    console.print("  1. Review the config: cat ~/.echobot/agents.json")
    console.print("  2. Start multi-agent mode: echobot gateway --multi")


@agents_app.command("list")
def agents_list():
    """List configured agents."""
    from echobot.config.loader import load_agents_config, get_agents_config_path

    config_path = get_agents_config_path()

    if not config_path.exists():
        console.print("[yellow]No agents.json found.[/yellow]")
        console.print("Create one with: echobot agents init")
        raise typer.Exit(1)

    config = load_agents_config()

    if not config.agents:
        console.print("No agents configured.")
        return

    table = Table(title="Configured Agents")
    table.add_column("ID", style="cyan")
    table.add_column("Name", style="green")
    table.add_column("Role", style="yellow")
    table.add_column("Enabled", style="blue")
    table.add_column("Workspace")

    for agent in config.agents:
        enabled_str = "[green]✓[/green]" if agent.enabled else "[dim]✗[/dim]"
        table.add_row(
            agent.id,
            agent.name,
            agent.role,
            enabled_str,
            agent.workspace,
        )

    console.print(table)

    # Show collaboration status
    if config.collaboration.enabled:
        console.print(f"\n[green]✓[/green] Collaboration enabled")
        console.print(f"[cyan]Allowed calls:[/cyan] {', '.join(config.collaboration.allow_calls) or 'none'}")
    else:
        console.print("\n[dim]Collaboration disabled[/dim]")

    # Show bindings
    if config.bindings:
        console.print("\n[bold]Bindings:[/bold]")
        for binding in config.bindings:
            match = binding.match
            mention_str = f" @{match.mention}" if match.mention else ""
            console.print(f"  {binding.agent_id}: {match.channel}:{match.chat_type}{mention_str}")


@agents_app.command("add")
def agents_add(
    agent_id: str = typer.Option(..., "--id", "-i", help="Agent ID"),
    name: str = typer.Option(None, "--name", "-n", help="Display name"),
    token: str = typer.Option("", "--token", "-t", help="Telegram Bot Token (required for telegram channel)"),
    role: str = typer.Option("default", "--role", "-r", help="Role name"),
    workspace: str = typer.Option(None, "--workspace", "-w", help="Workspace path"),
    channel: str = typer.Option("telegram", "--channel", "-c", help="Binding channel (telegram/dingtalk)"),
):
    """Add a new agent to agents.json."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config
    from echobot.config.schema import AgentInstanceConfig, AgentBinding, AgentBindingMatch

    config_path = get_agents_config_path()

    if not config_path.exists():
        console.print("[red]agents.json not found.[/red]")
        console.print("Create one first: echobot agents init")
        raise typer.Exit(1)

    config = load_agents_config()

    # Check if agent already exists
    for agent in config.agents:
        if agent.id == agent_id:
            console.print(f"[red]Agent '{agent_id}' already exists.[/red]")
            raise typer.Exit(1)

    # Use defaults
    if not name:
        name = agent_id.title()
    if not workspace:
        workspace = f"~/.echobot/agents/{agent_id}"

    if channel not in {"telegram", "dingtalk"}:
        console.print(f"[red]Unsupported channel '{channel}'. Use telegram or dingtalk.[/red]")
        raise typer.Exit(1)
    if channel == "telegram" and not token:
        console.print("[red]--token is required when channel=telegram[/red]")
        raise typer.Exit(1)

    # Add agent
    new_agent = AgentInstanceConfig(
        id=agent_id,
        name=name,
        token=token,
        role=role,
        workspace=workspace,
        enabled=True,
    )
    config.agents.append(new_agent)

    # Add default bindings
    config.bindings.append(AgentBinding(
        agent_id=agent_id,
        match=AgentBindingMatch(channel=channel, chat_type="private"),
    ))

    save_agents_config(config, config_path)
    console.print(f"[green]✓[/green] Added agent '{agent_id}'")
    console.print(f"\nTo enable @mention in groups, run:")
    console.print(f"  echobot agents bind {agent_id} --channel {channel} <mention>")


@agents_app.command("bind")
def agents_bind(
    agent_id: str = typer.Argument(..., help="Agent ID"),
    mention: str = typer.Option(None, "--mention", "-m", help="@mention keyword for routing (optional)"),
    chat_type: str = typer.Option("group", "--chat-type", "-c", help="Chat type (private/group)"),
    channel: str = typer.Option("telegram", "--channel", help="Binding channel (telegram/dingtalk)"),
    chat_id: str = typer.Option(None, "--chat-id", help="Exact chat/conversation ID for direct routing"),
):
    """Add binding rule for an agent."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config
    from echobot.config.schema import AgentBinding, AgentBindingMatch

    config_path = get_agents_config_path()

    if not config_path.exists():
        console.print("[red]agents.json not found.[/red]")
        raise typer.Exit(1)

    config = load_agents_config()

    # Find agent
    agent = None
    for a in config.agents:
        if a.id == agent_id:
            agent = a
            break

    if not agent:
        console.print(f"[red]Agent '{agent_id}' not found.[/red]")
        raise typer.Exit(1)

    if not mention and not chat_id and chat_type in {"group", "supergroup"}:
        mention = agent_id
    if channel not in {"telegram", "dingtalk"}:
        console.print(f"[red]Unsupported channel '{channel}'. Use telegram or dingtalk.[/red]")
        raise typer.Exit(1)

    # Add binding
    binding = AgentBinding(
        agent_id=agent_id,
        match=AgentBindingMatch(
            channel=channel,
            chat_type=chat_type,
            chat_id=chat_id,
            mention=mention,
        ),
    )
    config.bindings.append(binding)

    save_agents_config(config, config_path)
    match_parts = [f"{channel}:{chat_type}"]
    if chat_id:
        match_parts.append(f"chat_id={chat_id}")
    if mention:
        match_parts.append(f"@{mention}")
    console.print(f"[green]✓[/green] Added binding: {agent_id} -> {', '.join(match_parts)}")


@agents_app.command("enable")
def agents_enable(
    agent_id: str = typer.Argument(..., help="Agent ID to enable"),
):
    """Enable an agent."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config

    config = load_agents_config(get_agents_config_path())

    for agent in config.agents:
        if agent.id == agent_id:
            agent.enabled = True
            save_agents_config(config)
            console.print(f"[green]✓[/green] Enabled agent '{agent_id}'")
            return

    console.print(f"[red]Agent '{agent_id}' not found.[/red]")
    raise typer.Exit(1)


@agents_app.command("disable")
def agents_disable(
    agent_id: str = typer.Argument(..., help="Agent ID to disable"),
):
    """Disable an agent."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config

    config = load_agents_config(get_agents_config_path())

    for agent in config.agents:
        if agent.id == agent_id:
            agent.enabled = False
            save_agents_config(config)
            console.print(f"[green]✓[/green] Disabled agent '{agent_id}'")
            return

    console.print(f"[red]Agent '{agent_id}' not found.[/red]")
    raise typer.Exit(1)


@agents_app.command("remove")
def agents_remove(
    agent_id: str = typer.Argument(..., help="Agent ID to remove"),
):
    """Remove an agent from configuration."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config

    config = load_agents_config(get_agents_config_path())

    original_count = len(config.agents)
    config.agents = [a for a in config.agents if a.id != agent_id]
    config.bindings = [b for b in config.bindings if b.agent_id != agent_id]

    if len(config.agents) == original_count:
        console.print(f"[red]Agent '{agent_id}' not found.[/red]")
        raise typer.Exit(1)

    save_agents_config(config)
    console.print(f"[green]✓[/green] Removed agent '{agent_id}' and its bindings")


@agents_app.command("collaboration")
def agents_collaboration(
    enable: bool = typer.Option(True, "--enable/--disable", help="Enable or disable collaboration"),
    add_calls: str = typer.Option(None, "--add", help="Add agents to allowed calls (comma-separated)"),
    remove_calls: str = typer.Option(None, "--remove", help="Remove agents from allowed calls"),
):
    """Manage collaboration settings."""
    from echobot.config.loader import load_agents_config, get_agents_config_path, save_agents_config

    config = load_agents_config(get_agents_config_path())

    if enable:
        config.collaboration.enabled = True

    if add_calls:
        for agent_id in [a.strip() for a in add_calls.split(",")]:
            if agent_id and agent_id not in config.collaboration.allow_calls:
                config.collaboration.allow_calls.append(agent_id)

    if remove_calls:
        for agent_id in [a.strip() for a in remove_calls.split(",")]:
            if agent_id in config.collaboration.allow_calls:
                config.collaboration.allow_calls.remove(agent_id)

    if not enable:
        config.collaboration.enabled = False

    save_agents_config(config)

    status = "[green]enabled[/green]" if config.collaboration.enabled else "[dim]disabled[/dim]"
    console.print(f"[green]✓[/green] Collaboration {status}")
    if config.collaboration.allow_calls:
        console.print(f"[cyan]Allowed calls:[/cyan] {', '.join(config.collaboration.allow_calls)}")


if __name__ == "__main__":
    app()
