"""
Brynq Runtime CLI — The terminal launcher for Brynq.

This is how users launch and interact with Brynq from their terminal.
Clean, fast, minimal — like ``claude`` for Claude Code.

Usage:
    brynq-runtime start              Start runtime + open browser
    brynq-runtime login claude       OAuth login for Claude
    brynq-runtime login gemini       OAuth login for Gemini
    brynq-runtime models             List available models
    brynq-runtime chat               Interactive terminal chat
    brynq-runtime chain "prompt"     Run a multi-model chain
    brynq-runtime status             Show runtime status
    brynq-runtime stop               Stop runtime

Environment:
    BRYNQ_RUNTIME_LOCAL_PORT    Runtime server port (default: 8003)
    BRYNQ_RUNTIME_OLLAMA_URL    Ollama URL (default: http://localhost:11434)
    BRYNQ_RUNTIME_DATA_DIR      Data directory (default: ~/.brynq)
"""

from __future__ import annotations

import argparse
import io
import json
import os
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request
import webbrowser
from pathlib import Path
from typing import Any

from runtime.config import RuntimeConfig, load_config

# ── Constants ─────────────────────────────────────────────────────────

VERSION = "0.1.0"
APP_URL = "http://localhost:3000"
_PID_FILENAME = "runtime.pid"

# ANSI colors (disabled on Windows without VT100 support)
_NO_COLOR = os.environ.get("NO_COLOR", "")


def _supports_color() -> bool:
    """Detect whether the terminal supports ANSI color."""
    if _NO_COLOR:
        return False
    if sys.platform == "win32":
        return os.environ.get("WT_SESSION") is not None or "ANSICON" in os.environ
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


_COLOR = _supports_color()


def _dim(text: str) -> str:
    return f"\033[2m{text}\033[0m" if _COLOR else text


def _bold(text: str) -> str:
    return f"\033[1m{text}\033[0m" if _COLOR else text


def _green(text: str) -> str:
    return f"\033[32m{text}\033[0m" if _COLOR else text


def _red(text: str) -> str:
    return f"\033[31m{text}\033[0m" if _COLOR else text


def _yellow(text: str) -> str:
    return f"\033[33m{text}\033[0m" if _COLOR else text


def _cyan(text: str) -> str:
    return f"\033[36m{text}\033[0m" if _COLOR else text


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


def _pid_file(config: RuntimeConfig) -> Path:
    return config.data_dir / _PID_FILENAME


def _is_runtime_running(config: RuntimeConfig) -> bool:
    """Check if the runtime server is responding on its port."""
    try:
        url = f"http://127.0.0.1:{config.local_port}/status"
        req = urllib.request.Request(url, method="GET")
        with urllib.request.urlopen(req, timeout=3.0) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return data.get("status") == "ok"
    except Exception:
        return False


def _runtime_get(config: RuntimeConfig, path: str, timeout: float = 10.0) -> dict[str, Any]:
    """GET request to the running runtime server."""
    url = f"http://127.0.0.1:{config.local_port}{path}"
    req = urllib.request.Request(url, method="GET")
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _runtime_post(
    config: RuntimeConfig, path: str, body: dict[str, Any], timeout: float = 120.0,
) -> dict[str, Any]:
    """POST request to the running runtime server."""
    url = f"http://127.0.0.1:{config.local_port}{path}"
    data = json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        url, data=data, headers={"Content-Type": "application/json"}, method="POST",
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _ollama_is_running(ollama_url: str) -> bool:
    """Check if Ollama is reachable."""
    try:
        req = urllib.request.Request(f"{ollama_url}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=3.0):
            return True
    except Exception:
        return False


def _ollama_list_models(ollama_url: str) -> list[str]:
    """Get local Ollama model names."""
    try:
        req = urllib.request.Request(f"{ollama_url}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=5.0) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return [m["name"].split(":")[0] for m in data.get("models", [])]
    except Exception:
        return []


def _read_keys(data_dir: Path) -> dict[str, str]:
    """Read stored API keys from disk."""
    path = data_dir / "keys.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _read_tokens(data_dir: Path) -> dict[str, dict[str, Any]]:
    """Read stored OAuth tokens from disk."""
    path = data_dir / "oauth_tokens.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _write_tokens(data_dir: Path, tokens: dict[str, dict[str, Any]]) -> None:
    """Write OAuth tokens to disk."""
    data_dir.mkdir(parents=True, exist_ok=True)
    (data_dir / "oauth_tokens.json").write_text(json.dumps(tokens, indent=2), "utf-8")


# ── Commands ──────────────────────────────────────────────────────────


def cmd_start(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Start the runtime server and open the app in the browser."""
    # Check if already running
    if _is_runtime_running(config):
        print(f"Brynq is already running at http://127.0.0.1:{config.local_port}")
        print(f"App: {APP_URL}")
        if not args.no_browser:
            webbrowser.open(APP_URL)
        return 0

    # Check Ollama
    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            print(f"  Ollama: {_green('running')} ({', '.join(models)})")
        else:
            print(f"  Ollama: {_green('running')} (no models pulled)")
    else:
        print(f"  Ollama: {_dim('not running')} (local models unavailable)")

    # Check API keys
    keys = _read_keys(config.data_dir)
    if keys:
        providers = ", ".join(sorted(keys.keys()))
        print(f"  API keys: {_green(providers)}")
    else:
        print(f"  API keys: {_dim('none configured')}")

    # Start the server as a background process
    env = {**os.environ}
    env["BRYNQ_RUNTIME_LOCAL_PORT"] = str(config.local_port)
    if config.debug:
        env["BRYNQ_RUNTIME_DEBUG"] = "1"

    cmd = [
        sys.executable, "-m", "uvicorn",
        "runtime.app:app",
        "--host", "127.0.0.1",
        "--port", str(config.local_port),
    ]

    # Redirect output to log file
    log_dir = config.data_dir / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    log_file = log_dir / "runtime.log"

    with open(log_file, "a") as lf:
        proc = subprocess.Popen(
            cmd,
            env=env,
            stdout=lf,
            stderr=lf,
            start_new_session=True,
        )

    # Save PID for later shutdown
    pid_file = _pid_file(config)
    config.data_dir.mkdir(parents=True, exist_ok=True)
    pid_file.write_text(str(proc.pid), "utf-8")

    # Wait for server to come up
    print()
    print("Starting Brynq runtime...", end="", flush=True)
    started = False
    for _ in range(30):
        time.sleep(0.5)
        if _is_runtime_running(config):
            started = True
            break
        print(".", end="", flush=True)

    if not started:
        print(f" {_red('failed')}")
        print(f"Server did not start. Check logs: {log_file}")
        return 1

    print(f" {_green('ready')}")
    print()
    print(f"  {_bold('Brynq is running')} at http://127.0.0.1:{config.local_port}")
    print(f"  App: {_cyan(APP_URL)}")
    print(f"  Logs: {_dim(str(log_file))}")
    print()
    print(f"  {_dim('Stop with:')} brynq-runtime stop")

    # Open browser
    if not args.no_browser:
        webbrowser.open(APP_URL)

    return 0


def cmd_stop(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Stop the runtime server."""
    pid_file = _pid_file(config)

    if not _is_runtime_running(config) and not pid_file.exists():
        print("Brynq is not running.")
        return 0

    # Try to kill by PID
    if pid_file.exists():
        try:
            pid = int(pid_file.read_text("utf-8").strip())
            os.kill(pid, signal.SIGTERM)
            print(f"Stopping Brynq (PID {pid})...", end="", flush=True)

            # Wait for shutdown
            for _ in range(20):
                time.sleep(0.25)
                try:
                    os.kill(pid, 0)  # Check if still alive
                except OSError:
                    break

            pid_file.unlink(missing_ok=True)
            print(f" {_green('stopped')}")
            return 0

        except (ValueError, OSError) as e:
            # PID file stale or process already dead
            pid_file.unlink(missing_ok=True)

    # Fallback: check if still responding
    if _is_runtime_running(config):
        print(f"{_yellow('Warning')}: Could not find server process. It may still be running on port {config.local_port}.")
        return 1

    print("Brynq stopped.")
    pid_file.unlink(missing_ok=True)
    return 0


def cmd_status(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Show full runtime status."""
    print(_bold("Brynq Runtime Status"))
    print()

    # Runtime server
    running = _is_runtime_running(config)
    if running:
        print(f"  Runtime:  {_green('running')} on port {config.local_port}")
    else:
        print(f"  Runtime:  {_red('stopped')}")

    # Ollama
    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            print(f"  Ollama:   {_green('running')} ({len(models)} models: {', '.join(models)})")
        else:
            print(f"  Ollama:   {_green('running')} (no models pulled)")
    else:
        print(f"  Ollama:   {_red('stopped')}")

    # Cloud connection (only if runtime is up)
    if running:
        try:
            status_data = _runtime_get(config, "/status")
            cloud = status_data.get("cloud", {})
            if cloud.get("connected"):
                print(f"  Cloud:    {_green('connected')} ({config.cloud_url})")
            else:
                print(f"  Cloud:    {_yellow('unreachable')}")
        except Exception:
            print(f"  Cloud:    {_dim('unknown')}")
    else:
        print(f"  Cloud:    {_dim('unknown (runtime stopped)')}")

    # API keys
    print()
    keys = _read_keys(config.data_dir)
    provider_labels = {
        "anthropic": "Claude",
        "openai": "ChatGPT",
        "google": "Gemini",
        "mistral": "Mistral",
        "cohere": "Cohere",
        "groq": "Groq",
    }

    print("  API Keys:")
    if keys:
        for provider, _key in sorted(keys.items()):
            label = provider_labels.get(provider, provider)
            print(f"    {label:<12} {_green('configured')}")
    else:
        print(f"    {_dim('None configured. Use the app or POST /keys to add API keys.')}")

    # OAuth tokens
    tokens = _read_tokens(config.data_dir)
    if tokens:
        print()
        print("  OAuth:")
        for provider, token_data in sorted(tokens.items()):
            label = provider_labels.get(provider, provider)
            email = token_data.get("email", "")
            if email:
                print(f"    {label:<12} {_green('connected')} as {email}")
            else:
                print(f"    {label:<12} {_green('connected')}")

    print()
    print(f"  Data dir: {_dim(str(config.data_dir))}")
    print(f"  Version:  {VERSION}")

    return 0


def cmd_models(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """List available models from all sources."""
    print(_bold("Available Models"))
    print()

    # Local models (Ollama)
    ollama_ok = _ollama_is_running(config.ollama_url)
    print("  Local (Ollama):")
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            for m in models:
                print(f"    {m:<20} {_green('ready')}")
        else:
            print(f"    {_dim('No models pulled. Run: ollama pull llama3')}")
    else:
        print(f"    {_dim('Ollama not running. Start: ollama serve')}")

    # Cloud models (BYOK)
    print()
    print("  Cloud (BYOK):")

    keys = _read_keys(config.data_dir)
    cloud_providers = {
        "anthropic": [
            ("claude-sonnet-4-20250514", "Claude Sonnet 4"),
            ("claude-opus-4-20250514", "Claude Opus 4"),
            ("claude-haiku-3-5-20241022", "Claude Haiku 3.5"),
        ],
        "openai": [
            ("gpt-4o", "GPT-4o"),
            ("gpt-4o-mini", "GPT-4o Mini"),
            ("o3-mini", "o3 Mini"),
        ],
        "google": [
            ("gemini-2.0-flash", "Gemini 2.0 Flash"),
            ("gemini-2.5-pro", "Gemini 2.5 Pro"),
        ],
    }

    any_cloud = False
    for provider, model_list in cloud_providers.items():
        if provider in keys:
            any_cloud = True
            for model_id, label in model_list:
                print(f"    {label:<24} {_green('ready')}  {_dim(model_id)}")
        else:
            for _model_id, label in model_list:
                print(f"    {label:<24} {_red('no key')}  {_dim('POST /keys to add')}")

    if not any_cloud and not keys:
        print(f"    {_dim('No API keys configured.')}")

    return 0


def cmd_login(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Start OAuth flow for a cloud provider."""
    from runtime.auth.providers import PROVIDERS, get_provider

    provider_name = args.provider.lower()

    # Map common aliases
    aliases = {"claude": "anthropic", "chatgpt": "openai", "gpt": "openai", "gemini": "google"}
    provider_key = aliases.get(provider_name, provider_name)

    try:
        provider_config = get_provider(provider_key)
    except ValueError as e:
        print(f"{_red('Error')}: {e}")
        return 1

    client_id = provider_config.get_client_id()
    if not client_id:
        print(f"{_red('Error')}: No client ID configured for {provider_config.name}.")
        print(f"  Set the environment variable: {provider_config.client_id_env}")
        return 1

    # Build the authorization URL
    import hashlib
    import secrets

    state = secrets.token_urlsafe(32)
    callback_port = 9876
    redirect_uri = f"http://localhost:{callback_port}/callback"

    params = {
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "response_type": "code",
        "scope": " ".join(provider_config.scopes),
        "state": state,
    }
    if provider_config.extra_auth_params:
        params.update(provider_config.extra_auth_params)

    # PKCE if required
    code_verifier = None
    if provider_config.requires_pkce:
        code_verifier = secrets.token_urlsafe(64)
        digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
        import base64
        code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
        params["code_challenge"] = code_challenge
        params["code_challenge_method"] = "S256"

    query = "&".join(f"{k}={urllib.request.quote(str(v))}" for k, v in params.items())
    auth_url = f"{provider_config.auth_url}?{query}"

    print(f"Connecting to {_bold(provider_config.name)}...")
    print(f"Opening browser for authorization...")
    print()

    # Start a temporary local server to receive the callback
    from http.server import BaseHTTPRequestHandler, HTTPServer
    from urllib.parse import parse_qs, urlparse

    auth_code = None
    received_state = None
    server_error = None

    class CallbackHandler(BaseHTTPRequestHandler):
        def do_GET(self) -> None:
            nonlocal auth_code, received_state, server_error
            parsed = urlparse(self.path)
            qs = parse_qs(parsed.query)

            if parsed.path == "/callback":
                auth_code = qs.get("code", [None])[0]
                received_state = qs.get("state", [None])[0]
                error = qs.get("error", [None])[0]

                if error:
                    server_error = qs.get("error_description", [error])[0]
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>")
                else:
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h2>Connected to Brynq!</h2><p>You can close this tab.</p></body></html>")
            else:
                self.send_response(404)
                self.end_headers()

        def log_message(self, format: str, *log_args: Any) -> None:
            pass  # Suppress request logging

    try:
        server = HTTPServer(("127.0.0.1", callback_port), CallbackHandler)
    except OSError:
        print(f"{_red('Error')}: Port {callback_port} is in use. Close other OAuth flows and try again.")
        return 1

    server.timeout = 120  # 2 minute timeout

    # Open browser
    webbrowser.open(auth_url)
    print(f"Waiting for authorization (timeout: 2 minutes)...")
    print(f"  {_dim('If the browser did not open, visit:')}")
    print(f"  {_dim(auth_url[:100])}{'...' if len(auth_url) > 100 else ''}")
    print()

    # Wait for callback
    server.handle_request()
    server.server_close()

    if server_error:
        print(f"{_red('Error')}: Authorization failed: {server_error}")
        return 1

    if not auth_code:
        print(f"{_red('Error')}: No authorization code received. Try again.")
        return 1

    if received_state != state:
        print(f"{_red('Error')}: State mismatch. Possible CSRF attack. Aborting.")
        return 1

    # Exchange code for token
    print("Exchanging authorization code for token...", end="", flush=True)

    token_body: dict[str, str] = {
        "grant_type": "authorization_code",
        "code": auth_code,
        "redirect_uri": redirect_uri,
        "client_id": client_id,
    }
    if code_verifier:
        token_body["code_verifier"] = code_verifier

    token_data = json.dumps(token_body).encode("utf-8")
    token_req = urllib.request.Request(
        provider_config.token_url,
        data=token_data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(token_req, timeout=30) as resp:
            token_resp = json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        print(f" {_red('failed')}")
        print(f"Token exchange error: {e}")
        return 1

    print(f" {_green('done')}")

    # Store token
    tokens = _read_tokens(config.data_dir)
    tokens[provider_key] = {
        "access_token": token_resp.get("access_token", ""),
        "refresh_token": token_resp.get("refresh_token", ""),
        "token_type": token_resp.get("token_type", "bearer"),
        "scope": token_resp.get("scope", ""),
        "email": token_resp.get("email", ""),
        "connected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }
    _write_tokens(config.data_dir, tokens)

    email = token_resp.get("email", "")
    if email:
        print(f"\n{_green('Connected')} to {provider_config.name} as {_bold(email)}")
    else:
        print(f"\n{_green('Connected')} to {provider_config.name}")

    return 0


def cmd_chat(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Interactive multi-model terminal chat with fixed input panel.

    Auto-detects all available models (Claude, Gemini, Ollama) and sends
    each message to all of them. The input bar stays pinned at the bottom
    of the terminal while messages scroll above.
    """
    import shutil
    import subprocess
    import threading

    # ── Detect available models ────────────────────────────────────
    active_models: list[str] = []

    if shutil.which("claude"):
        active_models.append("claude")
    if shutil.which("gemini"):
        active_models.append("gemini")

    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        for m in _ollama_list_models(config.ollama_url):
            active_models.append(f"ollama:{m}")

    if not active_models:
        print(f"{_red('No models found.')} Install Claude, Gemini, or Ollama.")
        return 1

    # ── ANSI helpers ───────────────────────────────────────────────
    _MAGENTA = "\033[35m"
    _BLUE = "\033[34m"
    _YELLOW = "\033[33m"
    _WHITE_BOLD = "\033[1;37m"
    _DIM = "\033[2m"
    _BG_PANEL = "\033[48;5;236m"   # dark grey background
    _BG_HEADER = "\033[44;37m"     # blue bg, white text
    _RESET = "\033[0m"
    _CLEAR_LINE = "\033[2K"
    _SAVE_POS = "\033[s"
    _RESTORE_POS = "\033[u"

    def _model_label(model: str) -> str:
        if model == "claude":
            name, color = "Claude (Opus 4.6)", _MAGENTA
        elif model == "gemini":
            name, color = "Gemini 3.1", _BLUE
        elif model.startswith("ollama:"):
            name, color = f"Ollama/{model.split(':', 1)[1]}", _YELLOW
        else:
            name, color = model, ""
        return f"{color}{name}{_RESET}" if _COLOR and color else name

    # ── Model runners ──────────────────────────────────────────────
    def _call_claude(prompt: str) -> str:
        try:
            r = subprocess.run(
                ["claude", "--print"], input=prompt,
                capture_output=True, text=True, timeout=600,
                encoding="utf-8", errors="replace",
            )
            return r.stdout.strip() if r.returncode == 0 else f"[error: {r.stderr.strip()[:200]}]"
        except subprocess.TimeoutExpired:
            return "[timed out]"
        except Exception as e:
            return f"[error: {e}]"

    def _call_gemini(prompt: str) -> str:
        try:
            r = subprocess.run(
                ["gemini", "-p", prompt], input=prompt,
                capture_output=True, text=True, timeout=600,
                encoding="utf-8", errors="replace", shell=True,
            )
            return r.stdout.strip() if r.returncode == 0 else f"[error: {r.stderr.strip()[:200]}]"
        except subprocess.TimeoutExpired:
            return "[timed out]"
        except Exception as e:
            return f"[error: {e}]"

    def _call_ollama(prompt: str, model: str) -> str:
        try:
            body = {
                "model": model,
                "messages": [{"role": "user", "content": prompt}],
                "stream": False,
            }
            data = json.dumps(body).encode("utf-8")
            req = urllib.request.Request(
                f"{config.ollama_url}/api/chat",
                data=data,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=config.ollama_timeout) as resp:
                result = json.loads(resp.read().decode("utf-8"))
                return result.get("message", {}).get("content", "")
        except Exception as e:
            return f"[error: {e}]"

    def _call_model(model: str, prompt: str) -> str:
        if model == "claude":
            return _call_claude(prompt)
        elif model == "gemini":
            return _call_gemini(prompt)
        elif model.startswith("ollama:"):
            return _call_ollama(prompt, model.split(":", 1)[1])
        return "[unknown model]"

    # ── Terminal drawing ───────────────────────────────────────────
    term = shutil.get_terminal_size((80, 24))
    cols = term.columns
    rows = term.lines

    def _bar(char: str = "─") -> str:
        return f"{_DIM}{char * cols}{_RESET}" if _COLOR else char * cols

    def _draw_header() -> None:
        # Clear screen and draw header
        sys.stdout.write("\033[2J\033[H")  # clear + move to top
        title = " BRYNQ CHAT "
        pad = max(0, (cols - len(title)) // 2)
        right = max(0, cols - pad - len(title))
        if _COLOR:
            print(f"{_BG_HEADER}{'─' * pad}{title}{'─' * right}{_RESET}")
        else:
            print(f"{'─' * pad}{title}{'─' * right}")

        model_tags = "  ".join(_model_label(m) for m in active_models)
        print(f"  {_DIM}Models:{_RESET} {model_tags}")
        print(f"  {_DIM}/add  /remove  /models  /chain  /quit{_RESET}")
        print(_bar())

    def _draw_input_panel() -> str:
        """Draw the input panel at the bottom and get user input."""
        print()
        # Top border of input panel
        if _COLOR:
            print(f"{_BG_PANEL}{_DIM}{'─' * cols}{_RESET}")
            sys.stdout.write(f"{_BG_PANEL} ")
        else:
            print("─" * cols)
            sys.stdout.write(" ")

        try:
            if _COLOR:
                user_input = input(f"{_WHITE_BOLD}brynq>{_RESET}{_BG_PANEL} ")
            else:
                user_input = input("brynq> ")
        except (EOFError, KeyboardInterrupt):
            return ""

        # Bottom border
        if _COLOR:
            print(f"{_BG_PANEL}{_DIM}{'─' * cols}{_RESET}")
        else:
            print("─" * cols)

        return user_input.strip()

    def _print_message(sender: str, content: str, meta: str = "") -> None:
        """Print a chat message with sender label."""
        print()
        header = f"  {sender}"
        if meta:
            header += f"  {_DIM}{meta}{_RESET}" if _COLOR else f"  {meta}"
        print(header)
        print(f"  {_bar('╌')}")
        for line in content.splitlines():
            print(f"  {line}")

    # ── Chat history (for redraw) ──────────────────────────────────
    chat_history: list[tuple[str, str, str, str]] = []  # (role, sender, content, meta)

    def _redraw() -> None:
        """Redraw the entire chat from history."""
        _draw_header()
        for role, sender, content, meta in chat_history:
            _print_message(sender, content, meta)

    _draw_header()

    # ── Main loop ──────────────────────────────────────────────────
    while True:
        user_input = _draw_input_panel()

        if not user_input:
            if user_input is None or user_input == "":
                # Check if it was Ctrl+C/Ctrl+D
                try:
                    _ = ""  # just to have something
                except Exception:
                    pass
            continue

        # Handle commands
        if user_input.startswith("/"):
            parts = user_input.split(maxsplit=1)
            cmd = parts[0].lower()
            cmd_arg = parts[1].strip() if len(parts) > 1 else ""

            if cmd in ("/quit", "/exit", "/q"):
                sys.stdout.write("\033[2J\033[H")  # clear screen
                print(_dim("Goodbye from Brynq."))
                return 0
            elif cmd == "/add":
                if cmd_arg and cmd_arg not in active_models:
                    active_models.append(cmd_arg)
                _redraw()
                continue
            elif cmd == "/remove":
                if cmd_arg in active_models and len(active_models) > 1:
                    active_models.remove(cmd_arg)
                _redraw()
                continue
            elif cmd == "/models":
                _redraw()
                print()
                for m in active_models:
                    print(f"  {_model_label(m)}")
                continue
            elif cmd == "/chain":
                if cmd_arg:
                    _run_chain_inline(config, cmd_arg)
                else:
                    print(f"  Usage: /chain <prompt>")
                continue
            elif cmd == "/clear":
                chat_history.clear()
                _redraw()
                continue
            else:
                _redraw()
                print(f"\n  {_dim('Unknown: ' + cmd)}")
                continue

        # ── Add user message to history ────────────────────────────
        you_label = f"{_WHITE_BOLD}You{_RESET}" if _COLOR else "You"
        chat_history.append(("user", you_label, user_input, ""))
        _redraw()

        # ── Send to all models in parallel ─────────────────────────
        print(f"\n  {_DIM}Sending to {len(active_models)} model(s)...{_RESET}" if _COLOR
              else f"\n  Sending to {len(active_models)} model(s)...")

        results: dict[str, str] = {}
        timings: dict[str, float] = {}
        completed: list[str] = []
        lock = threading.Lock()

        def _run_threaded(model: str) -> None:
            start = time.time()
            output = _call_model(model, user_input)
            elapsed = time.time() - start
            with lock:
                results[model] = output
                timings[model] = elapsed
                completed.append(model)
                # Live update: show which models have responded
                count = len(completed)
                sys.stdout.write(
                    f"\r  {_DIM}{count}/{len(active_models)} responses received{_RESET}" if _COLOR
                    else f"\r  {count}/{len(active_models)} responses received"
                )
                sys.stdout.flush()

        threads = []
        for m in active_models:
            t = threading.Thread(target=_run_threaded, args=(m,), daemon=True)
            threads.append(t)
            t.start()

        for t in threads:
            t.join()

        # ── Add responses to history and redraw ────────────────────
        for m in active_models:
            content = results.get(m, "[no response]")
            elapsed = timings.get(m, 0)
            chat_history.append(("assistant", _model_label(m), content, f"{elapsed:.1f}s"))

        _redraw()

    return 0  # pragma: no cover


def _run_chain_inline(config: RuntimeConfig, prompt: str) -> None:
    """Execute a chain and print results inline in chat."""
    if not _is_runtime_running(config):
        print(f"  {_red('Error')}: Chains require the runtime server. Run: brynq-runtime start")
        return

    print()
    print(f"  {_dim('Running chain...')}")
    start = time.time()

    try:
        result = _runtime_post(config, "/chain", {"request": prompt})
        total_ms = result.get("total_duration_ms", 0)
        steps = result.get("steps", [])
        success = result.get("success", False)

        print()
        for i, step in enumerate(steps, 1):
            model = step.get("model", "unknown")
            content = step.get("content", "")
            step_ok = step.get("success", False)
            status = _green("done") if step_ok else _red("failed")

            print(f"  Step {i} [{_cyan(model)}] {status}")
            print(f"  {content[:200]}{'...' if len(content) > 200 else ''}")
            print()

        print(f"  {_dim(f'Chain complete: {len(steps)} steps, {total_ms}ms total')}")
        print()

    except urllib.error.HTTPError as e:
        try:
            error_body = json.loads(e.read().decode("utf-8"))
            detail = error_body.get("detail", str(e))
        except Exception:
            detail = str(e)
        print(f"  {_red('Error')}: {detail}")
        print()

    except Exception as e:
        print(f"  {_red('Error')}: {e}")
        print()


def cmd_chain(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Run a multi-model chain from the terminal."""
    if not _is_runtime_running(config):
        print(f"{_red('Error')}: Runtime is not running. Start it first:")
        print(f"  brynq-runtime start")
        return 1

    prompt = args.prompt
    print(f"{_bold('Chain')}: {prompt}")
    print()

    start = time.time()

    try:
        result = _runtime_post(config, "/chain", {"request": prompt})
    except urllib.error.HTTPError as e:
        try:
            error_body = json.loads(e.read().decode("utf-8"))
            detail = error_body.get("detail", str(e))
        except Exception:
            detail = str(e)
        print(f"{_red('Error')}: {detail}")
        return 1
    except Exception as e:
        print(f"{_red('Error')}: Could not reach runtime: {e}")
        return 1

    total_ms = result.get("total_duration_ms", 0)
    steps = result.get("steps", [])
    success = result.get("success", False)

    for i, step in enumerate(steps, 1):
        model = step.get("model", "unknown")
        content = step.get("content", "")
        step_ok = step.get("success", False)
        error = step.get("error")

        status = _green("done") if step_ok else _red("failed")
        print(f"  Step {i}/{len(steps)} [{_cyan(model)}] {status}")
        print()

        if content:
            # Indent content
            for line in content.splitlines():
                print(f"    {line}")
            print()

        if error:
            print(f"    {_red('Error')}: {error}")
            print()

    # Summary
    wall_ms = int((time.time() - start) * 1000)
    status_str = _green("success") if success else _red("failed")
    print(f"  {_dim('---')}")
    print(f"  Result: {status_str}  |  Steps: {len(steps)}  |  Time: {total_ms}ms (wall: {wall_ms}ms)")

    return 0 if success else 1


def cmd_setup(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Create a desktop shortcut for Brynq Chat."""
    import shutil

    desktop = Path.home() / "Desktop"
    if not desktop.exists():
        # Try XDG desktop dir (Linux)
        desktop = Path.home()

    python = sys.executable

    if sys.platform == "win32":
        # Windows: create a .bat file
        bat_path = desktop / "Brynq Chat.bat"
        bat_path.write_text(
            f'@echo off\ntitle Brynq Chat\n"{python}" -m runtime chat\n',
            encoding="utf-8",
        )
        print(f"  {_green('Created:')} {bat_path}")
        print(f"  Double-click it to start Brynq Chat.")

    elif sys.platform == "darwin":
        # macOS: create a .command file
        cmd_path = desktop / "Brynq Chat.command"
        cmd_path.write_text(
            f'#!/bin/bash\n"{python}" -m runtime chat\n',
            encoding="utf-8",
        )
        cmd_path.chmod(0o755)
        print(f"  {_green('Created:')} {cmd_path}")

    else:
        # Linux: create a .desktop file
        desktop_path = desktop / "brynq-chat.desktop"
        desktop_path.write_text(
            f"[Desktop Entry]\n"
            f"Name=Brynq Chat\n"
            f"Comment=Multi-model AI chat\n"
            f"Exec={python} -m runtime chat\n"
            f"Terminal=true\n"
            f"Type=Application\n"
            f"Categories=Development;\n",
            encoding="utf-8",
        )
        desktop_path.chmod(0o755)
        print(f"  {_green('Created:')} {desktop_path}")

    return 0


# ── CLI Parser ────────────────────────────────────────────────────────


def build_parser() -> argparse.ArgumentParser:
    """Build the argument parser for the brynq-runtime CLI."""
    parser = argparse.ArgumentParser(
        prog="brynq-runtime",
        description="Brynq Runtime -- Launch and control the local AI runtime.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "examples:\n"
            "  brynq-runtime start              Start runtime + open browser\n"
            "  brynq-runtime login claude        OAuth login for Claude\n"
            "  brynq-runtime models              List available models\n"
            "  brynq-runtime chat                Interactive terminal chat\n"
            '  brynq-runtime chain "summarize"   Run a multi-model chain\n'
            "  brynq-runtime status              Show runtime status\n"
            "  brynq-runtime stop                Stop the runtime server\n"
        ),
    )

    sub = parser.add_subparsers(dest="command", metavar="command")

    # start
    p_start = sub.add_parser("start", help="Start the runtime server and open the app")
    p_start.add_argument(
        "--no-browser", action="store_true",
        help="Do not open the browser automatically",
    )

    # stop
    sub.add_parser("stop", help="Stop the runtime server")

    # status
    sub.add_parser("status", help="Show runtime status")

    # models
    sub.add_parser("models", help="List available models (local + cloud)")

    # login
    p_login = sub.add_parser("login", help="OAuth login for a cloud provider")
    p_login.add_argument(
        "provider",
        help="Provider name: claude, gemini, openai (or: anthropic, google)",
    )

    # chat
    p_chat = sub.add_parser("chat", help="Interactive terminal chat")
    p_chat.add_argument("--model", "-m", default=None, help="Model to use (auto-detected if omitted)")

    # chain
    p_chain = sub.add_parser("chain", help="Run a multi-model chain")
    p_chain.add_argument("prompt", help="Chain prompt describing what to do")

    # setup
    sub.add_parser("setup", help="Create a desktop shortcut for Brynq Chat")

    return parser


def _fix_windows_encoding() -> None:
    """Fix Windows console encoding: cp1252 cannot handle unicode output."""
    if sys.platform != "win32":
        return
    for stream_name in ("stdout", "stderr"):
        stream = getattr(sys, stream_name, None)
        if stream is None:
            continue
        # Only wrap if it has a raw buffer and is not already UTF-8
        buf = getattr(stream, "buffer", None)
        if buf is None:
            continue
        if getattr(stream, "encoding", "").lower().replace("-", "") == "utf8":
            continue
        try:
            wrapped = io.TextIOWrapper(buf, encoding="utf-8", errors="replace")
            setattr(sys, stream_name, wrapped)
        except Exception:
            pass


def main(argv: list[str] | None = None) -> int:
    """Entry point for the brynq-runtime CLI."""
    _fix_windows_encoding()

    parser = build_parser()
    args = parser.parse_args(argv)

    if not args.command:
        parser.print_help()
        return 0

    config = load_config()

    commands: dict[str, Any] = {
        "start": cmd_start,
        "stop": cmd_stop,
        "status": cmd_status,
        "models": cmd_models,
        "login": cmd_login,
        "chat": cmd_chat,
        "chain": cmd_chain,
        "setup": cmd_setup,
    }

    handler = commands.get(args.command)
    if handler is None:
        parser.print_help()
        return 1

    return handler(args, config)


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