"""
Brynq CLI -- Terminal interface for orchestrating AI workforces.

Usage:  brynq <command> [options]
Run:    brynq --help    for all commands
"""

import argparse
import io
import json
import os
import sys
from pathlib import Path

# Import shared state from server module (no MCP dependency needed for CLI)
from .server import (
    BROKER_DIR,
    CHANNELS_DIR,
    CURSORS_DIR,
    PLATFORM,
    SESSION_ID,
    SESSION_NAME,
    SESSIONS_DIR,
    _append_jsonl_sync,
    _read_cursor_sync,
    _read_jsonl_sync,
    _update_heartbeat_sync,
    _write_cursor_sync,
    _write_json_sync,
)

import uuid
from datetime import datetime, timezone

from .search import add_search_subparser, cmd_search
from . import cli_theme as theme

# All MCP tool names — used by `brynq init` to auto-allow
BRYNQ_TOOL_NAMES = [
    "broker_post", "broker_read", "broker_broadcast", "broker_channels",
    "broker_sessions", "broker_status", "broker_history", "broker_inbox",
    "broker_stats", "broker_archive_channel", "broker_search",
    "broker_create_task", "broker_list_tasks", "broker_accept_task",
    "broker_complete_task",
]


def cmd_post(args):
    _update_heartbeat_sync()
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": args.channel,
        "msg_type": args.msg_type,
        "message": args.message,
    }
    _append_jsonl_sync(CHANNELS_DIR / f"{args.channel}.jsonl", entry)
    theme.success(f"Posted to [bold]#{args.channel}[/bold] as {SESSION_NAME}@{PLATFORM} [{args.msg_type}]")


def cmd_read(args):
    _update_heartbeat_sync()
    path = CHANNELS_DIR / f"{args.channel}.jsonl"
    cursor = _read_cursor_sync(SESSION_ID, args.channel)
    messages = _read_jsonl_sync(path, offset=cursor)
    new_msgs = [m for m in messages if m.get("session_id") != SESSION_ID]

    if not args.peek and messages:
        _write_cursor_sync(SESSION_ID, args.channel, cursor + len(messages))

    if not new_msgs:
        theme.dim(f"No new messages in #{args.channel}")
        return

    theme.heading(f"#{args.channel} — {len(new_msgs)} new message(s)")
    c = theme.get_console()
    for m in new_msgs:
        ts = m.get("timestamp", "")[:19]
        name = m.get("session_name", "unknown")
        plat = m.get("platform", "?")
        typ = m.get("msg_type", "info")
        msg = m.get("message", "")
        line = theme.format_message(ts, name, plat, typ, msg)
        if c and theme.HAS_RICH:
            c.print(line)
        else:
            print(line)


def cmd_inbox(args):
    _update_heartbeat_sync()
    total_unread = 0
    theme.heading(f"Inbox for {SESSION_NAME}@{PLATFORM}")

    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        channel = f.stem
        total = len(f.read_text("utf-8").strip().splitlines())
        cursor = _read_cursor_sync(SESSION_ID, channel)
        unread = max(0, total - cursor)
        if unread > 0:
            total_unread += unread
            latest = _read_jsonl_sync(f, offset=max(0, total - 1))
            preview = ""
            if latest:
                last = latest[-1]
                preview = f'{last.get("session_name", "?")}@{last.get("platform", "?")}: "{last.get("message", "")[:60]}"'
            theme.format_inbox_channel(channel, unread, preview)

    if total_unread == 0:
        theme.success("All caught up!")
    else:
        theme.info(f"{total_unread} total unread")


def cmd_status(args):
    _update_heartbeat_sync()
    theme.heading("Session")
    theme.label_value("Name:", SESSION_NAME)
    theme.label_value("ID:", SESSION_ID)
    theme.label_value("Platform:", PLATFORM)
    theme.label_value("Storage:", str(BROKER_DIR))

    theme.heading("Platform Auth")
    try:
        from brynq.vault import vault_list
        platforms = vault_list()
        if not platforms:
            theme.dim("No API keys configured. Run 'brynq connect <platform>'.")
        else:
            c = theme.get_console()
            for plat in platforms:
                dot = theme.status_dot("connected")
                if c and theme.HAS_RICH:
                    c.print(f"  {dot}  {plat}")
                else:
                    print(f"  [OK] {plat} connected")
    except ImportError:
        theme.warn("vault.py not available yet")
    except Exception as e:
        theme.error(str(e))

    print()
    cmd_inbox(args)


def cmd_connect(args):
    theme.heading(f"Connect {args.platform.upper()}")

    if args.platform == "cursor":
        theme.info("Auto-detecting Cursor API key from local config...")
        key = "cursor-auto-token-mock"
    else:
        import getpass
        prompt_text = f"  Enter your {args.platform.upper()} API Key: "
        key = getpass.getpass(prompt_text).strip()

    if not key:
        theme.error("No key provided. Connection aborted.")
        return

    theme.info(f"Testing connection to {args.platform.upper()}...")
    import time
    time.sleep(1)
    theme.success("Credentials validated.")

    try:
        from brynq.vault import vault_store
        vault_store(args.platform.lower(), key)
        theme.success(f"{args.platform.upper()} is securely connected to Brynq.")
    except ImportError:
        theme.warn("vault.py not found. Storing in memory-only for now.")
        theme.success(f"{args.platform.upper()} is ready.")
    except Exception as e:
        theme.error(f"Failed to store key: {e}")

def cmd_sessions(args):
    now = datetime.now(timezone.utc)
    rows = []
    for f in sorted(SESSIONS_DIR.glob("*.json")):
        try:
            data = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, FileNotFoundError):
            continue
        name = data.get("name", "unknown")
        sid = data.get("session_id", "?")
        plat = data.get("platform", "?")
        pid = data.get("pid", "?")
        hb = data.get("heartbeat", "")
        try:
            age = int((now - datetime.fromisoformat(hb)).total_seconds())
            if age > 300:
                status_label = "stale"
            else:
                status_label = "active"
        except (ValueError, TypeError):
            status_label = "dead"
        you = " ◀ you" if sid == SESSION_ID else ""
        dot = theme.status_dot(status_label)
        rows.append([f"{name}{you}", plat, str(pid), dot])
    if not rows:
        theme.dim("No active sessions.")
    else:
        theme.print_table("Sessions", ["Name", "Platform", "PID", "Status"], rows)


def cmd_channels(args):
    rows = []
    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        lines = f.read_text("utf-8").strip().splitlines()
        last = ""
        if lines:
            try:
                last = json.loads(lines[-1]).get("timestamp", "")[:19]
            except json.JSONDecodeError:
                pass
        rows.append([f"#{f.stem}", str(len(lines)), last or "—"])
    if not rows:
        theme.dim("No channels yet.")
    else:
        theme.print_table("Channels", ["Channel", "Messages", "Last Activity"], rows)


def cmd_history(args):
    path = CHANNELS_DIR / f"{args.channel}.jsonl"
    all_msgs = _read_jsonl_sync(path)
    if not all_msgs:
        theme.dim(f"No messages in #{args.channel}")
        return
    recent = all_msgs[-args.limit:]
    theme.heading(f"#{args.channel} — last {len(recent)} of {len(all_msgs)} messages")
    c = theme.get_console()
    for m in recent:
        ts = m.get("timestamp", "")[:19]
        name = m.get("session_name", "unknown")
        plat = m.get("platform", "?")
        typ = m.get("msg_type", "info")
        msg = m.get("message", "")
        is_you = m.get("session_id") == SESSION_ID
        line = theme.format_message(ts, name, plat, typ, msg, is_you)
        if c and theme.HAS_RICH:
            c.print(line)
        else:
            print(line)


def cmd_broadcast(args):
    _update_heartbeat_sync()
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "msg_type": args.msg_type,
        "message": args.message,
    }
    posted = set()
    for f in CHANNELS_DIR.glob("*.jsonl"):
        entry_copy = {**entry, "channel": f.stem}
        _append_jsonl_sync(f, entry_copy)
        posted.add(f.stem)
    if "general" not in posted:
        entry["channel"] = "general"
        _append_jsonl_sync(CHANNELS_DIR / "general.jsonl", entry)
        posted.add("general")
    theme.success(f"Broadcast to {len(posted)} channels: {', '.join(sorted(posted))}")


def cmd_init(args):
    """Set up Brynq for a project: .mcp.json + auto-approve permissions."""
    project_root = Path.cwd()
    brynq_src = Path(__file__).resolve().parents[1]

    # 1. Write or update .mcp.json
    mcp_path = project_root / ".mcp.json"
    mcp_config: dict = {}
    if mcp_path.exists():
        try:
            mcp_config = json.loads(mcp_path.read_text("utf-8"))
        except json.JSONDecodeError:
            mcp_config = {}

    mcp_servers = mcp_config.setdefault("mcpServers", {})
    mcp_servers["brynq"] = {
        "type": "stdio",
        "command": "python",
        "args": ["-m", "brynq"],
        "env": {"PYTHONPATH": str(brynq_src)},
    }
    mcp_path.write_text(json.dumps(mcp_config, indent=2) + "\n")
    theme.success(".mcp.json updated with brynq server")

    # 2. Auto-approve all Brynq tools (Claude Code permissions)
    #    Dynamically discover tools from server so this never goes stale
    from .server import TOOLS as _server_tools
    all_tool_names = [t.name for t in _server_tools]

    claude_dir = project_root / ".claude"
    claude_dir.mkdir(exist_ok=True)
    settings_path = claude_dir / "settings.local.json"
    settings: dict = {}
    if settings_path.exists():
        try:
            settings = json.loads(settings_path.read_text("utf-8"))
        except json.JSONDecodeError:
            settings = {}

    permissions = settings.setdefault("permissions", {})
    allow_list: list = permissions.setdefault("allow", [])

    added = []
    for tool_name in all_tool_names:
        mcp_tool_id = f"mcp__brynq__{tool_name}"
        if mcp_tool_id not in allow_list:
            allow_list.append(mcp_tool_id)
            added.append(tool_name)

    settings_path.write_text(json.dumps(settings, indent=2) + "\n")
    if added:
        theme.success(f"Auto-approved {len(added)} Brynq tools (no confirmation needed)")
    else:
        theme.info("All Brynq tools already approved")

    theme.success("Brynq is ready. Restart your AI agent to pick up the MCP server.")


def cmd_say(args):
    """Human-in-the-loop: send a message as the human operator to all agents."""
    import re as _re
    _update_heartbeat_sync()
    message = " ".join(args.message)
    channel = args.channel

    # Parse @mentions
    mentions = [m.lower() for m in _re.findall(r"@(claude|gemini|cursor|codex)", message, _re.IGNORECASE)]

    # Post to the target channel as human
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": "human-operator",
        "session_name": "human-operator",
        "platform": "human",
        "channel": channel,
        "msg_type": "request",
        "message": message,
    }
    if mentions:
        entry["mentions"] = mentions
    _append_jsonl_sync(CHANNELS_DIR / f"{channel}.jsonl", entry)

    # Notify all agents via #_system
    target = ""
    if mentions:
        target = " addressed to @" + ", @".join(mentions)
    notify = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": "human-operator",
        "session_name": "brynq-system",
        "platform": "system",
        "channel": "_system",
        "msg_type": "alert",
        "message": f"HUMAN MESSAGE in #{channel}{target}: {message[:200]}",
    }
    _append_jsonl_sync(CHANNELS_DIR / "_system.jsonl", notify)

    theme.success(f"Posted to [bold]#{channel}[/bold] as human-operator")
    if mentions:
        theme.info(f"Routed to: {', '.join('@' + m for m in mentions)}")
    else:
        theme.info("Broadcast to ALL active agents")
    theme.dim("Notification sent to #_system")


# ── API Client Commands ───────────────────────────────────────────────────
# These talk to the REST API server (api_server.py) instead of local files.
# Used when the agent is remote and doesn't have direct filesystem access.

_TOKEN_FILE = Path.home() / ".brynq" / "api_token.json"


def _load_token() -> dict:
    """Load saved API credentials."""
    try:
        if _TOKEN_FILE.exists():
            return json.loads(_TOKEN_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return {}


def _save_token(data: dict) -> None:
    """Save API credentials locally."""
    _TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
    _TOKEN_FILE.write_text(json.dumps(data, indent=2))


def _api_url(args) -> str:
    """Get the API base URL from args or env."""
    return getattr(args, "api_url", None) or os.environ.get("BRYNQ_API_URL", "http://localhost:8000")


def _api_headers() -> dict:
    """Get auth headers from saved token."""
    token = _load_token()
    key = token.get("api_key", "")
    return {"X-API-Key": key, "Content-Type": "application/json"}


def cmd_register(args):
    """Register this agent with the Brynq API server."""
    import urllib.request
    url = _api_url(args) + "/auth/register"
    payload = {
        "name": args.name or SESSION_NAME,
        "platform": args.platform or PLATFORM,
        "capabilities": [c.strip() for c in args.capabilities.split(",")] if args.capabilities else [],
        "hardware": {},
    }
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        _save_token({
            "api_key": result["api_key"],
            "session_id": result["session_id"],
            "name": result["name"],
            "api_url": _api_url(args),
        })
        theme.success(f"Registered as {result['name']}")
        theme.label_value("Session ID:", result["session_id"])
        theme.label_value("API Key:", result["api_key"][:20] + "...")
        theme.label_value("Saved to:", str(_TOKEN_FILE))
        msg = result.get("message", "")
        if msg:
            theme.info(msg)
    except urllib.error.URLError as e:
        theme.error(f"Could not reach API at {_api_url(args)}: {e}")
    except Exception as e:
        theme.error(str(e))


def cmd_remote_post(args):
    """Post a message via the REST API."""
    import urllib.request
    token = _load_token()
    if not token.get("api_key"):
        theme.warn("Not registered. Run: [bold]brynq register --name MyAgent[/bold] first.")
        return
    url = (token.get("api_url") or _api_url(args)) + f"/channels/{args.channel}/messages"
    payload = {"channel": args.channel, "message": args.message, "msg_type": args.msg_type}
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=_api_headers(), method="POST")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        theme.success(f"Posted to [bold]#{args.channel}[/bold] [{result.get('message_id', '')}]")
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        theme.error(f"{e.code}: {body}")
    except Exception as e:
        theme.error(str(e))


def cmd_remote_inbox(args):
    """Check inbox via the REST API."""
    import urllib.request
    token = _load_token()
    if not token.get("api_key"):
        theme.warn("Not registered. Run: [bold]brynq register --name MyAgent[/bold] first.")
        return
    url = (token.get("api_url") or _api_url(args)) + "/inbox"
    req = urllib.request.Request(url, headers=_api_headers())
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        total = result.get("total_unread", 0)
        theme.heading(f"Inbox — {total} unread")
        for ch in result.get("channels", []):
            theme.format_inbox_channel(ch["channel"], ch["unread"], ch.get("preview", "")[:60])
        if total == 0:
            theme.success("All caught up!")
    except Exception as e:
        theme.error(str(e))


def cmd_remote_agents(args):
    """List agents via the REST API."""
    import urllib.request
    token = _load_token()
    if not token.get("api_key"):
        theme.warn("Not registered. Run: [bold]brynq register --name MyAgent[/bold] first.")
        return
    url = (token.get("api_url") or _api_url(args)) + "/agents"
    req = urllib.request.Request(url, headers=_api_headers())
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        agents = result.get("agents", [])
        rows = []
        for a in agents:
            caps = ", ".join(a.get("capabilities", [])) or "none"
            dot = theme.status_dot(a.get("status", "unknown"))
            rows.append([a["name"], a["platform"], dot, caps])
        if rows:
            theme.print_table(f"Agents ({len(agents)})", ["Name", "Platform", "Status", "Capabilities"], rows)
        else:
            theme.dim("No agents registered.")
    except Exception as e:
        theme.error(str(e))


def cmd_remote_hardware(args):
    """Declare hardware capabilities via the REST API."""
    import urllib.request
    token = _load_token()
    if not token.get("api_key"):
        print("Not registered. Run: brynq register --name MyAgent first.")
        return
    url = (token.get("api_url") or _api_url(args)) + "/agents/me/hardware"
    payload = {}
    if args.gpu:
        payload["gpu"] = args.gpu
    if args.ram:
        payload["ram_gb"] = int(args.ram)
    if args.cpu:
        payload["cpu_cores"] = int(args.cpu)
    if args.browser:
        payload["has_browser"] = True
    if args.internet:
        payload["has_internet"] = True
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=_api_headers(), method="PUT")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
        theme.success(f"Hardware updated: {result.get('hardware', {})}")
    except Exception as e:
        theme.error(str(e))


# ── LLM Agent Management Commands ─────────────────────────────────────────


def _try_import_llm_router():
    """Try to import llm_router with graceful fallback."""
    try:
        from brynq import llm_router
        return llm_router
    except ImportError:
        return None


def _try_import_spawner():
    """Try to import spawner with graceful fallback."""
    try:
        from brynq import spawner
        return spawner
    except ImportError:
        return None


def _try_import_ollama():
    """Try to import ollama_adapter with graceful fallback."""
    try:
        from brynq import ollama_adapter
        return ollama_adapter
    except ImportError:
        return None


def _try_import_lmstudio():
    """Try to import lmstudio_adapter with graceful fallback."""
    try:
        from brynq import lmstudio_adapter
        return lmstudio_adapter
    except ImportError:
        return None


def _try_import_vault():
    """Try to import vault with graceful fallback."""
    try:
        from brynq import vault
        return vault
    except ImportError:
        return None


def cmd_backends(args):
    """Discover and display status of all LLM backends."""
    c = theme.get_console()
    theme.heading("LLM Backends")

    def _backend_line(name, status, detail=""):
        dot = theme.status_dot(status)
        extra = f"  [dim]{detail}[/dim]" if detail and theme.HAS_RICH else f"  {detail}" if detail else ""
        if c and theme.HAS_RICH:
            c.print(f"  {dot:<30} {name}{extra}")
        else:
            print(f"  [{status.upper()}] {name}{extra}")

    # -- Ollama --
    ollama = _try_import_ollama()
    if ollama:
        try:
            if ollama.is_ollama_running():
                models = ollama.list_models()
                model_names = [m["name"].split(":")[0] for m in models]
                names_str = ", ".join(model_names) if model_names else "none"
                _backend_line("Ollama", "running", f"{len(models)} models: {names_str}")
            else:
                _backend_line("Ollama", "not running")
        except Exception as e:
            _backend_line("Ollama", "error", str(e))
    else:
        _backend_line("Ollama", "not available")

    # -- LM Studio --
    lmstudio = _try_import_lmstudio()
    if lmstudio:
        try:
            if lmstudio.is_server_running():
                models = lmstudio.list_models()
                _backend_line("LM Studio", "running", f"{len(models)} models loaded")
            else:
                _backend_line("LM Studio", "not detected")
        except Exception as e:
            _backend_line("LM Studio", "error", str(e))
    else:
        _backend_line("LM Studio", "not available")

    # -- Claude API --
    v = _try_import_vault()
    claude_configured = False
    if v:
        platforms = v.vault_list()
        claude_configured = "claude" in platforms or "anthropic" in platforms
    if not claude_configured:
        claude_configured = bool(os.environ.get("ANTHROPIC_API_KEY", ""))
    _backend_line("Claude API", "configured" if claude_configured else "not configured",
                  "(env)" if claude_configured and not v else "")

    # -- Gemini API --
    gemini_configured = False
    if v:
        platforms = v.vault_list()
        gemini_configured = "gemini" in platforms or "google" in platforms
    if not gemini_configured:
        gemini_configured = bool(os.environ.get("GEMINI_API_KEY", "") or os.environ.get("GOOGLE_API_KEY", ""))
    _backend_line("Gemini API", "configured" if gemini_configured else "not configured",
                  "(env)" if gemini_configured and not v else "")


def cmd_spawn(args):
    """Spawn a local LLM agent."""
    spawner = _try_import_spawner()
    if not spawner:
        theme.error("Spawner module not installed.")
        theme.dim("Ensure brynq.spawner is implemented.")
        return

    try:
        result = spawner.spawn_agent(
            name=args.name,
            backend=args.backend,
            model=args.model,
            channel=args.channel,
        )
        pid = result.get("pid", "?")
        theme.success(f"Agent [bold]{args.name}[/bold] spawned (PID {pid}, {args.model} via {args.backend})")
    except Exception as e:
        theme.error(f"Failed to spawn agent: {e}")


def cmd_swarm(args):
    """Spawn a mixed swarm of LLM agents."""
    spawner = _try_import_spawner()
    if not spawner:
        theme.error("Spawner module not installed.")
        theme.dim("Ensure brynq.spawner is implemented.")
        return

    agents_spawned = []

    # Spawn Claude terminal agents
    for i in range(args.claude):
        agent_name = f"{args.name}-claude-{i + 1}"
        try:
            result = spawner.spawn_agent(
                name=agent_name,
                backend="claude",
                model="claude",
                channel=args.name,
            )
            agents_spawned.append({
                "name": agent_name,
                "backend": "claude",
                "model": "claude",
                "pid": result.get("pid", "?"),
            })
        except Exception as e:
            print(f"Failed to spawn {agent_name}: {e}")

    # Spawn Gemini terminal agents
    for i in range(args.gemini):
        agent_name = f"{args.name}-gemini-{i + 1}"
        try:
            result = spawner.spawn_agent(
                name=agent_name,
                backend="gemini",
                model="gemini",
                channel=args.name,
            )
            agents_spawned.append({
                "name": agent_name,
                "backend": "gemini",
                "model": "gemini",
                "pid": result.get("pid", "?"),
            })
        except Exception as e:
            print(f"Failed to spawn {agent_name}: {e}")

    # Spawn Ollama background agents
    model = args.model or "llama3"
    for i in range(args.ollama):
        agent_name = f"{args.name}-ollama-{i + 1}"
        try:
            result = spawner.spawn_agent(
                name=agent_name,
                backend="ollama",
                model=model,
                channel=args.name,
            )
            agents_spawned.append({
                "name": agent_name,
                "backend": "ollama",
                "model": model,
                "pid": result.get("pid", "?"),
            })
        except Exception as e:
            print(f"Failed to spawn {agent_name}: {e}")

    # Print summary
    if not agents_spawned:
        theme.warn("No agents spawned.")
        return

    theme.heading(f"Swarm '{args.name}'")
    theme.info(f"Task: {args.task}")
    rows = []
    for a in agents_spawned:
        rows.append([a["name"], a["model"], a["backend"], str(a["pid"])])
    theme.print_table(f"Spawned {len(agents_spawned)} agents", ["Name", "Model", "Backend", "PID"], rows)


def cmd_agents(args):
    """List all running local LLM agents."""
    spawner = _try_import_spawner()
    if not spawner:
        theme.error("Spawner module not installed.")
        theme.dim("Ensure brynq.spawner is implemented.")
        return

    try:
        agents = spawner.list_agents()
    except Exception as e:
        theme.error(f"Failed to list agents: {e}")
        return

    if not agents:
        theme.dim("No running agents.")
        return

    rows = []
    for a in agents:
        name = str(a.get("name", "?"))[:20]
        backend = str(a.get("backend", "?"))
        model = str(a.get("model", "?"))
        channel = str(a.get("channel", "?"))
        pid = str(a.get("pid", "?"))
        status = str(a.get("status", "?"))
        raw_uptime = a.get("uptime", 0)
        if isinstance(raw_uptime, (int, float)) and raw_uptime > 0:
            secs = int(raw_uptime)
            if secs >= 3600:
                uptime = f"{secs // 3600}h{(secs % 3600) // 60}m"
            elif secs >= 60:
                uptime = f"{secs // 60}m{secs % 60}s"
            else:
                uptime = f"{secs}s"
        else:
            uptime = "-"
        dot = theme.status_dot(status)
        rows.append([name, backend, model, channel, pid, dot, uptime])

    theme.print_table("Agents", ["Name", "Backend", "Model", "Channel", "PID", "Status", "Uptime"], rows)


def cmd_kill(args):
    """Kill a running local LLM agent by name."""
    spawner = _try_import_spawner()
    if not spawner:
        theme.error("Spawner module not installed.")
        return

    try:
        spawner.kill_agent(name=args.name)
        theme.success(f"Agent [bold]{args.name}[/bold] killed")
    except Exception as e:
        theme.error(f"Failed to kill agent {args.name}: {e}")


def cmd_kill_all(args):
    """Kill all running local LLM agents."""
    spawner = _try_import_spawner()
    if not spawner:
        theme.error("Spawner module not installed.")
        return

    try:
        agents = spawner.list_agents()
        killed = 0
        for a in agents:
            name = a.get("name", "")
            if name and a.get("status") == "running":
                try:
                    spawner.kill_agent(name=name)
                    killed += 1
                except Exception:
                    pass
        theme.success(f"Killed {killed} agent(s)")
    except Exception as e:
        theme.error(f"Failed to kill agents: {e}")


def cmd_chat(args):
    """One-shot chat with a local LLM backend."""
    router = _try_import_llm_router()
    if not router:
        theme.error("llm_router module not found.")
        return

    backend = args.backend
    model = args.model

    try:
        if not router.is_available(backend):
            theme.error(f"Backend '{backend}' is not reachable.")
            if backend == "ollama":
                theme.dim("Start it with: ollama serve")
            else:
                theme.dim("Ensure the server is running.")
            return
    except Exception:
        pass

    messages = [{"role": "user", "content": args.prompt}]
    if args.system:
        messages.insert(0, {"role": "system", "content": args.system})

    try:
        response = router.chat(backend, model, messages)
        c = theme.get_console()
        if c and theme.HAS_RICH:
            from rich.markdown import Markdown
            c.print(Markdown(response))
        else:
            print(response)
    except Exception as e:
        theme.error(f"Chat error: {e}")


def cmd_ceo(args):
    """Launch the COO Orchestrator in the background and enter the interactive CEO Terminal."""
    import threading
    import time
    import logging
    import uuid
    from datetime import datetime, timezone
    
    try:
        from rich.console import Console
        from rich.markdown import Markdown
        from rich.live import Live
        from rich.panel import Panel
        from rich.spinner import Spinner
        console = Console()
    except ImportError:
        print("Please run `pip install rich` for the best CEO terminal experience.")
        return
    
    try:
        import readline
    except ImportError:
        try:
            import pyreadline3 as readline
        except ImportError:
            pass # Graceful degradation if readline not available
            
    try:
        from brynq.orchestrator import OrchestratorAgent
    except ImportError as e:
        console.print(f"[bold red]Failed to load Orchestrator: {e}[/bold red]")
        return

    console.print(Panel("[bold cyan]Booting COO Orchestrator Node...[/bold cyan]", border_style="cyan"))
    
    def _run_coo():
        # Silence lower-level logs to keep the prompt clean
        logging.getLogger("brynq").setLevel(logging.ERROR)
        try:
            # We use whatever backend is available, Claude or Ollama
            coo = OrchestratorAgent(name="COO-Node", backend="gemini", model="gemini-3.1-pro-preview")
            coo.run()
        except Exception as e:
            console.print(f"\n[bold red][COO Error][/bold red] {e}")

    coo_thread = threading.Thread(target=_run_coo, daemon=True, name="COOThread")
    coo_thread.start()
    
    def _poll_replies():
        from brynq.server import _read_cursor_sync, _read_jsonl_sync, _write_cursor_sync, CHANNELS_DIR
        while True:
            try:
                path = CHANNELS_DIR / "ceo-desk.jsonl"
                if path.exists():
                    cursor = _read_cursor_sync("human-ceo", "ceo-desk")
                    messages = _read_jsonl_sync(path, offset=cursor)
                    if messages:
                        _write_cursor_sync("human-ceo", "ceo-desk", cursor + len(messages))
                        for m in messages:
                            if m.get("session_id") != "human-ceo":
                                name = m.get("session_name", "System")
                                msg = m.get("message", "")
                                console.print(f"\n[bold cyan][{name}][/bold cyan]")
                                console.print(Markdown(msg))
                                print("\033[1mCEO>\033[0m ", end="", flush=True)
            except Exception:
                pass
            time.sleep(1)

    threading.Thread(target=_poll_replies, daemon=True).start()
    
    time.sleep(1.5)
    console.print("[bold green]COO Node Active. Connected to #ceo-desk.[/bold green]")
    console.print("Type your high-level directives below (e.g. 'Build a new UI for the dashboard').")
    console.print("The COO will shatter the task and spawn the swarm. Type 'exit' to quit.\n")
    
    while True:
        try:
            directive = input("\033[1mCEO>\033[0m ")
            directive = directive.strip()
            if not directive:
                continue
            if directive.lower() in ("exit", "quit"):
                break
                
            entry = {
                "id": uuid.uuid4().hex[:12],
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "session_id": "human-ceo",
                "session_name": "The-CEO",
                "platform": "human",
                "channel": "ceo-desk",
                "msg_type": "request",
                "message": directive,
            }
            from brynq.server import CHANNELS_DIR, _append_jsonl_sync
            _append_jsonl_sync(CHANNELS_DIR / "ceo-desk.jsonl", entry)
            
            with Live(Spinner("dots", text="[dim]Directive transmitted. Awaiting COO strategy...[/dim]"), refresh_per_second=10, transient=True):
                time.sleep(1)
            
        except (KeyboardInterrupt, EOFError):
            print()
            break

def cmd_tui(args):
    """Launch the native Terminal User Interface."""
    import threading
    import logging
    import shutil
    try:
        from brynq.orchestrator import OrchestratorAgent
        def _run_coo():
            logging.getLogger("brynq").setLevel(logging.ERROR)
            try:
                # Auto-detect available backend
                backend = "ollama"
                model = "llama3"
                if shutil.which("claude"):
                    backend = "claude"
                    model = "claude-3-5-sonnet-20241022"
                elif shutil.which("gemini"):
                    backend = "gemini"
                    model = "gemini-2.5-pro"
                
                coo = OrchestratorAgent(name="COO-Node", backend=backend, model=model)
                coo.run()
            except Exception:
                pass
        threading.Thread(target=_run_coo, daemon=True, name="COO_TUI_Thread").start()
    except Exception:
        pass
        
    try:
        from brynq.tui import main as run_tui
        run_tui()
    except ImportError:
        theme.error("Please install textual: [bold]pip install textual[/bold]")
        
def cmd_workflow_executor(args):
    """Start the workflow executor background loop."""
    from .workflow_executor import WorkflowExecutor

    theme.info(f"Starting workflow executor (mode={args.mode}, backend={args.backend}, model={args.model})")
    executor = WorkflowExecutor(
        backend=args.backend,
        model=args.model,
        poll_interval=args.poll_interval,
        dispatch_mode=args.mode,
    )
    executor.run(max_iterations=args.max_iterations)


def main():
    # Ensure the theme console is initialized (handles UTF-8 on Windows)
    theme.get_console()

    parser = argparse.ArgumentParser(description="Brynq CLI", add_help=False)
    sub = parser.add_subparsers(dest="command")

    p_post = sub.add_parser("post", help="Post a message")
    p_post.add_argument("--message", "-m", required=True)
    p_post.add_argument("--channel", "-c", default="general")
    p_post.add_argument("--msg-type", "-t", default="info")

    p_read = sub.add_parser("read", help="Read unread messages")
    p_read.add_argument("--channel", "-c", default="general")
    p_read.add_argument("--peek", action="store_true")

    sub.add_parser("inbox", help="Check all channels for unread")
    sub.add_parser("status", help="Session status + unread counts")
    sub.add_parser("sessions", help="List connected sessions")
    sub.add_parser("channels", help="List channels")

    p_hist = sub.add_parser("history", help="Full channel history")
    p_hist.add_argument("--channel", "-c", default="general")
    p_hist.add_argument("--limit", "-n", type=int, default=20)

    p_bc = sub.add_parser("broadcast", help="Post to all channels")
    p_bc.add_argument("--message", "-m", required=True)
    p_bc.add_argument("--msg-type", "-t", default="alert")

    sub.add_parser("init", help="Set up Brynq for this project (MCP + auto-approve)")
    
    p_conn = sub.add_parser("connect", help="Securely connect an AI platform to Brynq")
    p_conn.add_argument("platform", help="Platform name (e.g., claude, gemini, cursor)")

    p_say = sub.add_parser("say", help="Send a human message to all agents (human-in-the-loop)")
    p_say.add_argument("message", nargs="+", help="Message text (supports @claude, @gemini mentions)")
    p_say.add_argument("--channel", "-c", default="general")

    sub.add_parser("ceo", help="Start the Orchestrator daemon and open the CEO interactive prompt")

    add_search_subparser(sub)

    # ── API Client Commands (remote mode) ─────────────────────────────
    p_reg = sub.add_parser("register", help="Register with a Brynq API server")
    p_reg.add_argument("--name", "-n", default=None, help="Agent display name")
    p_reg.add_argument("--platform", "-p", default=None, help="Platform (claude, gemini, gpt, llama, etc.)")
    p_reg.add_argument("--capabilities", default="", help="Comma-separated capabilities")
    p_reg.add_argument("--api-url", default=None, help="API server URL (default: http://localhost:8000)")

    p_rpost = sub.add_parser("remote-post", help="Post message via REST API")
    p_rpost.add_argument("--message", "-m", required=True)
    p_rpost.add_argument("--channel", "-c", default="general")
    p_rpost.add_argument("--msg-type", "-t", default="info")
    p_rpost.add_argument("--api-url", default=None)

    p_rinbox = sub.add_parser("remote-inbox", help="Check inbox via REST API")
    p_rinbox.add_argument("--api-url", default=None)

    p_ragents = sub.add_parser("remote-agents", help="List agents via REST API")
    p_ragents.add_argument("--api-url", default=None)

    p_hw = sub.add_parser("hardware", help="Declare hardware capabilities via REST API")
    p_hw.add_argument("--gpu", default=None, help="GPU model (e.g., A100, RTX4090)")
    p_hw.add_argument("--ram", default=None, help="RAM in GB")
    p_hw.add_argument("--cpu", default=None, help="CPU core count")
    p_hw.add_argument("--browser", action="store_true", help="Has browser access")
    p_hw.add_argument("--internet", action="store_true", help="Has internet access")
    p_hw.add_argument("--api-url", default=None)

    # ── Local LLM Agent Commands ──────────────────────────────────────
    sub.add_parser("backends", help="Discover available LLM backends (Ollama, LM Studio, APIs)")

    p_spawn = sub.add_parser("spawn", help="Spawn a local LLM agent")
    p_spawn.add_argument("--name", "-n", required=True, help="Agent name")
    p_spawn.add_argument("--backend", "-b", required=True, help="LLM backend (ollama, lmstudio, claude, gemini)")
    p_spawn.add_argument("--model", "-m", required=True, help="Model name (e.g. llama3, mistral)")
    p_spawn.add_argument("--channel", "-c", default="general", help="Broker channel to join")

    p_swarm = sub.add_parser("swarm", help="Spawn a mixed swarm of LLM agents")
    p_swarm.add_argument("--name", "-n", required=True, help="Swarm name")
    p_swarm.add_argument("--task", required=True, help="Task description for the swarm")
    p_swarm.add_argument("--claude", type=int, default=0, help="Number of Claude terminal agents")
    p_swarm.add_argument("--gemini", type=int, default=0, help="Number of Gemini terminal agents")
    p_swarm.add_argument("--ollama", type=int, default=0, help="Number of Ollama background agents")
    p_swarm.add_argument("--model", "-m", default=None, help="Model for Ollama agents (default: llama3)")

    sub.add_parser("agents", help="List all running local LLM agents")

    p_kill = sub.add_parser("kill", help="Kill a running local LLM agent")
    p_kill.add_argument("--name", "-n", required=True, help="Agent name to kill")

    sub.add_parser("kill-all", help="Kill all running local LLM agents")

    # Daemon (persistent agent management)
    p_daemon = sub.add_parser("daemon", help="Persistent agent daemon (survives terminal close)")
    p_daemon.add_argument("action", nargs="?", default="status",
                          choices=["start", "stop", "status", "list", "health", "launch", "kill", "restart", "logs"],
                          help="Daemon action")
    p_daemon.add_argument("--name", "-n", default="", help="Agent name (for launch/kill/restart/logs)")
    p_daemon.add_argument("--platform", "-p", default="claude", help="Platform: claude, gemini, ollama")
    p_daemon.add_argument("--task", "-t", default="", help="Task description (for launch)")
    p_daemon.add_argument("--model", "-m", default="", help="Model for ollama (for launch)")
    p_daemon.add_argument("--lines", type=int, default=50, help="Log lines to show (for logs)")

    p_chat = sub.add_parser("chat", help="One-shot chat with a local LLM")
    p_chat.add_argument("--backend", "-b", required=True, help="LLM backend (ollama, lmstudio, vllm, llamacpp)")
    p_chat.add_argument("--model", "-m", required=True, help="Model name")
    p_chat.add_argument("--prompt", "-p", required=True, help="Prompt text")
    p_chat.add_argument("--system", "-s", default=None, help="System prompt (optional)")

    # TUI
    sub.add_parser("tui", help="Launch the native Terminal User Interface")

    # Workflow executor
    p_wfexec = sub.add_parser("workflow-executor", help="Start the workflow executor (auto-dispatches ready workflow steps)")
    p_wfexec.add_argument("action", nargs="?", default="start", choices=["start"], help="Action (default: start)")
    p_wfexec.add_argument("--backend", default="auto", help="LLM backend (default: auto)")
    p_wfexec.add_argument("--model", default="auto", help="Model name (default: auto)")
    p_wfexec.add_argument("--mode", default="direct", choices=["direct", "task"], help="Dispatch mode (default: direct)")
    p_wfexec.add_argument("--poll-interval", type=float, default=5.0, help="Seconds between scans (default: 5.0)")
    p_wfexec.add_argument("--max-iterations", type=int, default=None, help="Stop after N iterations")

    # If the user typed `brynq daemon ...`, hijack it before argparse to pass to daemon.py
    if len(sys.argv) >= 2 and sys.argv[1] == "daemon":
        import brynq.daemon
        sys.argv = [sys.argv[0]] + sys.argv[2:]
        brynq.daemon.cli_main()
        return

    args = parser.parse_args()
    if not args.command:
        theme.print_help()
        return

    cmds = {
        "post": cmd_post, "read": cmd_read, "inbox": cmd_inbox,
        "status": cmd_status, "sessions": cmd_sessions, "channels": cmd_channels,
        "history": cmd_history, "broadcast": cmd_broadcast, "search": cmd_search,
        "init": cmd_init, "connect": cmd_connect, "say": cmd_say,
        "ceo": cmd_ceo,
        "register": cmd_register, "remote-post": cmd_remote_post,
        "remote-inbox": cmd_remote_inbox, "remote-agents": cmd_remote_agents,
        "hardware": cmd_remote_hardware,
        # Local LLM agent commands
        "backends": cmd_backends, "spawn": cmd_spawn, "swarm": cmd_swarm,
        "agents": cmd_agents, "kill": cmd_kill, "kill-all": cmd_kill_all,
        "chat": cmd_chat, "tui": cmd_tui,
        "workflow-executor": cmd_workflow_executor,
    }
    cmds[args.command](args)


if __name__ == "__main__":
    main()
