#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
import tempfile
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

try:
    from .fleet_interactive import (
        interactive_add_bot as _interactive_add_bot,
        interactive_manifest_setup as _interactive_manifest_setup,
    )
    from .fleet_manifest import (
        DEFAULT_MANIFEST_PATH,
        _bot_seed_is_placeholder,
        _child_process_env,
        _dump_json,
        _is_placeholder_text,
        _load_json,
        _normalize_context_settings,
        _required_text,
        _scaffold_manifest,
        _scan_placeholder_fields,
        _validate_webhook_ports,
        prepare_from_manifest,
    )
    from .fleet_process import (
        fleet_web_status_payload as _proc_fleet_web_status_payload,
        is_pid_alive as _proc_is_pid_alive,
        resolve_bot_from_manifest as _proc_resolve_bot_from_manifest,
        _safe_load_json as _proc_safe_load_json,
        start_all_bots_from_manifest as _proc_start_all_bots_from_manifest,
        start_fleet_web as _proc_start_fleet_web,
        start_single_bot_from_manifest as _proc_start_single_bot_from_manifest,
        status_all_bots_from_manifest as _proc_status_all_bots_from_manifest,
        status_single_bot_from_manifest as _proc_status_single_bot_from_manifest,
        stop_all_bots_from_manifest as _proc_stop_all_bots_from_manifest,
        stop_all_fallback_without_manifest as _proc_stop_all_fallback_without_manifest,
        stop_fleet_web as _proc_stop_fleet_web,
        stop_fleet_web_fallback as _proc_stop_fleet_web_fallback,
        stop_single_bot_from_manifest as _proc_stop_single_bot_from_manifest,
    )
    from .fleet_output import (
        format_agent_system_payload,
        format_emergency_stop_payload,
        format_list_bots_payload,
        format_prepare_payload,
        format_restart_payload,
        format_start_stop_payload,
        format_status_payload,
        format_web_payload,
        print_json,
    )
except ImportError:
    from fleet_interactive import (
        interactive_add_bot as _interactive_add_bot,
        interactive_manifest_setup as _interactive_manifest_setup,
    )
    from fleet_manifest import (
        DEFAULT_MANIFEST_PATH,
        _bot_seed_is_placeholder,
        _child_process_env,
        _dump_json,
        _is_placeholder_text,
        _load_json,
        _normalize_context_settings,
        _required_text,
        _scaffold_manifest,
        _scan_placeholder_fields,
        _validate_webhook_ports,
        prepare_from_manifest,
    )
    from fleet_process import (
        fleet_web_status_payload as _proc_fleet_web_status_payload,
        is_pid_alive as _proc_is_pid_alive,
        resolve_bot_from_manifest as _proc_resolve_bot_from_manifest,
        _safe_load_json as _proc_safe_load_json,
        start_all_bots_from_manifest as _proc_start_all_bots_from_manifest,
        start_fleet_web as _proc_start_fleet_web,
        start_single_bot_from_manifest as _proc_start_single_bot_from_manifest,
        status_all_bots_from_manifest as _proc_status_all_bots_from_manifest,
        status_single_bot_from_manifest as _proc_status_single_bot_from_manifest,
        stop_all_bots_from_manifest as _proc_stop_all_bots_from_manifest,
        stop_all_fallback_without_manifest as _proc_stop_all_fallback_without_manifest,
        stop_fleet_web as _proc_stop_fleet_web,
        stop_fleet_web_fallback as _proc_stop_fleet_web_fallback,
        stop_single_bot_from_manifest as _proc_stop_single_bot_from_manifest,
    )
    from fleet_output import (
        format_agent_system_payload,
        format_emergency_stop_payload,
        format_list_bots_payload,
        format_prepare_payload,
        format_restart_payload,
        format_start_stop_payload,
        format_status_payload,
        format_web_payload,
        print_json,
    )

REPO_ROOT = Path(__file__).resolve().parents[1]
SERVICE_PATH = REPO_ROOT / "cheapclaw_service.py"
FLEET_WEB_CONSOLE_PATH = REPO_ROOT / "scripts" / "fleet_web_console.py"
AGENT_LIBRARY_ROOT = REPO_ROOT / "assets" / "agent_library"

# Keep these legacy private names stable because the web console and older
# internal callers still import them from fleet_one_click during upgrades.
_safe_load_json = _proc_safe_load_json
_is_pid_alive = _proc_is_pid_alive
_status_single_bot_from_manifest = _proc_status_single_bot_from_manifest
_status_all_bots_from_manifest = _proc_status_all_bots_from_manifest
_start_single_bot_from_manifest = _proc_start_single_bot_from_manifest
_start_all_bots_from_manifest = _proc_start_all_bots_from_manifest
_stop_single_bot_from_manifest = _proc_stop_single_bot_from_manifest
_stop_all_bots_from_manifest = _proc_stop_all_bots_from_manifest

def _run(cmd: List[str], env: Dict[str, str], check: bool = True) -> int:
    print("[RUN]", " ".join(cmd))
    proc = subprocess.run(cmd, env=env)
    if check and proc.returncode != 0:
        raise SystemExit(proc.returncode)
    return int(proc.returncode)


def _run_capture(cmd: List[str], env: Dict[str, str], check: bool = True) -> subprocess.CompletedProcess:
    proc = subprocess.run(cmd, env=env, capture_output=True, text=True)
    if check and proc.returncode != 0:
        stderr = (proc.stderr or "").strip()
        stdout = (proc.stdout or "").strip()
        detail = stderr or stdout or f"exit code={proc.returncode}"
        raise SystemExit(f"[ERROR] command failed: {' '.join(cmd)}\n{detail}")
    return proc


def _print_log_tail(path: Path, lines: int = 200) -> None:
    if not path.exists():
        print(f"[WARN] log file not found: {path}")
        return
    total = max(1, int(lines))
    try:
        content = path.read_text(encoding="utf-8", errors="ignore").splitlines()
    except Exception as exc:
        raise SystemExit(f"[ERROR] failed to read log: {path} ({exc})")
    print(f"[LOG] {path} (last {total} lines)")
    for line in content[-total:]:
        print(line)


def _install_agent_system_archive(archive_path: Path, destination_root: Path) -> Dict[str, Any]:
    archive = archive_path.expanduser().resolve()
    destination = destination_root.expanduser().resolve()
    if not archive.exists():
        raise SystemExit(f"[ERROR] archive not found: {archive}")
    if archive.suffix.lower() != ".zip":
        raise SystemExit(f"[ERROR] archive must be a .zip file: {archive}")

    with tempfile.TemporaryDirectory(prefix="cheapclaw-agent-system-") as tmpdir:
        tmp_root = Path(tmpdir)
        try:
            with zipfile.ZipFile(archive, "r") as zf:
                zf.extractall(tmp_root)
        except zipfile.BadZipFile as exc:
            raise SystemExit(f"[ERROR] invalid zip archive: {archive} ({exc})")

        entries = [item for item in tmp_root.iterdir() if item.name != "__MACOSX"]
        if len(entries) == 1 and entries[0].is_dir():
            source_root = entries[0]
        else:
            source_root = tmp_root

        level_3 = source_root / "level_3_agents.yaml"
        if not level_3.exists():
            children = [item for item in source_root.iterdir() if item.is_dir()]
            if len(children) == 1 and (children[0] / "level_3_agents.yaml").exists():
                source_root = children[0]
                level_3 = source_root / "level_3_agents.yaml"
        if not level_3.exists():
            raise SystemExit(f"[ERROR] agent system archive missing level_3_agents.yaml: {archive}")

        system_name = source_root.name
        target_dir = destination / system_name
        destination.mkdir(parents=True, exist_ok=True)
        if target_dir.exists():
            shutil.rmtree(target_dir)
        shutil.copytree(source_root, target_dir)

        periodic_variant = ""
        if destination.name == "agent_library" and system_name != "CheapClawSupervisor" and not system_name.endswith("_tmp"):
            previous_root = os.environ.get("MLA_USER_DATA_ROOT", "")
            try:
                os.environ["MLA_USER_DATA_ROOT"] = str(destination.parent)
                try:
                    from .tool_runtime_helpers import ensure_periodic_agent_overlay
                except ImportError:
                    from tool_runtime_helpers import ensure_periodic_agent_overlay
                overlay = ensure_periodic_agent_overlay(str(destination.parent / "cheapclaw" / "supervisor_task"), agent_system=system_name, agent_name="")
                if overlay.get("status") == "success":
                    periodic_variant = str(overlay.get("periodic_agent_system") or "")
            finally:
                if previous_root:
                    os.environ["MLA_USER_DATA_ROOT"] = previous_root
                else:
                    os.environ.pop("MLA_USER_DATA_ROOT", None)

        file_count = sum(1 for item in target_dir.rglob("*") if item.is_file())
        payload = {
            "status": "success",
            "name": system_name,
            "archive": str(archive),
            "target": str(target_dir),
            "files": file_count,
        }
        if periodic_variant:
            payload["periodic_variant"] = periodic_variant
        return payload


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="CheapClaw one-click fleet launcher")
    sub = parser.add_subparsers(dest="command", required=True)

    def _add_json_flag(p: argparse.ArgumentParser) -> None:
        p.add_argument("--json", action="store_true", help="Print machine-readable JSON output")

    def _add_restore_defaults_flag(p: argparse.ArgumentParser) -> None:
        p.add_argument(
            "--restore-package-defaults",
            action="store_true",
            help="Overwrite built-in runtime agent systems from current package assets during prepare/bootstrap",
        )

    p_init = sub.add_parser("init")
    p_init.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_init.add_argument("--force", action="store_true", help="Overwrite manifest if it already exists")
    p_init.add_argument("--open-config", action="store_true", help="Open manifest in editor after init")
    p_init.add_argument("--interactive", action="store_true", help="Configure manifest interactively")

    p_config = sub.add_parser("config")
    p_config.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_config.add_argument("--force", action="store_true", help="Overwrite manifest if it already exists")
    p_config.add_argument("--open-config", action="store_true", help="Open manifest in editor after config")
    p_config.add_argument("--interactive", dest="interactive", action="store_true", default=True, help="Configure manifest interactively (default: true)")
    p_config.add_argument("--no-interactive", dest="interactive", action="store_false", help="Disable interactive setup")

    for name in ("prepare", "up", "run"):
        p = sub.add_parser(name)
        p.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
        p.add_argument("--fleet-config-out", default="", help="Optional output path for generated fleet config")
        p.add_argument("--poll-interval", type=int, default=5, help="Polling interval for fleet run")
        p.add_argument(
            "--init-if-missing",
            dest="init_if_missing",
            action="store_true",
            default=True,
            help="Auto-copy template manifest when manifest is missing (default: true)",
        )
        p.add_argument(
            "--no-init-if-missing",
            dest="init_if_missing",
            action="store_false",
            help="Disable auto init when manifest is missing",
        )
        p.add_argument("--open-config-on-init", action="store_true", help="Open manifest when auto-initialized")
        _add_restore_defaults_flag(p)
        p.add_argument(
            "--interactive",
            action="store_true",
            help="Run interactive setup when manifest is missing or still has placeholders",
        )
        p.add_argument(
            "--foreground",
            action="store_true",
            help="Run in foreground (debug mode). Default starts services in background.",
        )
        p.add_argument(
            "--with-web-console",
            dest="with_web_console",
            action="store_true",
            default=True,
            help="Ensure fleet web console is running (default: true)",
        )
        p.add_argument(
            "--no-web-console",
            dest="with_web_console",
            action="store_false",
            help="Do not start fleet web console automatically",
        )
        p.add_argument("--web-host", default="127.0.0.1", help="Fleet web console host")
        p.add_argument("--web-port", type=int, default=8787, help="Fleet web console port")
        _add_json_flag(p)

    p_list = sub.add_parser("list-bots")
    p_list.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    _add_json_flag(p_list)

    p_status = sub.add_parser("status")
    p_status.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_status.add_argument("--bot-id", default="", help="Optional bot id filter")
    _add_json_flag(p_status)

    p_start_all = sub.add_parser("start")
    p_start_all.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_start_all.add_argument("--bot-id", default="", help="Optional bot id; when omitted starts all enabled bots")
    p_start_all.add_argument("--poll-interval", type=int, default=5, help="Polling interval for started bot process")
    p_start_all.add_argument("--prepare-first", action="store_true", help="Run prepare before starting")
    p_start_all.add_argument(
        "--with-web-console",
        dest="with_web_console",
        action="store_true",
        default=True,
        help="Ensure fleet web console is running (default: true)",
    )
    p_start_all.add_argument(
        "--no-web-console",
        dest="with_web_console",
        action="store_false",
        help="Do not start fleet web console automatically",
    )
    p_start_all.add_argument("--web-host", default="127.0.0.1", help="Fleet web console host")
    p_start_all.add_argument("--web-port", type=int, default=8787, help="Fleet web console port")
    _add_restore_defaults_flag(p_start_all)
    _add_json_flag(p_start_all)

    p_stop_all = sub.add_parser("stop")
    p_stop_all.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_stop_all.add_argument("--bot-id", default="", help="Optional bot id; when omitted stops all bots in manifest")
    p_stop_all.add_argument("--timeout-sec", type=int, default=8, help="Graceful stop timeout before SIGKILL")
    p_stop_all.add_argument(
        "--with-web-console",
        dest="with_web_console",
        action="store_true",
        default=True,
        help="Stop fleet web console together (default: true)",
    )
    p_stop_all.add_argument(
        "--no-web-console",
        dest="with_web_console",
        action="store_false",
        help="Stop bots only and keep fleet web console running",
    )
    _add_json_flag(p_stop_all)

    p_restart = sub.add_parser("restart")
    p_restart.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_restart.add_argument("--bot-id", default="", help="Optional bot id; when omitted restarts all bots")
    p_restart.add_argument("--poll-interval", type=int, default=5, help="Polling interval for started bot process")
    p_restart.add_argument("--timeout-sec", type=int, default=8, help="Graceful stop timeout before SIGKILL")
    p_restart.add_argument("--prepare-first", action="store_true", help="Run prepare before restart")
    p_restart.add_argument(
        "--with-web-console",
        dest="with_web_console",
        action="store_true",
        default=True,
        help="Ensure fleet web console is running (default: true)",
    )
    p_restart.add_argument(
        "--no-web-console",
        dest="with_web_console",
        action="store_false",
        help="Do not start fleet web console automatically",
    )
    p_restart.add_argument("--web-host", default="127.0.0.1", help="Fleet web console host")
    p_restart.add_argument("--web-port", type=int, default=8787, help="Fleet web console port")
    _add_restore_defaults_flag(p_restart)
    _add_json_flag(p_restart)

    p_logs = sub.add_parser("logs")
    p_logs.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_logs.add_argument("--bot-id", required=True, help="Bot id to inspect logs")
    p_logs.add_argument("--lines", type=int, default=200, help="How many lines to print")
    p_logs.add_argument(
        "--kind",
        choices=["loop", "service"],
        default="loop",
        help="loop=service.loop.log, service=cheapclaw_service.log",
    )

    p_add = sub.add_parser("add-bot")
    p_add.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_add.add_argument("--poll-interval", type=int, default=5, help="Polling interval for started bot process")
    p_add.add_argument("--prepare", dest="prepare_after", action="store_true", default=True, help="Run prepare after adding bot (default: true)")
    p_add.add_argument("--no-prepare", dest="prepare_after", action="store_false", help="Do not run prepare after adding bot")
    p_add.add_argument("--start", dest="start_after", action="store_true", default=True, help="Start new bot process after prepare (default: true)")
    p_add.add_argument("--no-start", dest="start_after", action="store_false", help="Do not start new bot process")
    _add_restore_defaults_flag(p_add)
    _add_json_flag(p_add)

    p_start = sub.add_parser("start-bot")
    p_start.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_start.add_argument("--bot-id", required=True, help="Bot id to start")
    p_start.add_argument("--poll-interval", type=int, default=5, help="Polling interval for started bot process")
    p_start.add_argument("--prepare-first", action="store_true", help="Run prepare before starting bot")
    _add_restore_defaults_flag(p_start)
    _add_json_flag(p_start)

    p_stop = sub.add_parser("stop-bot")
    p_stop.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_stop.add_argument("--bot-id", required=True, help="Bot id to stop")
    p_stop.add_argument("--timeout-sec", type=int, default=8, help="Graceful stop timeout before SIGKILL")
    _add_json_flag(p_stop)

    p_reload = sub.add_parser("reload-bot")
    p_reload.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_reload.add_argument("--bot-id", required=True, help="Bot id to reload")
    p_reload.add_argument("--poll-interval", type=int, default=5, help="Polling interval for started bot process")
    p_reload.add_argument("--prepare-first", action="store_true", help="Run prepare before reloading bot")
    p_reload.add_argument("--timeout-sec", type=int, default=8, help="Graceful stop timeout before SIGKILL")
    _add_restore_defaults_flag(p_reload)
    _add_json_flag(p_reload)

    p_web = sub.add_parser("web", help="Run fleet web console in foreground")
    p_web.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_web.add_argument("--host", default="127.0.0.1", help="Host to bind web console")
    p_web.add_argument("--port", type=int, default=8787, help="Port to bind web console")

    p_web_start = sub.add_parser("web-start", help="Start fleet web console in background")
    p_web_start.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_web_start.add_argument("--host", default="127.0.0.1", help="Host to bind web console")
    p_web_start.add_argument("--port", type=int, default=8787, help="Port to bind web console")
    _add_json_flag(p_web_start)

    p_web_stop = sub.add_parser("web-stop", help="Stop fleet web console")
    p_web_stop.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_web_stop.add_argument("--timeout-sec", type=int, default=8, help="Graceful stop timeout before SIGKILL")
    _add_json_flag(p_web_stop)

    p_web_status = sub.add_parser("web-status", help="Show fleet web console status")
    p_web_status.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    _add_json_flag(p_web_status)

    p_agent_system = sub.add_parser(
        "bot-agent-system",
        aliases=["bot-agent-sysyem"],
        help="Install or manage extra agent systems",
    )
    p_agent_system_sub = p_agent_system.add_subparsers(dest="agent_system_command", required=True)

    p_agent_add = p_agent_system_sub.add_parser("add", help="Install an agent system from a zip archive")
    p_agent_add.add_argument("archive", help="Path to an agent system zip archive")
    p_agent_add.add_argument("--manifest", default=str(DEFAULT_MANIFEST_PATH), help="Path to fleet manifest JSON")
    p_agent_scope = p_agent_add.add_mutually_exclusive_group()
    p_agent_scope.add_argument("--bot-id", default="", help="Install only into one bot runtime")
    p_agent_scope.add_argument("--global", dest="install_global", action="store_true", help="Install into project assets/agent_library")
    p_agent_add.add_argument("--reload-after", action="store_true", help="Reload target bot after install (requires --bot-id)")
    p_agent_add.add_argument("--poll-interval", type=int, default=5, help="Polling interval when reloading bot")
    _add_json_flag(p_agent_add)
    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    json_output = bool(getattr(args, "json", False))
    manifest_path = Path(args.manifest).expanduser().resolve()

    if args.command in {"init", "config"}:
        if bool(args.interactive):
            seed_manifest: Dict[str, Any] = {}
            if manifest_path.exists():
                seed_manifest = _load_json(manifest_path)
            target_manifest_path, payload = _interactive_manifest_setup(
                manifest_path,
                seed_manifest=seed_manifest,
            )
            if target_manifest_path.exists() and not bool(args.force):
                should_overwrite = _prompt_bool(
                    f"manifest exists, overwrite? {target_manifest_path}",
                    False,
                )
                if not should_overwrite:
                    print("[ABORT] manifest not changed")
                    return 1
            _dump_json(target_manifest_path, payload)
            print(f"[OK] manifest saved: {target_manifest_path}")
            print("[NEXT] run: python scripts/fleet_one_click.py up --manifest", target_manifest_path)
            if bool(args.open_config):
                _open_config_file(target_manifest_path)
            return 0

        created = _scaffold_manifest(manifest_path, force=bool(args.force))
        if created:
            print(f"[OK] manifest created from template: {manifest_path}")
        else:
            print(f"[OK] manifest already exists: {manifest_path}")
        print("[NEXT] fill llm_config_path/runtime_root and bot config (credentials if needed) before running 'up'")
        if bool(args.open_config):
            _open_config_file(manifest_path)
        return 0

    if args.command == "web":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        if not FLEET_WEB_CONSOLE_PATH.exists():
            raise SystemExit(f"[ERROR] fleet web script not found: {FLEET_WEB_CONSOLE_PATH}")
        run_cmd = [
            sys.executable,
            str(FLEET_WEB_CONSOLE_PATH),
            "--manifest",
            str(manifest_path),
            "--host",
            str(args.host),
            "--port",
            str(int(args.port)),
        ]
        env = os.environ.copy()
        return _run(run_cmd, env=env, check=False)

    if args.command == "web-start":
        payload = _proc_start_fleet_web(
            manifest_path,
            host=str(args.host or "127.0.0.1"),
            port=int(args.port or 8787),
        )
        if json_output:
            print_json(payload)
        else:
            format_web_payload(payload, title="WEB")
        return 0

    if args.command == "web-stop":
        if manifest_path.exists():
            payload = _proc_stop_fleet_web(
                manifest_path,
                timeout_sec=int(args.timeout_sec),
            )
        else:
            payload = _proc_stop_fleet_web_fallback(
                timeout_sec=int(args.timeout_sec),
            )
        if json_output:
            print_json(payload)
        else:
            format_web_payload(payload, title="WEB")
        return 0

    if args.command == "web-status":
        payload = _proc_fleet_web_status_payload(manifest_path)
        if json_output:
            print_json(payload)
        else:
            format_web_payload(payload, title="WEB")
        return 0

    if args.command == "bot-agent-system":
        if args.agent_system_command == "add":
            archive_path = Path(str(args.archive)).expanduser().resolve()
            bot_id = str(args.bot_id or "").strip()
            if bool(args.reload_after) and not bot_id:
                raise SystemExit("[ERROR] --reload-after requires --bot-id")

            if bot_id:
                if not manifest_path.exists():
                    raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
                _manifest, _target, user_data_root, _llm = _proc_resolve_bot_from_manifest(manifest_path, bot_id)
                destination_root = (user_data_root / "agent_library").resolve()
                payload = _install_agent_system_archive(archive_path, destination_root)
                payload["scope"] = "bot_runtime"
                payload["bot_id"] = bot_id
                payload["next_step"] = f"cheapclaw reload-bot --bot-id {bot_id}"
                if bool(args.reload_after):
                    reload_result = _proc_stop_single_bot_from_manifest(
                        manifest_path,
                        bot_id=bot_id,
                        timeout_sec=8,
                    )
                    start_result = _proc_start_single_bot_from_manifest(
                        manifest_path,
                        bot_id=bot_id,
                        poll_interval=int(args.poll_interval),
                    )
                    payload["reload"] = {"stop": reload_result, "start": start_result}
                    payload["next_step"] = "已自动 reload 目标 bot"
            else:
                destination_root = AGENT_LIBRARY_ROOT.resolve()
                payload = _install_agent_system_archive(archive_path, destination_root)
                payload["scope"] = "global_assets"
                payload["next_step"] = "运行 `cheapclaw prepare` 或 `cheapclaw reload-bot --bot-id <id> --prepare-first` 使 bot 生效"

            if json_output:
                print_json(payload)
            else:
                format_agent_system_payload(payload)
            return 0

    if args.command == "list-bots":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        bots = _list_manifest_bots(manifest_path, enabled_only=False)
        payload = {
            "status": "success",
            "manifest_path": str(manifest_path),
            "bot_count": len(bots),
            "bots": [
                {
                    "bot_id": str(item.get("bot_id") or ""),
                    "display_name": str(item.get("display_name") or item.get("bot_id") or ""),
                    "channel": str(item.get("channel") or ""),
                    "enabled": bool(item.get("enabled", True)),
                    "serve_webhooks": bool(item.get("serve_webhooks", False)),
                }
                for item in bots
            ],
        }
        if json_output:
            print_json(payload)
        else:
            format_list_bots_payload(payload)
        return 0

    if args.command == "status":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        bot_id = str(args.bot_id or "").strip()
        if bot_id:
            payload = {
                "status": "success",
                "manifest_path": str(manifest_path),
                "bot_count": 1,
                "running_count": 1 if _proc_status_single_bot_from_manifest(manifest_path, bot_id).get("alive") else 0,
                "bots": [_proc_status_single_bot_from_manifest(manifest_path, bot_id)],
            }
        else:
            payload = _proc_status_all_bots_from_manifest(manifest_path)
        if json_output:
            payload = {
                **payload,
                "web_console": _proc_fleet_web_status_payload(manifest_path),
            }
            print_json(payload)
        else:
            format_status_payload(payload)
            format_web_payload(_proc_fleet_web_status_payload(manifest_path), title="WEB")
        return 0

    if args.command == "start":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        if bool(args.prepare_first):
            result = prepare_from_manifest(
                manifest_path,
                write_fleet_to=None,
                run_capture=_run_capture,
                restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
            )
            if json_output:
                print_json(result)
            else:
                format_prepare_payload(result)
        bot_id = str(args.bot_id or "").strip()
        if bot_id:
            payload = _proc_start_single_bot_from_manifest(
                manifest_path,
                bot_id=bot_id,
                poll_interval=int(args.poll_interval),
            )
        else:
            payload = _proc_start_all_bots_from_manifest(
                manifest_path,
                poll_interval=int(args.poll_interval),
            )
        if json_output:
            print_json(payload)
        else:
            format_start_stop_payload(payload, title="START")
        if bool(args.with_web_console):
            web_payload = _proc_start_fleet_web(
                manifest_path,
                host=str(args.web_host or "127.0.0.1"),
                port=int(args.web_port or 8787),
            )
            if json_output:
                print_json(web_payload)
            else:
                format_web_payload(web_payload, title="WEB")
        return 0

    if args.command == "stop":
        if not manifest_path.exists():
            bot_id = str(args.bot_id or "").strip()
            if bot_id:
                raise SystemExit(
                    f"[ERROR] manifest not found: {manifest_path}\n"
                    f"[HINT] cannot stop specific bot '{bot_id}' without manifest; "
                    "use `cheapclaw stop` to stop all by fallback scan."
                )
            payload = _proc_stop_all_fallback_without_manifest(
                timeout_sec=int(args.timeout_sec),
            )
            if json_output:
                print_json(payload)
            else:
                format_emergency_stop_payload(payload)
            return 0
        bot_id = str(args.bot_id or "").strip()
        if bot_id:
            payload = _proc_stop_single_bot_from_manifest(
                manifest_path,
                bot_id=bot_id,
                timeout_sec=int(args.timeout_sec),
            )
        else:
            payload = _proc_stop_all_bots_from_manifest(
                manifest_path,
                timeout_sec=int(args.timeout_sec),
            )
        if json_output:
            print_json(payload)
        else:
            format_start_stop_payload(payload, title="STOP")
        if bool(args.with_web_console):
            web_payload = _proc_stop_fleet_web(
                manifest_path,
                timeout_sec=int(args.timeout_sec),
            )
            if json_output:
                print_json(web_payload)
            else:
                format_web_payload(web_payload, title="WEB")
        return 0

    if args.command == "restart":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        if bool(args.prepare_first):
            result = prepare_from_manifest(
                manifest_path,
                write_fleet_to=None,
                run_capture=_run_capture,
                restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
            )
            if json_output:
                print_json(result)
            else:
                format_prepare_payload(result)
        bot_id = str(args.bot_id or "").strip()
        if bot_id:
            stop_payload = _proc_stop_single_bot_from_manifest(
                manifest_path,
                bot_id=bot_id,
                timeout_sec=int(args.timeout_sec),
            )
            start_payload = _proc_start_single_bot_from_manifest(
                manifest_path,
                bot_id=bot_id,
                poll_interval=int(args.poll_interval),
            )
        else:
            stop_payload = _proc_stop_all_bots_from_manifest(
                manifest_path,
                timeout_sec=int(args.timeout_sec),
            )
            start_payload = _proc_start_all_bots_from_manifest(
                manifest_path,
                poll_interval=int(args.poll_interval),
            )
        payload = {
            "status": "success",
            "manifest_path": str(manifest_path),
            "stop": stop_payload,
            "start": start_payload,
        }
        if json_output:
            print_json(payload)
        else:
            format_restart_payload(payload)
        if bool(args.with_web_console):
            web_payload = _proc_start_fleet_web(
                manifest_path,
                host=str(args.web_host or "127.0.0.1"),
                port=int(args.web_port or 8787),
            )
            if json_output:
                print_json(web_payload)
            else:
                format_web_payload(web_payload, title="WEB")
        return 0

    if args.command == "logs":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        status = _proc_status_single_bot_from_manifest(manifest_path, str(args.bot_id))
        log_key = "loop_log_path" if str(args.kind) == "loop" else "service_log_path"
        log_path = Path(str(status.get(log_key) or "")).expanduser().resolve()
        _print_log_tail(log_path, lines=int(args.lines))
        return 0

    if args.command == "add-bot":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}\n[HINT] run `cheapclaw config` first.")
        new_bot_id = _interactive_add_bot(manifest_path)
        print(f"[OK] bot added to manifest: {new_bot_id}")
        if bool(args.prepare_after):
            result = prepare_from_manifest(
                manifest_path,
                write_fleet_to=None,
                run_capture=_run_capture,
                restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
            )
            if json_output:
                print_json(result)
            else:
                format_prepare_payload(result)
        if bool(args.start_after):
            start_result = _proc_start_single_bot_from_manifest(
                manifest_path,
                bot_id=new_bot_id,
                poll_interval=int(args.poll_interval),
            )
            if json_output:
                print_json(start_result)
            else:
                format_start_stop_payload(start_result, title="START")
        return 0

    if args.command == "start-bot":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        if bool(args.prepare_first):
            result = prepare_from_manifest(
                manifest_path,
                write_fleet_to=None,
                run_capture=_run_capture,
                restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
            )
            if json_output:
                print_json(result)
            else:
                format_prepare_payload(result)
        start_result = _proc_start_single_bot_from_manifest(
            manifest_path,
            bot_id=str(args.bot_id),
            poll_interval=int(args.poll_interval),
        )
        if json_output:
            print_json(start_result)
        else:
            format_start_stop_payload(start_result, title="START")
        return 0

    if args.command == "stop-bot":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        stop_result = _proc_stop_single_bot_from_manifest(
            manifest_path,
            bot_id=str(args.bot_id),
            timeout_sec=int(args.timeout_sec),
        )
        if json_output:
            print_json(stop_result)
        else:
            format_start_stop_payload(stop_result, title="STOP")
        return 0

    if args.command == "reload-bot":
        if not manifest_path.exists():
            raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
        if bool(args.prepare_first):
            result = prepare_from_manifest(
                manifest_path,
                write_fleet_to=None,
                run_capture=_run_capture,
                restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
            )
            if json_output:
                print_json(result)
            else:
                format_prepare_payload(result)
        stop_result = _proc_stop_single_bot_from_manifest(
            manifest_path,
            bot_id=str(args.bot_id),
            timeout_sec=int(args.timeout_sec),
        )
        if json_output:
            print_json(stop_result)
        else:
            format_start_stop_payload(stop_result, title="STOP")
        start_result = _proc_start_single_bot_from_manifest(
            manifest_path,
            bot_id=str(args.bot_id),
            poll_interval=int(args.poll_interval),
        )
        if json_output:
            print_json(start_result)
        else:
            format_start_stop_payload(start_result, title="START")
        return 0

    if not manifest_path.exists():
        if bool(args.interactive):
            seed_manifest: Dict[str, Any] = {}
            target_manifest_path, payload = _interactive_manifest_setup(
                manifest_path,
                seed_manifest=seed_manifest,
            )
            _dump_json(target_manifest_path, payload)
            manifest_path = target_manifest_path
            print(f"[OK] manifest saved: {manifest_path}")
            if bool(args.open_config_on_init):
                _open_config_file(manifest_path)
        else:
            if not bool(args.init_if_missing):
                raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")
            _scaffold_manifest(manifest_path, force=False)
            print(f"[INIT] manifest auto-created from template: {manifest_path}")
            print("[NEXT] edit manifest first, then rerun the same command.")
            if bool(args.open_config_on_init):
                _open_config_file(manifest_path)
            return 0

    manifest_preview = _load_json(manifest_path)
    placeholder_fields = _scan_placeholder_fields(manifest_preview)
    if placeholder_fields and bool(args.interactive):
        print("[SETUP] placeholder values detected, entering interactive setup")
        target_manifest_path, payload = _interactive_manifest_setup(
            manifest_path,
            seed_manifest=manifest_preview,
        )
        _dump_json(target_manifest_path, payload)
        manifest_path = target_manifest_path
        manifest_preview = payload
        placeholder_fields = _scan_placeholder_fields(manifest_preview)
    if placeholder_fields:
        print("[ERROR] manifest still contains placeholder values:")
        for item in placeholder_fields[:20]:
            print(" -", item)
        if len(placeholder_fields) > 20:
            print(f" - ... and {len(placeholder_fields) - 20} more")
        print(f"[HINT] Please edit: {manifest_path}")
        if bool(args.open_config_on_init):
            _open_config_file(manifest_path)
        return 2

    fleet_out = Path(args.fleet_config_out).expanduser().resolve() if str(args.fleet_config_out).strip() else None
    result = prepare_from_manifest(
        manifest_path,
        write_fleet_to=fleet_out,
        run_capture=_run_capture,
        restore_package_defaults=bool(getattr(args, "restore_package_defaults", False)),
    )
    if json_output:
        print_json(result)
    else:
        format_prepare_payload(result)

    if args.command == "prepare":
        print("[OK] prepare done")
        return 0

    if args.command in {"up", "run"} and not bool(args.foreground):
        payload = _proc_start_all_bots_from_manifest(
            manifest_path,
            poll_interval=int(args.poll_interval),
        )
        if json_output:
            print_json(payload)
        else:
            format_start_stop_payload(payload, title="START")
        if bool(args.with_web_console):
            web_payload = _proc_start_fleet_web(
                manifest_path,
                host=str(args.web_host or "127.0.0.1"),
                port=int(args.web_port or 8787),
            )
            if json_output:
                print_json(web_payload)
            else:
                format_web_payload(web_payload, title="WEB")
        print("[OK] services are running in background; use `cheapclaw status` and `cheapclaw logs --bot-id <id>`")
        return 0

    run_cmd = [
        sys.executable,
        str(SERVICE_PATH),
        "--fleet-config-path",
        str(result["fleet_config_path"]),
        "--run-fleet",
        "--poll-interval",
        str(max(1, int(args.poll_interval))),
    ]
    manifest = _load_json(manifest_path)
    env = _child_process_env(manifest)
    return _run(run_cmd, env=env, check=False)


if __name__ == "__main__":
    raise SystemExit(main())
