"""CLI entry point for asiai."""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
import time
from importlib.metadata import entry_points

from asiai import __version__

PLUGIN_API_VERSION: int = 1
"""Major version of the asiai sub-CLI plugin contract offered by this release."""


def _load_subcommand_plugins(
    subparsers: argparse._SubParsersAction,
    plugin_commands: dict,
) -> None:
    """Discover and register sub-CLI plugins from the asiai.subcommands entry-point group.

    Each plugin exposes a ``register(subparsers, commands)`` callable that adds its
    own argparse subparsers and populates *plugin_commands* with its handlers.
    Failures are non-fatal: a broken plugin is logged to stderr but never crashes asiai.
    """
    for ep in entry_points(group="asiai.subcommands"):
        try:
            register_fn = ep.load()
            register_fn(subparsers, plugin_commands)
        except Exception as exc:  # pragma: no cover
            print(
                f"[asiai] sub-CLI plugin {ep.name!r} failed to register: {exc}",
                file=sys.stderr,
            )


def _discover_engines(urls: list[str] | None = None) -> list:
    """Detect inference engines and return instantiated adapters."""
    from asiai.engines.detect import detect_engines
    from asiai.engines.exo import ExoEngine
    from asiai.engines.llamacpp import LlamaCppEngine
    from asiai.engines.lmstudio import LMStudioEngine
    from asiai.engines.mlxlm import MlxLmEngine
    from asiai.engines.ollama import OllamaEngine
    from asiai.engines.omlx import OmlxEngine
    from asiai.engines.vllm_mlx import VllmMlxEngine
    from asiai.engines.vmlx import VmlxEngine

    engine_map = {
        "ollama": OllamaEngine,
        "lmstudio": LMStudioEngine,
        "mlxlm": MlxLmEngine,
        "llamacpp": LlamaCppEngine,
        "omlx": OmlxEngine,
        "vllm_mlx": VllmMlxEngine,
        "vmlx": VmlxEngine,
        "exo": ExoEngine,
    }

    found = detect_engines(urls)
    engines = []
    for url, name, _version in found:
        cls = engine_map.get(name)
        if cls:
            engines.append(cls(url))
    return engines


def _parse_urls(url_arg: str | None) -> list[str] | None:
    """Parse comma-separated URL argument."""
    if url_arg:
        return [u.strip() for u in url_arg.split(",") if u.strip()]
    return None


def cmd_detect(args: argparse.Namespace) -> int:
    """Handle 'detect' command."""
    from asiai.display.cli_renderer import render_detect

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)

    results = []
    for engine in engines:
        models = engine.list_running()
        results.append(
            {
                "name": engine.name,
                "version": engine.version(),
                "url": engine.base_url,
                "models": [
                    {
                        "name": m.name,
                        "size_vram": m.size_vram,
                        "format": m.format,
                        "quantization": m.quantization,
                    }
                    for m in models
                ],
            }
        )

    render_detect(results)
    return 0


def cmd_models(args: argparse.Namespace) -> int:
    """Handle 'models' command."""
    from asiai.display.formatters import bold, dim, format_bytes, green

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)

    if not engines:
        if getattr(args, "json_output", False):
            print(json.dumps({"engines": []}, indent=2))
        else:
            print(dim("No inference engines detected."))
            print(dim("Try: brew install ollama && ollama serve"))
        return 1

    if getattr(args, "json_output", False):
        data = {"engines": []}
        for engine in engines:
            running = engine.list_running()
            entry = {
                "name": engine.name,
                "url": engine.base_url,
                "version": engine.version() or "",
                "models": [
                    {
                        "name": m.name,
                        "size_vram": m.size_vram,
                        "format": m.format,
                        "quantization": m.quantization,
                        "context_length": m.context_length,
                    }
                    for m in running
                ],
            }
            data["engines"].append(entry)
        print(json.dumps(data, indent=2))
        return 0

    for engine in engines:
        ver = engine.version()
        ver_str = f"  {dim('v' + ver)}" if ver else ""
        print(bold(f"{engine.name}") + ver_str + f"  {dim(engine.base_url)}")
        running = engine.list_running()
        if running:
            for m in running:
                vram = format_bytes(m.size_vram) if m.size_vram else ""
                quant = m.quantization or ""
                ctx = ""
                if m.context_length > 0:
                    if m.context_length >= 1024:
                        ctx = f"{m.context_length // 1024}k ctx"
                    else:
                        ctx = f"{m.context_length} ctx"
                print(f"  {green('●')} {m.name:<40} {vram:>10} {quant:>6}  {dim(ctx)}")
        else:
            print(dim("  No models loaded."))
        print()

    return 0


def cmd_monitor(args: argparse.Namespace) -> int:
    """Handle 'monitor' command."""
    from asiai.collectors.snapshot import collect_snapshot
    from asiai.display.cli_renderer import (
        render_analyze,
        render_compare,
        render_history,
        render_snapshot,
    )
    from asiai.storage.db import (
        DEFAULT_DB_PATH,
        init_db,
        query_compare,
        query_history,
        store_snapshot,
    )

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)
    db_path = args.db or DEFAULT_DB_PATH

    # Initialize DB
    init_db(db_path)

    if args.history:
        # Parse hours from period string (e.g. "24h", "1h", "48")
        period = args.history.rstrip("h")
        hours = int(period) if period.isdigit() else 24
        data = query_history(db_path, hours)
        render_history(data, hours)
        return 0

    if args.analyze is not None:
        hours = args.analyze if args.analyze > 0 else 24
        data = query_history(db_path, hours)
        render_analyze(data, hours)
        return 0

    if args.compare:
        parts = args.compare
        if len(parts) != 2:
            from asiai.display.formatters import red

            print(red("Error: --compare requires exactly 2 timestamps"), file=sys.stderr)
            return 1
        data = query_compare(db_path, int(parts[0]), int(parts[1]))
        render_compare(data)
        return 0

    quiet = getattr(args, "quiet", False)
    json_output = getattr(args, "json_output", False)
    webhook_url = getattr(args, "alert_webhook", None)

    if args.watch is not None and args.watch < 1:
        args.watch = 1

    # IOReport power sampler (no sudo, optional)
    ioreport_sampler = None
    try:
        from asiai.collectors.ioreport import IOReportSampler, ioreport_available

        if ioreport_available():
            ioreport_sampler = IOReportSampler()
            # Allow initial baseline to accumulate (1s minimum)
            time.sleep(1)
    except Exception:
        pass

    prev_snapshot: dict | None = None

    # Default: snapshot (with optional --watch)
    try:
        if args.watch:
            try:
                while True:
                    if not quiet:
                        subprocess.run(["clear"], check=False)
                    snap = collect_snapshot(engines, ioreport_sampler=ioreport_sampler)
                    store_snapshot(db_path, snap)
                    if webhook_url:
                        from asiai.alerting import check_and_alert

                        check_and_alert(snap, prev_snapshot, webhook_url, db_path)
                        prev_snapshot = snap
                    if json_output:
                        print(json.dumps(snap, indent=2))
                    elif not quiet:
                        render_snapshot(snap)
                    time.sleep(args.watch)
            except KeyboardInterrupt:
                if not quiet:
                    print()
                return 0
        else:
            snap = collect_snapshot(engines, ioreport_sampler=ioreport_sampler)
            store_snapshot(db_path, snap)
            if webhook_url:
                from asiai.alerting import check_and_alert

                check_and_alert(snap, prev_snapshot, webhook_url, db_path)
            if json_output:
                print(json.dumps(snap, indent=2))
            elif not quiet:
                render_snapshot(snap)
            return 0
    finally:
        if ioreport_sampler is not None:
            ioreport_sampler.close()


def cmd_doctor(args: argparse.Namespace) -> int:
    """Handle 'doctor' command."""
    from asiai.display.cli_renderer import render_doctor
    from asiai.doctor import run_checks

    db_path = args.db or None
    checks = run_checks(db_path) if db_path else run_checks()
    render_doctor(checks)

    # Return non-zero if any check failed
    if any(c.status == "fail" for c in checks):
        return 1
    return 0


def cmd_tui(args: argparse.Namespace) -> int:
    """Handle 'tui' command."""
    try:
        from asiai.display.tui import run_tui
    except ImportError:
        from asiai.display.formatters import dim, red

        print(red("Textual is required for the TUI."), file=sys.stderr)
        print(dim("Install with: pip install asiai[tui]"), file=sys.stderr)
        return 1

    from asiai.storage.db import DEFAULT_DB_PATH, init_db

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)
    db_path = args.db or DEFAULT_DB_PATH
    init_db(db_path)
    try:
        run_tui(engines=engines, db_path=db_path)
    except ImportError:
        from asiai.display.formatters import dim, red

        print(red("Textual is required for the TUI."), file=sys.stderr)
        print(dim("Install with: pip install asiai[tui]"), file=sys.stderr)
        return 1
    return 0


def cmd_daemon(args: argparse.Namespace) -> int:
    """Handle 'daemon' command."""
    from asiai.daemon import (
        SERVICES,
        _read_plist_config,
        daemon_logs,
        daemon_start,
        daemon_status_all,
        daemon_stop,
        daemon_stop_all,
    )
    from asiai.display.formatters import bold, dim, green, red, yellow

    action = args.action
    if not action:
        print(dim("Usage: asiai daemon {start|stop|status|logs}"), file=sys.stderr)
        return 1

    if action == "start":
        service = getattr(args, "service", "monitor")
        kwargs: dict = {}
        if service == "monitor":
            kwargs["interval"] = getattr(args, "interval", 60)
            webhook = getattr(args, "alert_webhook", None)
            if webhook:
                kwargs["webhook_url"] = webhook
        elif service == "web":
            kwargs["port"] = getattr(args, "port", 8899)
            kwargs["host"] = getattr(args, "host", "127.0.0.1")
            if kwargs["host"] != "127.0.0.1":
                print(yellow(f"Warning: binding to {kwargs['host']} exposes the dashboard."))
                print(yellow("No authentication is configured. Use with caution."))

        result = daemon_start(service, **kwargs)
        if result["status"] == "started":
            profile = SERVICES[service]
            print(green(f"{profile.description} started"))
            print(f"  Plist: {result['plist']}")
            if service == "monitor":
                print(f"  Interval: {result.get('interval', 60)}s")
                if kwargs.get("webhook_url"):
                    print(f"  Webhook: {kwargs['webhook_url']}")
            elif service == "web":
                host = result.get("host", "127.0.0.1")
                port = result.get("port", 8899)
                print(f"  URL: http://{host}:{port}")
        else:
            print(red(f"Error: {result['message']}"), file=sys.stderr)
            return 1

    elif action == "stop":
        if getattr(args, "stop_all", False):
            results = daemon_stop_all()
            for svc_name, result in results.items():
                profile = SERVICES[svc_name]
                print(f"  {profile.description}: {result['status']}")
        else:
            service = getattr(args, "service", "monitor")
            result = daemon_stop(service)
            profile = SERVICES[service]
            if result["status"] == "stopped":
                print(green(f"{profile.description} stopped"))
            else:
                print(red(f"Error: {result['message']}"), file=sys.stderr)
                return 1

    elif action == "status":
        statuses = daemon_status_all()
        print(bold("Services"))
        for svc_name, status in statuses.items():
            profile = SERVICES[svc_name]
            if status["running"]:
                pid_str = f" (PID {status['pid']})" if status["pid"] else ""
                extra = ""
                config = _read_plist_config(svc_name)
                if svc_name == "monitor" and config.get("interval"):
                    extra = f"  every {config['interval']}s"
                    if config.get("webhook_url"):
                        extra += f"  webhook: {config['webhook_url']}"
                elif svc_name == "web":
                    host = config.get("host", "127.0.0.1")
                    port = config.get("port", 8899)
                    extra = f"  http://{host}:{port}"
                print(f"  {green('●')} {bold(profile.description)}{pid_str}{dim(extra)}")
            else:
                if status["plist_exists"]:
                    detail = dim("  installed but not running")
                else:
                    detail = dim("  not installed")
                print(f"  {dim('○')} {dim(profile.description)}{detail}")

    elif action == "logs":
        service = getattr(args, "service", "monitor")
        lines = getattr(args, "lines", 50)
        output = daemon_logs(service, lines)
        print(output)

    return 0


def cmd_web(args: argparse.Namespace) -> int:
    """Handle 'web' command."""
    try:
        import uvicorn  # noqa: F401

        has_fastapi = True
    except ImportError:
        has_fastapi = False

    if not has_fastapi:
        from asiai.display.formatters import dim, red

        print(red("FastAPI is required for the web dashboard."), file=sys.stderr)
        print(dim("Install with: pip install asiai[web]"), file=sys.stderr)
        return 1

    from asiai.storage.db import DEFAULT_DB_PATH, init_db
    from asiai.web.app import create_app
    from asiai.web.state import AppState

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)
    db_path = args.db or DEFAULT_DB_PATH
    init_db(db_path)

    state = AppState(engines=engines, db_path=db_path)
    app = create_app(state)

    host = args.host
    port = args.port

    # Open browser unless --no-open
    if not args.no_open:
        import threading
        import webbrowser

        def _open_browser() -> None:
            import time as _time

            _time.sleep(1.5)
            webbrowser.open(f"http://{host}:{port}")

        threading.Thread(target=_open_browser, daemon=True).start()

    uvicorn.run(app, host=host, port=port, log_level="info")
    return 0


def _parse_compare_arg(arg: str) -> tuple[str, str | None]:
    """Parse 'model@engine' or 'model' into (model, engine_name_or_None)."""
    if "@" in arg:
        idx = arg.rfind("@")
        return arg[:idx], arg[idx + 1 :]
    return arg, None


def expand_compare_args(
    compare_args: list[str],
    engines_filter: str | None,
    detected_engines: list,
) -> list:
    """Expand --compare arguments into BenchmarkSlot list.

    Args:
        compare_args: Raw --compare values (e.g. ["qwen3.5:4b@lmstudio", "deepseek:7b"])
        engines_filter: --engines value (comma-separated) or None
        detected_engines: All detected InferenceEngine instances

    Returns:
        List of BenchmarkSlot instances. Raises SystemExit on error.
    """
    from asiai.benchmark.runner import BenchmarkSlot

    # Determine available engines (filtered or all detected)
    if engines_filter:
        wanted = {e.strip().lower() for e in engines_filter.split(",")}
        available_engines = [e for e in detected_engines if e.name in wanted]
    else:
        available_engines = detected_engines

    engine_by_name = {e.name: e for e in detected_engines}

    # Pre-validate: estimate max expansion before building slots
    explicit_count = sum(1 for a in compare_args if "@" in a)
    expandable_count = len(compare_args) - explicit_count
    max_slots = explicit_count + expandable_count * max(len(available_engines), 1)
    if max_slots > 8:
        print(
            f"✗ Too many slots (up to {max_slots}). Use -e to narrow engines "
            f"or model@engine for explicit pairs. Max 8.",
            file=sys.stderr,
        )
        sys.exit(1)

    slots: list = []
    seen: set = set()
    for arg in compare_args:
        model, engine_name = _parse_compare_arg(arg)
        if engine_name:
            # Explicit slot: model@engine
            engine = engine_by_name.get(engine_name)
            if engine is None:
                known = ", ".join(sorted(engine_by_name.keys()))
                print(
                    f"✗ Engine '{engine_name}' not found (available: {known})",
                    file=sys.stderr,
                )
                sys.exit(1)
            key = (engine_name, model)
            if key not in seen:
                seen.add(key)
                slots.append(BenchmarkSlot(engine=engine, model=model))
        else:
            # Expand across available engines
            for engine in available_engines:
                key = (engine.name, model)
                if key not in seen:
                    seen.add(key)
                    slots.append(BenchmarkSlot(engine=engine, model=model))

    if not slots:
        print("✗ No benchmark slots to run.", file=sys.stderr)
        sys.exit(1)

    return slots


def _run_agentic_bench(args: argparse.Namespace) -> int:
    """Run the 8-run agentic prefix cache reuse benchmark (US-047)."""
    from asiai.benchmark.agentic import run_agentic_bench
    from asiai.benchmark.auto_restart import auto_restart_engine
    from asiai.display.formatters import dim, red, yellow

    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)
    if not engines:
        print(red("✗ No inference engines detected."), file=sys.stderr)
        return 1
    if len(engines) > 1 and not args.url:
        print(
            red("✗ --agentic-mode requires exactly one target. Specify --url to pick one."),
            file=sys.stderr,
        )
        return 1
    engine = engines[0]

    if getattr(args, "agentic_auto_restart", False):
        print(f"  {dim('●')} aisctl restart {engine.name}...")
        ok, msg = auto_restart_engine(engine.name, engine.base_url)
        if ok:
            print(f"  {dim('●')} {msg}")
        else:
            print(yellow(f"  ⚠ auto-restart skipped: {msg}"))
            if getattr(args, "agentic_auto_restart_required", False):
                print(
                    red("✗ --agentic-auto-restart-required set: aborting."),
                    file=sys.stderr,
                )
                return 1

    model_id = args.model
    if not model_id:
        running = engine.list_running()
        if not running:
            print(
                red(f"✗ No model loaded on {engine.name}. Specify --model or load one."),
                file=sys.stderr,
            )
            return 1
        model_id = running[0].name

    only = None
    if args.agentic_only:
        only = [p.strip() for p in args.agentic_only.split(",") if p.strip()]

    print(
        f"  {dim('●')} agentic-mode bench {engine.name} @ {engine.base_url} "
        f"model={model_id} pause={args.agentic_pause}s "
        f"skip_long={'yes' if args.agentic_skip_long else 'no'}"
    )

    def _on_run(run):  # noqa: ANN001 — duck-typed AgenticRun
        cached = run.cached_tokens if run.cached_tokens is not None else "-"
        ttft = f"{run.ttft_ms}ms" if run.ttft_ms is not None else "?ms"
        tps = f"{run.decode_tok_s:.2f}" if run.decode_tok_s is not None else "?"
        if run.error:
            print(f"  [{run.phase}] ERROR {run.error}: {run.error_body or ''}")
        else:
            print(
                f"  [{run.phase}] TTFT={ttft} decode={tps} tok/s "
                f"prompt={run.prompt_tokens} cached={cached} "
                f"compl={run.completion_tokens}"
            )

    result = run_agentic_bench(
        base_url=engine.base_url,
        engine_name=engine.name,
        model=model_id,
        pause=args.agentic_pause,
        skip_long=args.agentic_skip_long,
        only=only,
        out_path=args.agentic_output,
        include_host=getattr(args, "agentic_include_host", False),
    )

    print(f"\nVerdict prefix_cache_reuse: {result['prefix_cache_reuse_verdict']}")
    qg = result.get("quality_gates") or {}
    es = qg.get("early_stop") or {}
    if es.get("detected"):
        truncated = ", ".join(t["phase"] for t in es.get("truncated_runs", []))
        print(red(f"  ⚠ Early-stop detected on: {truncated}"))
    mp = qg.get("memory_pressure") or {}
    if mp.get("alerted"):
        print(red(f"  ⚠ Memory pressure during bench: {mp.get('alert_reason')}"))
    dups = qg.get("duplicate_processes") or []
    if dups:
        pids = ", ".join(d["pid"] for d in dups)
        print(
            red(f"  ⚠ Duplicate {engine.name} processes (PIDs: {pids}) — bench may be unreliable")
        )
    if args.agentic_output:
        print(f"Saved {args.agentic_output}")
    return 0


def cmd_bench(args: argparse.Namespace) -> int:
    """Handle 'bench' command."""
    from asiai.benchmark.regression import detect_regressions
    from asiai.benchmark.reporter import aggregate_results, build_report, export_benchmark
    from asiai.benchmark.runner import find_common_model, run_benchmark
    from asiai.display.cli_renderer import render_bench, render_bench_history, render_regressions
    from asiai.display.formatters import dim, red, yellow
    from asiai.storage.db import (
        DEFAULT_DB_PATH,
        init_db,
        query_benchmarks,
        store_benchmark,
    )

    db_path = args.db or DEFAULT_DB_PATH
    init_db(db_path)

    # Agentic mode: 8-run prefix cache reuse protocol (US-047)
    if getattr(args, "agentic_mode", False):
        return _run_agentic_bench(args)

    # History mode
    if args.history:
        period = args.history.rstrip("dh")
        if args.history.endswith("d"):
            hours = int(period) * 24 if period.isdigit() else 168
        else:
            hours = int(period) if period.isdigit() else 24
        rows = query_benchmarks(db_path, hours=hours, model=args.model or "")
        render_bench_history(rows)
        return 0

    # Benchmark mode
    urls = _parse_urls(args.url)
    engines = _discover_engines(urls)

    if not engines:
        print(red("✗ No inference engines detected."), file=sys.stderr)
        print(dim("  Try: brew install ollama && ollama serve"), file=sys.stderr)
        return 1

    # Pre-bench environment validation
    for eng in engines:
        ver = eng.version()
        if ver:
            print(f"  {dim('●')} {eng.name} v{ver} @ {eng.base_url}")
        else:
            print(f"  {dim('●')} {eng.name} @ {eng.base_url}")
        # Show Ollama runner type
        if eng.name == "ollama":
            from asiai.benchmark.runner import detect_ollama_runner_type

            runner = detect_ollama_runner_type()
            if runner:
                print(f"    {dim('runner:')} {runner}")
    print()

    compare_mode = getattr(args, "compare", None)

    # Quick mode: 1 prompt (code), 1 run
    if getattr(args, "quick", False):
        prompt_names = ["code"]
        args.runs = 1
    else:
        prompt_names = None

    # Parse prompt types (explicit --prompts overrides --quick)
    if args.prompts:
        prompt_names = [p.strip() for p in args.prompts.split(",")]

    runs = max(1, min(getattr(args, "runs", 1), 100))
    power = getattr(args, "power", False)
    context_size = 0
    if getattr(args, "context_size", None):
        from asiai.benchmark.prompts import parse_context_size

        context_size = parse_context_size(args.context_size)

    if compare_mode:
        # --- Cross-model comparison mode ---
        bench_slots = expand_compare_args(args.compare, args.engines, engines)

        # Show what we are about to do
        slot_descs = []
        for s in bench_slots:
            slot_descs.append(f"{s.model}@{s.engine.name}")
        print(f"Comparing {len(bench_slots)} slots: {', '.join(slot_descs)}...")
        print()

        bench_run = run_benchmark(
            prompt_names=prompt_names,
            runs=runs,
            power=power,
            context_size=context_size,
            slots=bench_slots,
            progress_cb=lambda msg: print(f"  {msg}"),
        )
    else:
        # --- Legacy engine comparison mode ---
        # Filter engines
        if args.engines:
            wanted = {e.strip().lower() for e in args.engines.split(",")}
            engines = [e for e in engines if e.name in wanted]
            if not engines:
                print(
                    red(f"✗ None of the specified engines found: {args.engines}"),
                    file=sys.stderr,
                )
                return 1

        model = find_common_model(engines, args.model or "")
        if not model:
            print(
                yellow("⚠ No model to benchmark. Load a model or use --model."),
                file=sys.stderr,
            )
            return 1

        engine_names = ", ".join(e.name for e in engines)
        print(f"Benchmarking {model} on {engine_names}...")
        print()

        bench_run = run_benchmark(
            engines,
            model,
            prompt_names,
            runs=runs,
            power=power,
            context_size=context_size,
            progress_cb=lambda msg: print(f"  {msg}"),
        )

    # Inject kv_cache metadata if specified
    kv_cache_type = getattr(args, "kv_cache", None) or ""
    if kv_cache_type:
        for r in bench_run.results:
            r["kv_cache_type"] = kv_cache_type

    # Store results
    if bench_run.results:
        store_benchmark(db_path, bench_run.results)
        from asiai.storage.db import store_benchmark_process

        store_benchmark_process(db_path, bench_run.results)

    # Display errors
    for err in bench_run.errors:
        print(f"  {yellow('⚠')} {err}", file=sys.stderr)

    # Aggregate and render
    if compare_mode:
        report = build_report(bench_run.results)
    else:
        report = aggregate_results(bench_run.results)
        report["model"] = model  # Use user-requested name, not engine-resolved
    bench_ctx_size = bench_run.results[0].get("context_size", 0) if bench_run.results else 0
    render_bench(report, context_size=bench_ctx_size)

    # Export to JSON if requested
    export_path = getattr(args, "export", None)
    if export_path and bench_run.results:
        path = export_benchmark(bench_run.results, report, export_path)
        from asiai.display.formatters import green

        print(f"  {green('✓')} Exported to {path}")

    # Check for regressions against historical data
    if bench_run.results:
        regressions = detect_regressions(bench_run.results, db_path)
        if regressions:
            render_regressions(regressions)

    # Benchmark card generation (before share so SVG can be included in payload)
    card_svg = ""
    if getattr(args, "card", False) and bench_run.results:
        from asiai.benchmark.card import (
            extract_card_metadata,
            generate_card_svg,
            get_share_url,
            save_card,
        )
        from asiai.display.formatters import dim, green

        first_result = bench_run.results[0]
        eng_vers, pw_data, eng_quants = extract_card_metadata(bench_run.results)
        card_svg = generate_card_svg(
            report,
            hw_chip=first_result.get("hw_chip", ""),
            model_quantization=first_result.get("model_quantization", ""),
            ram_gb=first_result.get("ram_gb", 0),
            gpu_cores=first_result.get("gpu_cores", 0),
            context_size=first_result.get("context_size", 0),
            engine_versions=eng_vers,
            power_data=pw_data,
            engine_quants=eng_quants,
            kv_cache_type=kv_cache_type,
        )
        svg_path = save_card(card_svg, fmt="svg")
        print(f"  {green('✓')} Card saved: {svg_path}")

    # Community share (opt-in)
    submission_id = ""
    if getattr(args, "share", False) and bench_run.results:
        import base64

        from asiai.community import build_submission, submit_benchmark
        from asiai.display.formatters import dim, green

        # Refuse to share results when every measurable run failed. The public
        # leaderboard was previously polluted with median_tok_s=0 entries
        # before this guard. ``--share-on-error`` opts back in for users who
        # want full diagnostic transparency.
        successful_runs = sum(1 for r in bench_run.results if r.get("tok_per_sec", 0) > 0)
        if successful_runs == 0 and not getattr(args, "share_on_error", False):
            n_total = len(bench_run.results)
            print(f"  {dim(f'⚠ Skipping share: 0/{n_total} runs succeeded.')}")
            print(f"  {dim('Use --share-on-error to share zero-value results anyway.')}")
        else:
            payload = build_submission(bench_run.results, report)
            # Include locally-rendered PNG for server-side card (macOS sips rendering)
            if card_svg and "svg_path" in dir() and svg_path:
                from asiai.benchmark.card import convert_svg_to_png

                local_png = convert_svg_to_png(svg_path)
                if local_png:
                    try:
                        with open(local_png, "rb") as f:
                            payload["card_png_b64"] = base64.b64encode(f.read()).decode("ascii")
                    except OSError:
                        pass
            result = submit_benchmark(payload, db_path=db_path)
            if result.success:
                submission_id = result.submission_id
                print(f"  {green('✓')} Shared to community ({result.submission_id[:8]}...)")
            else:
                print(f"  {dim('⚠ Share failed: ' + result.error)}")

    # Card share URL + PNG download
    if card_svg and getattr(args, "card", False):
        from asiai.display.formatters import dim, green

        if submission_id:
            from asiai.benchmark.card import download_card_png, get_share_url

            png_path = download_card_png(submission_id)
            if png_path:
                print(f"  {green('✓')} PNG card: {png_path}")
                share_url = get_share_url(submission_id)
                print(f"  {dim('Share URL: ' + share_url)}")
            else:
                print(f"  {dim('⚠ PNG download failed (SVG available locally)')}")
        else:
            print(f"  {dim('Tip: add --share to get a PNG + shareable URL')}")

        # S2: open card on macOS
        try:
            has_png = submission_id and "png_path" in dir() and png_path
            card_to_open = png_path if has_png else svg_path
            subprocess.Popen(
                ["open", card_to_open],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
        except OSError:
            pass

    return 0


def cmd_leaderboard(args: argparse.Namespace) -> int:
    """Handle 'leaderboard' command."""
    from asiai.community import fetch_leaderboard
    from asiai.display.formatters import bold, dim, green

    chip = getattr(args, "chip", "")
    model = getattr(args, "model", "")

    entries = fetch_leaderboard(chip=chip, model=model)
    if not entries:
        print(dim("No community data available (server may be unreachable)."))
        return 1

    print(bold("Community Leaderboard"))
    if chip:
        print(dim(f"  Chip: {chip}"))
    if model:
        print(dim(f"  Model: {model}"))
    print()

    # Table header
    print(f"  {'Engine':<12} {'Model':<30} {'tok/s':>8} {'TTFT':>8} {'Samples':>8}")
    print(f"  {'─' * 12} {'─' * 30} {'─' * 8} {'─' * 8} {'─' * 8}")
    for entry in entries:
        eng = entry.get("engine", "?")
        mdl = entry.get("model", "?")
        tok = entry.get("median_tok_s", 0.0)
        ttft = entry.get("median_ttft_ms", 0.0)
        samples = entry.get("samples", entry.get("submissions", 0))
        print(f"  {green(eng):<12} {mdl:<30} {tok:>8.1f} {ttft:>7.0f}ms {samples:>8}")

    return 0


def cmd_compare(args: argparse.Namespace) -> int:
    """Handle 'compare' command."""
    from asiai.benchmark.reporter import aggregate_results
    from asiai.community import fetch_comparison
    from asiai.display.formatters import bold, dim, green, red
    from asiai.storage.db import DEFAULT_DB_PATH, init_db, query_benchmarks

    db_path = args.db or DEFAULT_DB_PATH
    init_db(db_path)

    chip = getattr(args, "chip", "")
    model = getattr(args, "model", "")

    if not chip:
        from asiai.collectors.system import collect_machine_info

        chip = collect_machine_info().split(" — ")[0] if " — " in collect_machine_info() else ""
        if not chip:
            from asiai.collectors.system import collect_hw_chip

            chip = collect_hw_chip()

    # Get local data
    rows = query_benchmarks(db_path, model=model)
    if not rows:
        print(dim("No local benchmarks found. Run 'asiai bench' first."))
        return 1

    local_report = aggregate_results(rows)
    comparison = fetch_comparison(chip, model or local_report.get("model", ""), local_report)

    if not comparison:
        print(dim("Could not fetch community data for comparison."))
        return 1

    print(bold(f"Your {chip} vs Community"))
    print()
    for eng_name, data in comparison.get("engines", {}).items():
        local_tok = data.get("local_median_tok_s", 0)
        comm_tok = data.get("community_median_tok_s", 0)
        delta_pct = data.get("delta_pct", 0)
        samples = data.get("community_samples", 0)

        if delta_pct > 0:
            delta_str = green(f"+{delta_pct:.1f}%")
        elif delta_pct < 0:
            delta_str = red(f"{delta_pct:.1f}%")
        else:
            delta_str = dim("=")

        print(
            f"  {eng_name:<12}  "
            f"you: {local_tok:.1f} tok/s  "
            f"community: {comm_tok:.1f} tok/s  "
            f"{delta_str}  "
            f"{dim(f'({samples} samples)')}"
        )

    return 0


def cmd_mcp(args: argparse.Namespace) -> int:
    """Handle 'mcp' command — start MCP server."""
    try:
        from asiai.mcp.server import serve
    except ImportError:
        from asiai.display.formatters import dim, red

        print(red("MCP SDK is required for the MCP server."), file=sys.stderr)
        print(dim("Install with: pip install asiai[mcp]"), file=sys.stderr)
        return 1

    serve(
        transport=getattr(args, "transport", "stdio"),
        host=getattr(args, "host", "127.0.0.1"),
        port=getattr(args, "port", 8900),
        register=getattr(args, "register", False),
    )
    return 0


def cmd_unregister(args: argparse.Namespace) -> int:
    """Handle 'unregister' command — remove agent credentials."""
    from asiai.community import get_agent_info, unregister_agent
    from asiai.display.formatters import dim, green, yellow

    info = get_agent_info()
    if not info:
        print(yellow("Not registered — no agent.json found."))
        return 0

    agent_id = info.get("agent_id", "unknown")
    removed = unregister_agent()
    if removed:
        print(green(f"Agent {agent_id} unregistered — credentials removed."))
        print(dim("You can re-register anytime with: asiai mcp --register"))
    else:
        print(yellow("Failed to remove agent.json"))
    return 0


def cmd_recommend(args: argparse.Namespace) -> int:
    """Handle 'recommend' command."""
    from asiai.advisor.recommender import recommend
    from asiai.collectors.system import collect_hw_chip, collect_memory
    from asiai.display.formatters import bold, dim, green, yellow
    from asiai.storage.db import DEFAULT_DB_PATH, init_db

    db_path = args.db or DEFAULT_DB_PATH
    init_db(db_path)

    chip = collect_hw_chip()
    mem = collect_memory()
    ram_gb = round(mem.total / (1024**3))

    use_case = getattr(args, "use_case", "throughput")
    model_filter = getattr(args, "model", "") or ""
    community = getattr(args, "community", False)
    community_url = ""
    if community:
        from asiai.community import get_api_url

        community_url = get_api_url()

    recs = recommend(
        chip=chip,
        ram_gb=ram_gb,
        use_case=use_case,
        model_filter=model_filter,
        db_path=db_path,
        community_url=community_url,
    )

    if not recs:
        print(dim("No recommendations available. Run 'asiai bench' first."))
        return 1

    print(bold(f"Recommendations for {chip} ({ram_gb} GB)"))
    if use_case != "throughput":
        print(dim(f"  Optimizing for: {use_case}"))
    print()

    for i, rec in enumerate(recs[:10], 1):
        score_color = green if rec.score >= 70 else (yellow if rec.score >= 40 else dim)
        conf_str = dim(f"[{rec.confidence}]")
        src_str = dim(f"({rec.source})")

        tok_str = f"{rec.median_tok_s:.1f} tok/s" if rec.median_tok_s else ""
        ttft_str = f"{rec.median_ttft_ms:.0f}ms TTFT" if rec.median_ttft_ms else ""
        metrics = ", ".join(filter(None, [tok_str, ttft_str]))

        line = f"  {i:>2}. {score_color(f'{rec.score:.0f}')} {rec.engine:<12} {rec.model:<30}"
        print(f"{line} {metrics}")
        print(f"      {dim(rec.reason)} {conf_str} {src_str}")
        for caveat in rec.caveats:
            print(f"      {yellow('⚠')} {caveat}")

    return 0


def cmd_setup(args: argparse.Namespace) -> int:
    """Interactive first-launch setup wizard."""
    from asiai.collectors.system import collect_hw_chip, collect_memory
    from asiai.display.formatters import bold, dim, green, yellow

    print(bold("asiai setup wizard"))
    print()

    # Step 1: Hardware detection
    chip = collect_hw_chip()
    mem = collect_memory()
    ram_gb = round(mem.total / (1024**3))
    print(f"  {green('✓')} Hardware: {chip}, {ram_gb} GB RAM")

    # Step 2: Engine detection
    print()
    print(bold("Detecting inference engines..."))
    engines = _discover_engines()

    if engines:
        for e in engines:
            ver = e.version() or ""
            ver_str = f" v{ver}" if ver else ""
            models = e.list_running()
            model_str = f", {len(models)} model(s) loaded" if models else ""
            print(f"  {green('✓')} {e.name}{ver_str} at {e.base_url}{model_str}")
    else:
        print(f"  {yellow('⚠')} No inference engines detected.")
        print()
        print(bold("Install an engine to get started:"))
        print()
        print("  Ollama (recommended):")
        print("    brew install ollama && ollama serve")
        print("    ollama pull qwen3:8b")
        print()
        print("  LM Studio:")
        print("    Download from https://lmstudio.ai")
        print("    Load a model and start the server")
        print()
        print(dim("After installing, run 'asiai setup' again."))
        return 0

    # Step 3: Suggest first actions
    print()
    print(bold("You're all set! Try these commands:"))
    print()
    print(f"  {dim('Quick benchmark:')}    asiai bench --quick")
    print(f"  {dim('Full benchmark:')}     asiai bench")
    print(f"  {dim('Monitor live:')}       asiai monitor --watch 5")
    print(f"  {dim('Start dashboard:')}    asiai web")
    print(f"  {dim('Health check:')}       asiai doctor")
    print()

    # Step 4: Recommend starting the daemon
    from asiai.daemon import daemon_status_all

    statuses = daemon_status_all()
    monitor_running = statuses.get("monitor", {}).get("running", False)

    if not monitor_running:
        print(bold("Enable background monitoring?"))
        print(dim("  This collects metrics every 60s for historical analysis."))
        print(f"  Run: {green('asiai daemon start')}")
        print()

    return 0


def cmd_config(args: argparse.Namespace) -> int:
    """Handle 'config' command — manage persistent engine configuration."""
    from asiai.display.formatters import bold, dim, green, yellow
    from asiai.engines.config import (
        load_config,
        remove_engine,
        reset_config,
        upsert_engine,
    )

    action = getattr(args, "config_action", None)

    if action == "show":
        config = load_config()
        engines = config.get("engines", [])
        if not engines:
            print(dim("No engines in config. Run 'asiai detect' to discover engines."))
            return 0

        import time as _time

        print(bold(f"Known engines ({len(engines)}):"))
        for entry in engines:
            url = entry.get("url", "?")
            engine = entry.get("engine", "?")
            version = entry.get("version", "")
            source = entry.get("source", "auto")
            label = entry.get("label", "")
            last_seen = entry.get("last_seen", 0)

            age = int(_time.time()) - last_seen if last_seen else 0
            if age < 3600:
                ago = f"{age // 60}m ago"
            elif age < 86400:
                ago = f"{age // 3600}h ago"
            else:
                ago = f"{age // 86400}d ago"

            ver_str = f" v{version}" if version else ""
            label_str = f" [{label}]" if label else ""
            source_str = dim(f"({source})") if source == "auto" else green(f"({source})")

            print(f"  {engine}{ver_str} at {url}{label_str}  {source_str}  last seen {ago}")
        return 0

    if action == "add":
        engine = args.engine_name
        url = args.engine_url
        label = getattr(args, "label", "") or ""
        upsert_engine(url, engine, source="manual", label=label)
        print(green(f"Added {engine} at {url}"))
        return 0

    if action == "remove":
        url = args.engine_url
        if remove_engine(url):
            print(green(f"Removed {url}"))
        else:
            print(yellow(f"No engine found at {url}"))
        return 0

    if action == "reset":
        if reset_config():
            print(green("Config reset — all engines cleared."))
        else:
            print(dim("No config file to reset."))
        return 0

    # No action specified
    print(dim("Usage: asiai config {show,add,remove,reset}"))
    return 0


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        prog="asiai",
        description="Multi-engine LLM benchmark & monitoring CLI for Apple Silicon.",
    )
    parser.add_argument("--version", action="version", version=f"asiai {__version__}")
    parser.add_argument(
        "--url",
        metavar="URL",
        help="Inference server URL(s), comma-separated (default: auto-detect)",
    )

    subparsers = parser.add_subparsers(dest="command")

    # detect
    detect_parser = subparsers.add_parser("detect", help="Detect installed inference engines")
    detect_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")

    # models
    models_parser = subparsers.add_parser("models", help="List loaded models across engines")
    models_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")
    models_parser.add_argument(
        "--json", action="store_true", dest="json_output", help="Output as JSON"
    )

    # monitor
    monitor_parser = subparsers.add_parser(
        "monitor",
        help="Monitor system and inference metrics",
    )
    monitor_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")
    monitor_parser.add_argument("--db", metavar="PATH", help="SQLite database path")
    monitor_parser.add_argument(
        "--watch",
        "-w",
        type=int,
        metavar="SEC",
        help="Refresh every SEC seconds",
    )
    monitor_parser.add_argument(
        "--history",
        "-H",
        metavar="PERIOD",
        help="Show history (e.g. 24h, 1h)",
    )
    monitor_parser.add_argument(
        "--analyze",
        "-a",
        type=int,
        nargs="?",
        const=24,
        metavar="HOURS",
        help="Comprehensive analysis (default: 24h)",
    )
    monitor_parser.add_argument(
        "--compare",
        "-c",
        nargs=2,
        metavar="TS",
        help="Compare two timestamps",
    )
    monitor_parser.add_argument(
        "--quiet",
        "-q",
        action="store_true",
        help="Collect and store without output (for daemon use)",
    )
    monitor_parser.add_argument(
        "--json", action="store_true", dest="json_output", help="Output as JSON"
    )
    monitor_parser.add_argument(
        "--alert-webhook",
        metavar="URL",
        help="POST alerts to webhook URL on state transitions",
    )

    # doctor
    doctor_parser = subparsers.add_parser("doctor", help="Diagnose installation and environment")
    doctor_parser.add_argument("--db", metavar="PATH", help="SQLite database path")

    # daemon
    daemon_parser = subparsers.add_parser("daemon", help="Manage background services")
    daemon_sub = daemon_parser.add_subparsers(dest="action")
    daemon_start_p = daemon_sub.add_parser("start", help="Start a background service")
    daemon_start_p.add_argument(
        "service",
        nargs="?",
        default="monitor",
        choices=["monitor", "web"],
        help="Service to start (default: monitor)",
    )
    daemon_start_p.add_argument(
        "--interval",
        "-i",
        type=int,
        default=60,
        metavar="SEC",
        help="Monitor collection interval in seconds (default: 60)",
    )
    daemon_start_p.add_argument(
        "--port", type=int, default=8899, help="Web dashboard port (default: 8899)"
    )
    daemon_start_p.add_argument(
        "--host", default="127.0.0.1", help="Web dashboard host (default: 127.0.0.1)"
    )
    daemon_start_p.add_argument(
        "--alert-webhook",
        metavar="URL",
        help="POST alerts to webhook URL (monitor service only)",
    )
    daemon_stop_p = daemon_sub.add_parser("stop", help="Stop a background service")
    daemon_stop_p.add_argument(
        "service",
        nargs="?",
        default="monitor",
        choices=["monitor", "web"],
        help="Service to stop (default: monitor)",
    )
    daemon_stop_p.add_argument(
        "--all", action="store_true", dest="stop_all", help="Stop all services"
    )
    daemon_sub.add_parser("status", help="Show all service statuses")
    daemon_logs_p = daemon_sub.add_parser("logs", help="Show service logs")
    daemon_logs_p.add_argument(
        "service",
        nargs="?",
        default="monitor",
        choices=["monitor", "web"],
        help="Service to show logs for (default: monitor)",
    )
    daemon_logs_p.add_argument(
        "--lines",
        "-n",
        type=int,
        default=50,
        metavar="N",
        help="Number of log lines to show (default: 50)",
    )

    # tui
    tui_parser = subparsers.add_parser("tui", help="Interactive TUI dashboard (requires textual)")
    tui_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")
    tui_parser.add_argument("--db", metavar="PATH", help="SQLite database path")

    # web
    web_parser = subparsers.add_parser("web", help="Launch web dashboard (requires fastapi)")
    web_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")
    web_parser.add_argument("--db", metavar="PATH", help="SQLite database path")
    web_parser.add_argument("--port", type=int, default=8899, help="Port (default: 8899)")
    web_parser.add_argument("--host", default="127.0.0.1", help="Host (default: 127.0.0.1)")
    web_parser.add_argument(
        "--no-open", action="store_true", help="Don't open browser automatically"
    )

    # bench
    bench_parser = subparsers.add_parser("bench", help="Benchmark models across engines")
    bench_parser.add_argument("--url", metavar="URL", help="URL(s) to scan")
    bench_model_group = bench_parser.add_mutually_exclusive_group()
    bench_model_group.add_argument(
        "--model", "-m", help="Model to benchmark (default: auto-detect)"
    )
    bench_model_group.add_argument(
        "--compare",
        nargs="+",
        metavar="MODEL",
        help=(
            "Compare models across engines. Use model@engine for explicit slots "
            "(e.g. qwen3.5:4b@lmstudio deepseek-r1:7b@ollama) or model names "
            "with -e to expand across engines"
        ),
    )
    bench_parser.add_argument("--db", metavar="PATH", help="SQLite database path")
    bench_parser.add_argument(
        "--engines",
        "-e",
        metavar="LIST",
        help="Engines to benchmark, comma-separated (e.g. ollama,lmstudio)",
    )
    bench_parser.add_argument(
        "--prompts",
        "-p",
        metavar="LIST",
        help="Prompt types, comma-separated (code,tool_call,reasoning,long_gen)",
    )
    bench_parser.add_argument(
        "--runs",
        "-r",
        type=int,
        default=3,
        metavar="N",
        help="Number of runs per prompt for variance measurement (default: 3)",
    )
    bench_parser.add_argument(
        "--power",
        "-P",
        action="store_true",
        help="Measure GPU power consumption (requires sudo)",
    )
    bench_parser.add_argument(
        "--context-size",
        "-C",
        metavar="SIZE",
        help="Fill context with N tokens to stress-test TTFT (e.g. 64k, 128k, 4096)",
    )
    bench_parser.add_argument(
        "--export",
        "-E",
        metavar="FILE",
        help="Export results to JSON file (e.g. bench.json)",
    )
    bench_parser.add_argument(
        "--history",
        "-H",
        metavar="PERIOD",
        help="Show past benchmarks (e.g. 7d, 24h)",
    )
    bench_parser.add_argument(
        "--share",
        action="store_true",
        help="Share results to community benchmark database",
    )
    bench_parser.add_argument(
        "--share-on-error",
        action="store_true",
        help=(
            "Share even when all runs failed (median tok/s = 0). "
            "Off by default to avoid polluting the public leaderboard with "
            "useless 0-value entries."
        ),
    )
    bench_parser.add_argument(
        "--quick",
        "-Q",
        action="store_true",
        help="Quick benchmark: 1 prompt (code), 1 run (~15 seconds)",
    )
    bench_parser.add_argument(
        "--card",
        action="store_true",
        help="Generate a shareable benchmark card (SVG locally, PNG with --share)",
    )
    bench_parser.add_argument(
        "--kv-cache",
        metavar="TYPE",
        help=(
            "KV cache type (turbo2, turbo3, turbo4, q8_0, f16). "
            "Records the cache type used by the target engine."
        ),
    )
    bench_parser.add_argument(
        "--agentic-mode",
        action="store_true",
        help=(
            "Run the 8-run agentic protocol with explicit prefix cache reuse test "
            "(sys identical + user different). Reads cached_tokens from streaming "
            "usage when the engine exposes it; falls back to TTFT ratio otherwise. "
            "Disables --runs, --prompts, --quick, --compare, --share."
        ),
    )
    bench_parser.add_argument(
        "--agentic-output",
        metavar="FILE",
        help="Write agentic-mode results to JSON file (default: stdout summary only)",
    )
    bench_parser.add_argument(
        "--agentic-pause",
        type=float,
        default=2.0,
        metavar="SEC",
        help="Seconds between agentic runs (default: 2.0)",
    )
    bench_parser.add_argument(
        "--agentic-skip-long",
        action="store_true",
        help="Skip phases 7-8 (50K context). Saves ~10 min on slow engines.",
    )
    bench_parser.add_argument(
        "--agentic-only",
        metavar="LIST",
        help="Comma-separated phase names to run (cold,warm,prefix-test-1,...).",
    )
    bench_parser.add_argument(
        "--agentic-include-host",
        action="store_true",
        help=(
            "Include the machine hostname under ``host`` in the agentic output "
            "JSON. Off by default; turn on for personal records, leave off when "
            "sharing results publicly."
        ),
    )
    bench_parser.add_argument(
        "--agentic-auto-restart",
        action="store_true",
        help=(
            "Before the bench, call ``aisctl restart <engine>`` for a cold-start "
            "baseline. Requires aisctl on PATH and the engine to be managed by "
            "aisrv. Silently skipped (with a warning) otherwise. Useful for "
            "reproducible cold-load measurements on engines without an unload API."
        ),
    )
    bench_parser.add_argument(
        "--agentic-auto-restart-required",
        action="store_true",
        help=(
            "Pair with --agentic-auto-restart: abort the bench instead of "
            "proceeding when aisctl is unavailable or the restart fails. "
            "Use in CI to guarantee a clean cold start or fail fast."
        ),
    )

    # leaderboard
    leaderboard_parser = subparsers.add_parser(
        "leaderboard", help="Show community benchmark leaderboard"
    )
    leaderboard_parser.add_argument("--chip", help="Filter by chip (e.g. 'Apple M4 Pro')")
    leaderboard_parser.add_argument("--model", "-m", help="Filter by model name")

    # compare
    compare_parser = subparsers.add_parser(
        "compare", help="Compare your benchmarks against community data"
    )
    compare_parser.add_argument("--chip", help="Hardware chip (default: auto-detect)")
    compare_parser.add_argument("--model", "-m", help="Model to compare")
    compare_parser.add_argument("--db", metavar="PATH", help="SQLite database path")

    # mcp
    mcp_parser = subparsers.add_parser(
        "mcp", help="Start MCP server for Claude Code / Cursor / Windsurf"
    )
    mcp_parser.add_argument(
        "--transport",
        choices=["stdio", "sse", "streamable-http"],
        default="stdio",
        help="Transport protocol (default: stdio for Claude Code)",
    )
    mcp_parser.add_argument(
        "--port", type=int, default=8900, help="Port for SSE/HTTP transport (default: 8900)"
    )
    mcp_parser.add_argument("--host", default="127.0.0.1", help="Host for SSE/HTTP transport")
    mcp_parser.add_argument(
        "--register",
        action="store_true",
        help="Register with asiai agent network (opt-in, anonymous)",
    )

    # config
    config_parser = subparsers.add_parser("config", help="Manage persistent engine configuration")
    config_sub = config_parser.add_subparsers(dest="config_action")
    config_sub.add_parser("show", help="Show known engines")
    config_add_p = config_sub.add_parser("add", help="Add an engine manually")
    config_add_p.add_argument("engine_name", help="Engine type (ollama, lmstudio, omlx, ...)")
    config_add_p.add_argument("engine_url", help="Engine URL (e.g. http://localhost:8800)")
    config_add_p.add_argument("--label", help="Optional label (e.g. mac-mini)")
    config_remove_p = config_sub.add_parser("remove", help="Remove an engine by URL")
    config_remove_p.add_argument("engine_url", help="URL to remove")
    config_sub.add_parser("reset", help="Clear all engine configuration")

    # unregister
    subparsers.add_parser("unregister", help="Remove agent registration (delete local credentials)")

    # version (alias for --version)
    subparsers.add_parser("version", help="Show version")

    # setup
    subparsers.add_parser("setup", help="Interactive first-launch setup wizard")

    # recommend
    recommend_parser = subparsers.add_parser(
        "recommend", help="Get engine recommendations for your hardware"
    )
    recommend_parser.add_argument("--model", "-m", help="Filter by model name")
    recommend_parser.add_argument("--db", metavar="PATH", help="SQLite database path")
    recommend_parser.add_argument(
        "--use-case",
        choices=["throughput", "latency", "efficiency"],
        default="throughput",
        help="Optimize for (default: throughput)",
    )
    recommend_parser.add_argument(
        "--community",
        action="store_true",
        help="Include community data in recommendations",
    )

    # Discover sub-CLI plugins (skip for --version / --help to preserve cold-start).
    _fast_path = argv is not None and bool(set(argv[:2]) & {"--version", "--help", "-h", "version"})
    plugin_commands: dict = {}
    if not _fast_path:
        _load_subcommand_plugins(subparsers, plugin_commands)

    args = parser.parse_args(argv)

    if args.command is None:
        parser.print_help()
        return 0

    if args.command == "version":
        from asiai.collectors.system import collect_hw_chip, collect_memory
        from asiai.daemon import daemon_status_all
        from asiai.display.formatters import dim

        chip = collect_hw_chip()
        mem = collect_memory()
        ram_gb = round(mem.total / (1024**3))

        # Detect engines (fast, ~1s)
        engines = _discover_engines()
        engine_names = ", ".join(e.name for e in engines) if engines else "none"

        # Daemon status
        statuses = daemon_status_all()
        running_svcs = [name for name, s in statuses.items() if s.get("running")]
        daemon_str = ", ".join(running_svcs) if running_svcs else "stopped"

        print(f"asiai {__version__}")
        print(dim(f"  {chip}, {ram_gb} GB RAM"))
        print(dim(f"  Engines: {engine_names}"))
        print(dim(f"  Daemon: {daemon_str}"))

        # Agent network status
        from asiai.community import get_agent_info

        agent_info = get_agent_info()
        if agent_info.get("agent_id"):
            total = agent_info.get("total_agents", "?")
            print(dim(f"  Agent network: registered (#{total})"))

        return 0

    commands = {
        "detect": cmd_detect,
        "models": cmd_models,
        "monitor": cmd_monitor,
        "bench": cmd_bench,
        "doctor": cmd_doctor,
        "daemon": cmd_daemon,
        "tui": cmd_tui,
        "web": cmd_web,
        "mcp": cmd_mcp,
        "leaderboard": cmd_leaderboard,
        "compare": cmd_compare,
        "recommend": cmd_recommend,
        "unregister": cmd_unregister,
        "setup": cmd_setup,
        "config": cmd_config,
    }

    handler = commands.get(args.command) or plugin_commands.get(args.command)
    if handler:
        return handler(args)

    print(f"asiai {__version__} — command '{args.command}' not yet implemented.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
