from __future__ import annotations

import asyncio
import json
import os
import queue
import re
import shutil
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
import uuid
import base64
from pathlib import Path
from typing import Any

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical, VerticalScroll, Container
from textual.widgets import Footer, Header, Input, Label, Static, Markdown, ListView, ListItem
from textual.worker import Worker, WorkerState

from runtime.config import RuntimeConfig
from runtime.intent import classify_intent
from runtime.rooms.profile import local_profile
from runtime.security.trust import TrustManager, TrustLevel
from runtime.security.moderation import ModerationManager
from runtime.security.permissions import PermissionManager
from runtime.security.docker_sandbox import docker_available
from runtime.rooms.project_store import ProjectStore
from runtime.rooms.room_manager import room_manager
from runtime.rooms.chat_store import RoomChatStore
from runtime.rooms import project_sync

_CODE_MODELS = {"claude", "gemini", "chatgpt"}

def _ollama_is_running(ollama_url: str) -> bool:
    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]:
    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 _classify(text: str) -> tuple[str, str]:
    t = text.lower()
    # Action/Code usually single strong model
    if any(k in t for k in ("create ", "delete ", "make ", "open ", "run ", "install ", "move ", "copy ", "rename ", "download ", "mkdir", "folder", "file ", "save ", "write to ", "execute")):
        return "action", "single"
    if any(k in t for k in ("code", "function", "class ", "debug", "fix ", "refactor", "implement", "script", "program", "api ", "endpoint", "bug ", "error ", "test ", "deploy")):
        return "code", "single"
    # Debate for comparison/opinion
    if any(k in t for k in ("compare", "vs ", "versus", "better", "pros and cons", "debate", "argue", "which ", "should i use", "difference between", "advantages")):
        return "compare", "debate"
    # Refine for creative/writing
    if any(k in t for k in ("write ", "draft ", "compose", "design ", "story", "email ", "blog ", "essay ", "article ", "landing page", "headline", "slogan", "pitch")):
        return "creative", "refine"
    # Fan-out for research/broad inquiry
    if any(k in t for k in ("analyze", "research", "explain", "how does", "what is", "why ", "investigate", "explore", "summarize", "overview")):
        return "research", "fan_out"
    return "general", "single"

_MAX_FILE_SIZE = 500 * 1024

_FILE_PATH_RE = re.compile(
    r"(?:^|(?<=\s))(?!https?://)(?!/[a-z]+(?:\s|$))((?:[A-Za-z]:)?[^\s\"'<>|*?]+)(?=\s|$)", re.MULTILINE
)

def _scan_file_paths(text: str) -> list[Path]:
    found: list[Path] = []
    seen: set[str] = set()
    for m in _FILE_PATH_RE.finditer(text):
        token = m.group(1)
        if "/" not in token and "\\" not in token and "." not in token:
            continue
        p = Path(token).expanduser()
        if not p.is_absolute():
            p = Path.cwd() / p
        resolved = str(p.resolve())
        if resolved in seen:
            continue
        seen.add(resolved)
        try:
            if p.is_file() and p.stat().st_size <= _MAX_FILE_SIZE:
                found.append(p)
        except OSError:
            pass
    return found

def _prepend_file_contents(text: str, files: list[Path]) -> str:
    if not files:
        return text
    parts: list[str] = []
    for fp in files:
        try:
            content = fp.read_text(encoding="utf-8", errors="replace")
            parts.append(f"--- Contents of {fp.name} ---\n{content}\n--- End of {fp.name} ---")
        except OSError:
            pass
    if not parts:
        return text
    return "\n\n".join(parts) + f"\n\n{text}"

def _pick_models(task_type: str, strategy: str, active_models: list[str]) -> list[str]:
    code_capable = [m for m in active_models if m in _CODE_MODELS]
    all_m = active_models
    if not all_m: return []
    if task_type in ("action", "code"):
        if "claude" in active_models: return ["claude"]
        return code_capable[:1] if code_capable else all_m[:1]
    if strategy == "debate":
        if len(all_m) >= 3: return all_m[:3]
        return all_m[:2] if len(all_m) >= 2 else all_m[:1]
    if strategy == "refine":
        fast = [m for m in all_m if m.startswith("ollama:")] or all_m[-1:]
        smart = code_capable[:1] or all_m[:1]
        return [fast[0], smart[0], fast[0]]
    if strategy == "fan_out":
        return all_m[:3]
    return code_capable[:1] if code_capable else all_m[:1]

def _model_display(model: str) -> str:
    if model == "claude": return "Claude (Opus 4.6)"
    elif model == "gemini": return "Gemini 3.1"
    elif model == "chatgpt": return "ChatGPT-4o"
    elif model.startswith("ollama:"): return f"Ollama/{model.split(':', 1)[1]}"
    return model

def _try_direct_action(text: str) -> str | None:
    t = text.lower().strip()
    m = re.search(r"(?:create|make|new)\s+(?:a\s+)?(?:folder|directory|dir)\s+(?:called|named)?\s*[\"']?(\S+)[\"']?", t)
    if not m: m = re.search(r"(?:mkdir)\s+[\"']?(\S+)[\"']?", t)
    if m:
        folder_name = m.group(1).strip("\"'")
        target = Path.home() / "Desktop" / folder_name if "desktop" in t else Path.cwd() / folder_name
        try:
            target.mkdir(parents=True, exist_ok=True)
            return f"Created folder: {target}"
        except OSError as e:
            return f"Failed to create folder: {e}"

    m = re.search(r"(?:delete|remove)\s+(?:the\s+)?(?:folder|directory|file)\s+(?:called|named)?\s*[\"']?(\S+)[\"']?", t)
    if m:
        name = m.group(1).strip("\"'")
        target = Path.home() / "Desktop" / name if "desktop" in t else Path.cwd() / name
        if target.exists():
            if target.is_dir(): shutil.rmtree(target)
            else: target.unlink()
            return f"Deleted: {target}"
        return f"Not found: {target}"

    m = re.search(r"(?:open)\s+(https?://\S+)", t)
    if m:
        import webbrowser
        webbrowser.open(m.group(1))
        return f"Opened: {m.group(1)}"
    return None

class ChatMessage(Static):
    def __init__(self, sender: str, content: str, msg_type: str = "ai", timestamp: str = "", css_class: str = "msg-ai"):
        super().__init__(classes=css_class)
        self._sender = sender
        self._content = content
        self._timestamp = timestamp

    def compose(self) -> ComposeResult:
        ts = f"  {self._timestamp}" if self._timestamp else ""
        yield Label(f"{self._sender}{ts}", classes="msg-sender")
        yield Markdown(self._content, classes="msg-body")

class RoomListItem(ListItem):
    def __init__(self, name: str, member_count: int, room_code: str, is_active: bool = False):
        super().__init__()
        self._name = name
        self._count = member_count
        self._code = room_code
        self._is_active = is_active

    def compose(self) -> ComposeResult:
        dot = "●" if self._is_active else "○"
        yield Label(f"{dot} {self._name} ({self._count})", classes="room-label")

class FileListItem(ListItem):
    def __init__(self, relative_path: str, size_bytes: int):
        super().__init__()
        self._path = relative_path
        self._size = size_bytes

    def compose(self) -> ComposeResult:
        kb = self._size / 1024
        yield Label(f"  {self._path} ({kb:.1f}KB)", classes="file-label")

class MemberListItem(ListItem):
    def __init__(self, username: str, is_online: bool = True):
        super().__init__()
        self._username = username
        self._is_online = is_online

    def compose(self) -> ComposeResult:
        dot = "●" if self._is_online else "○"
        yield Label(f"{dot} {self._username}")

class ModelStatusItem(Static):
    def __init__(self, model_name: str, display_name: str, status: str = "idle"):
        css = f"model-{status}"
        super().__init__(classes=f"model-item {css}")
        icons = {"idle": "IDLE", "active": "BUSY", "done": "DONE", "error": "ERR"}
        self._label = f"[{icons.get(status, '--')}] {display_name}"

    def compose(self) -> ComposeResult:
        yield Label(self._label)

class BrynqChat(App):
    TITLE = "BRYNQ"
    SUB_TITLE = "Local AI Orchestration"
    
    CSS = """
Screen { layout: vertical; background: #0f0f1a; color: #d0d0e0; }

#body { layout: horizontal; height: 1fr; }

/* Panels */
#left-panel { width: 30; background: #12122a; border-right: double #2a2a4a; }
#right-panel { width: 26; background: #12122a; border-left: double #2a2a4a; }
#center-panel { width: 1fr; background: #0f0f1a; }

/* Headers */
.section-header { 
    background: #1a1a3a; 
    color: #00d4aa; 
    text-style: bold; 
    text-align: center; 
    width: 100%; 
    height: 1; 
    border-bottom: solid #00d4aa;
}
#center-header { 
    background: #1a2a4a; 
    color: #00d4aa; 
    text-style: bold; 
    text-align: left; 
    width: 100%; 
    height: 1; 
    padding: 0 1;
}

/* Lists */
ListView { background: transparent; }
ListItem { padding: 0 1; background: transparent; color: #8888aa; }
ListItem:hover { background: #1a2a4a; color: #00d4aa; }
ListItem.--highlight { background: #00d4aa; color: #0f0f1a; text-style: bold; }

.room-label { color: #8888aa; }
.file-label { color: #6688aa; }

/* Chat Messages */
#chat-feed { height: 1fr; overflow-y: scroll; padding: 1 2; }

ChatMessage { margin: 0 0 1 0; border: none; }
.msg-self { background: #1a2a4a; border-left: thick #00d4aa; padding: 1 2; margin-left: 4; }
.msg-peer { background: #1a1a2e; border-left: thick #5599ff; padding: 1 2; }
.msg-ai { background: #1a1a2e; border-left: thick #cc77ff; padding: 1 2; margin-right: 4; }
.msg-system { background: #0a0a18; padding: 0 2; color: #556677; text-align: center; border: none; }

.msg-sender { text-style: bold; color: #00d4aa; margin-bottom: 1; }
.msg-body { color: #d0d0e0; }
.msg-body MarkdownBlock { background: transparent; border: none; padding: 0; }
.msg-body Markdown CodeBlock { background: #0a0a1a; color: #00ffaa; border: solid #1a1a3a; padding: 1; }

/* Model Indicators */
.model-item { padding: 0 1; height: 1; }
.model-idle { color: #444466; }
.model-active { color: #ffcc00; text-style: italic; }
.model-done { color: #00d4aa; text-style: bold; }
.model-error { color: #ff4444; text-style: blink; }

/* Input Bar */
#input-bar { 
    dock: bottom; 
    height: auto; 
    background: #12122a; 
    border-top: double #2a2a4a; 
    padding: 1 1;
}
#chat-input { 
    width: 100%; 
    background: #0a0a18; 
    color: #00ffaa; 
    border: tall #2a2a4a;
}
#chat-input:focus { 
    border: tall #00d4aa;
}
#input-hint { 
    color: #444466; 
    text-align: right; 
    height: 1; 
    font-size: small;
}
    """

    BINDINGS = [
        Binding("ctrl+c", "quit", "Quit", show=True),
        Binding("ctrl+l", "clear_chat", "Clear", show=True),
        Binding("escape", "focus_input", "Focus Input", show=False),
        Binding("f1", "help", "Help", show=True),
    ]

    def __init__(self, config: RuntimeConfig) -> None:
        super().__init__()
        self.config = config
        self.active_models: list[str] = []
        self.conversation: list[dict[str, str]] = []
        self._model_states: dict[str, str] = {}
        self._last_timer: dict[str, float] = {}
        self._ws_listener_active: bool = False

    def compose(self) -> ComposeResult:
        yield Header()
        with Horizontal(id="body"):
            with Vertical(id="left-panel"):
                yield Label("ROOMS", classes="section-header")
                yield ListView(id="room-list")
                yield Label("PROJECT FILES", classes="section-header")
                yield ListView(id="project-files")
            with Vertical(id="center-panel"):
                yield Label("CHAT LOG", id="center-header")
                yield VerticalScroll(id="chat-feed")
            with Vertical(id="right-panel"):
                yield Label("MEMBERS", classes="section-header")
                yield ListView(id="member-list")
                yield Label("AI MODELS", classes="section-header")
                yield VerticalScroll(id="model-list")
        with Container(id="input-bar"):
            yield Input(placeholder="Type a message or /command...", id="chat-input")
            yield Label("[F1] HELP | /ROOM | /PROJECT | /MODELS | /CHAIN", id="input-hint")
        yield Footer()

    def on_mount(self) -> None:
        self._detect_models()
        self._refresh_model_list()
        self._refresh_room_list()
        self._refresh_member_list()
        self._refresh_project_files()
        prof = local_profile.get()
        self._trust = TrustManager(owner_id=prof.get("user_id", ""))
        self._moderation = ModerationManager()
        self._permissions = PermissionManager()
        self._add_system_message("--- BRYNQ COGNITIVE ENGINE ONLINE ---")
        self._add_system_message("Type /help for available commands or F1 for manual.")
        self.query_one("#chat-input", Input).focus()
        self.run_worker(self._heartbeat_loop, thread=True)

    def action_clear_chat(self) -> None:
        feed = self.query_one("#chat-feed", VerticalScroll)
        feed.remove_children()
        self.conversation.clear()
        self._last_timer.clear()
        for m in self._model_states:
            self._model_states[m] = "idle"
        self._refresh_model_list()
        self._add_system_message("Memory buffer cleared.")

    def action_focus_input(self) -> None:
        self.query_one("#chat-input", Input).focus()

    def action_help(self) -> None:
        self._handle_command("/help")

    def _detect_models(self) -> None:
        self.active_models.clear()
        if shutil.which("claude") or shutil.which("claude.cmd"):
            self.active_models.append("claude")
        if shutil.which("gemini") or shutil.which("gemini.cmd"):
            self.active_models.append("gemini")
        if os.environ.get("OPENAI_API_KEY"):
            self.active_models.append("chatgpt")
        if _ollama_is_running(self.config.ollama_url):
            for m in _ollama_list_models(self.config.ollama_url):
                self.active_models.append(f"ollama:{m}")
        for m in self.active_models:
            self._model_states[m] = "idle"

    def _refresh_room_list(self) -> None:
        container = self.query_one("#room-list", ListView)
        container.clear()
        if room_manager.active_room:
            name = room_manager.active_room.get("name", "Unnamed")
            members = room_manager.active_room.get("members", [])
            code = room_manager.active_room.get("room_code", "----")
            count = len(members) if members else 1
            container.append(RoomListItem(name, count, code, is_active=True))
        else:
            container.append(ListItem(Label(" No active room", classes="room-label")))

    def _refresh_project_files(self) -> None:
        container = self.query_one("#project-files", ListView)
        container.clear()
        store = self._get_project_store()
        if not store:
            container.append(ListItem(Label(" No project loaded", classes="file-label")))
            return
        files = store.list_files()
        if not files:
            container.append(ListItem(Label(" Project is empty", classes="file-label")))
            return
        for f in files:
            container.append(FileListItem(f["relative_path"], f["size_bytes"]))

    def _refresh_member_list(self) -> None:
        container = self.query_one("#member-list", ListView)
        container.clear()
        if not room_manager.active_room:
            container.append(ListItem(Label(" Solo Mode", classes="member-offline")))
            return
        prof = local_profile.get()
        container.append(MemberListItem(prof["username"], is_online=True))
        members = room_manager.active_room.get("members", [])
        for m in members:
            if m.get("user_id") != prof.get("user_id"):
                container.append(MemberListItem(
                    m.get("username", "Unknown"),
                    is_online=m.get("online", True)
                ))

    def _refresh_model_list(self) -> None:
        container = self.query_one("#model-list", VerticalScroll)
        container.remove_children()
        for m in self.active_models:
            state = self._model_states.get(m, "idle")
            display = _model_display(m)
            container.mount(ModelStatusItem(m, display, state))

    def _set_model_state(self, model: str, state: str) -> None:
        self._model_states[model] = state
        self.call_from_thread(self._refresh_model_list)
        
    def _reset_model_states(self) -> None:
        for m in self._model_states:
            self._model_states[m] = "idle"
        self.call_from_thread(self._refresh_model_list)
        
    def _get_project_store(self) -> ProjectStore | None:
        if room_manager.is_host and room_manager.host_service:
            return getattr(room_manager.host_service, 'project_store', None)
        elif room_manager.client_service:
            return getattr(room_manager.client_service, 'project_store', None)
        return None

    def _in_room(self) -> bool:
        return room_manager.active_room is not None

    def _heartbeat_loop(self) -> None:
        loop = asyncio.new_event_loop()
        async def _beat():
            while True:
                try:
                    await room_manager.send_heartbeat(
                        status="online", models=self.active_models,
                    )
                except Exception:
                    pass
                await asyncio.sleep(30)
        loop.run_until_complete(_beat())

    def _update_center_header(self) -> None:
        header = self.query_one("#center-header", Label)
        if room_manager.active_room:
            name = room_manager.active_room.get("name", "Room")
            code = room_manager.active_room.get("room_code", "----")
            header.update(f"ROOM: {name.upper()} [{code}]")
        else:
            header.update("CHAT LOG (OFFLINE)")

    def _add_chat_message(self, sender: str, content: str, msg_type: str = "ai", timestamp: str = "") -> None:
        css_map = {"self": "msg-self", "peer": "msg-peer", "ai": "msg-ai", "system": "msg-system"}
        css_class = css_map.get(msg_type, "msg-ai")
        feed = self.query_one("#chat-feed", VerticalScroll)
        msg = ChatMessage(sender, content, msg_type=msg_type, timestamp=timestamp, css_class=css_class)
        feed.mount(msg)
        msg.scroll_visible()

    def _add_system_message(self, text: str) -> None:
        self._add_chat_message("[SYSTEM]", text, msg_type="system")

    def _post_chat_message(self, sender: str, content: str, msg_type: str = "ai", timestamp: str = "") -> None:
        self.call_from_thread(self._add_chat_message, sender, content, msg_type, timestamp)

    def _post_system_message(self, text: str) -> None:
        self.call_from_thread(self._add_system_message, text)

    def on_input_submitted(self, event: Input.Submitted) -> None:
        text = event.value.strip()
        if not text:
            return
        event.input.value = ""
        command, original = classify_intent(text)
        if command is not None:
            self._handle_command(command)
        elif self._in_room():
            prof = local_profile.get()
            model, cleaned = self._check_ai_mention(text)
            if model:
                self._add_chat_message(prof["username"], text, msg_type="self")
                room_manager.send_message(text)
                self.run_worker(lambda: self._run_ai_in_room(model, cleaned), thread=True)
            else:
                self._add_chat_message(prof["username"], text, msg_type="self")
                room_manager.send_message(text)
        else:
            if not self.active_models:
                self._add_system_message("No models available. Install one first. Type /help for setup info.")
                return
            self._route_to_ai(text)

    def _check_ai_mention(self, text: str) -> tuple[str | None, str]:
        for model in self.active_models:
            short = model.split(":")[-1]
            if text.lower().startswith(f"@{short} "):
                return model, text[len(short) + 2:]
        return None, text
        
    def _run_ai_in_room(self, model: str, cleaned: str) -> None:
        res, t = self._invoke_model(model, cleaned)
        room_manager.send_message_raw({
            "type": "chat",
            "sender_name": f"[{_model_display(model)}]",
            "sender_id": local_profile.get().get("user_id", ""),
            "content": res,
            "timestamp": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
        })
        self._reset_model_states()

    def _handle_command(self, command: str) -> None:
        parts = command.split(maxsplit=2)
        cmd = parts[0].lower()
        arg1 = parts[1] if len(parts) > 1 else ""
        arg2 = parts[2] if len(parts) > 2 else ""

        if cmd in ("/quit", "/exit", "/q"):
            self.exit()
        elif cmd == "/room":
            self._handle_room_command(arg1, arg2)
        elif cmd == "/project":
            self._handle_project_command(arg1, " ".join(parts[2:]).strip() if len(parts) > 2 else arg2)
        elif cmd == "/models":
            lines = [f"Available Models ({len(self.active_models)}):"]
            for m in self.active_models:
                lines.append(f"  > {_model_display(m)}")
            self._add_system_message("\n".join(lines))
        elif cmd == "/add":
            if arg1 and arg1 not in self.active_models:
                self.active_models.append(arg1)
                self._model_states[arg1] = "idle"
                self._refresh_model_list()
                self._add_system_message(f"Attached model: {arg1}")
        elif cmd == "/remove":
            if arg1 in self.active_models:
                self.active_models.remove(arg1)
                self._model_states.pop(arg1, None)
                self._refresh_model_list()
                self._add_system_message(f"Detached model: {arg1}")
        elif cmd == "/status":
            lines = ["Cognitive Buffer Status:"]
            for m in self.active_models:
                lines.append(f"  {_model_display(m)}: {self._model_states.get(m, 'idle').upper()}")
            lines.append(f"Ollama Node: {'ACTIVE' if _ollama_is_running(self.config.ollama_url) else 'OFFLINE'}")
            self._add_system_message("\n".join(lines))
        elif cmd == "/chain":
            prompt = arg1 + " " + arg2
            if cmd == "/chain" and arg1 in ["debate", "refine", "fan_out"]:
                self.run_worker(lambda: self._execute_strategy(arg2, "general", arg1, self.active_models[:3]), thread=True)
            else:
                self.run_worker(lambda: self._execute_strategy(prompt, "general", "single", self.active_models[:1]), thread=True)
        elif cmd == "/trust":
            if not arg1 or not arg2:
                self._add_system_message("Usage: /trust <username> <level> (STRANGER, ROOM_MEMBER, FRIEND, OWNER)")
                return
            try:
                level = TrustLevel[arg2.upper()]
                self._trust.set_trust_level(arg1, level)
                self._add_system_message(f"Trust protocol for {arg1} updated to {arg2.upper()}")
            except KeyError:
                self._add_system_message(f"Unknown trust level: {arg2}")
        elif cmd == "/block":
            if not arg1:
                self._add_system_message("Usage: /block <username>")
                return
            self._moderation.block_user(arg1, arg2)
            self._add_system_message(f"User {arg1} added to blacklist.")
        elif cmd == "/unblock":
            if not arg1:
                self._add_system_message("Usage: /unblock <username>")
                return
            self._moderation.unblock_user(arg1)
            self._add_system_message(f"User {arg1} removed from blacklist.")
        elif cmd == "/permissions":
            grants = self._permissions.list_grants()
            if not grants:
                self._add_system_message("No active permission grants.")
            else:
                for peer_id, actions in grants.items():
                    self._add_system_message(f"  {peer_id}: {', '.join(actions)}")
        elif cmd == "/security":
            docker_ok = docker_available()
            self._add_system_message(
                "--- SECURITY AUDIT ---\n"
                f"  Sandbox (Docker): {'YES' if docker_ok else 'NO'}\n"
                f"  Vault Status:    ENCRYPTED\n"
                f"  Active Blocks:   {len(self._moderation.list_blocked())}\n"
                f"  Trust Overrides: {len(self._trust.list_overrides())}"
            )
        elif cmd == "/help":
            self._add_system_message(
                "PRIMARY COMMANDS:\n"
                "  /room create <name> | join <code> | status | leave\n"
                "  /project files | save <file> | open <file> | history\n"
                "  /models | /status | /add <id> | /remove <id>\n"
                "  /chain debate | refine | fan_out <prompt>\n"
                "  /clear | /export | /quit"
            )
        elif cmd == "/clear":
            self.action_clear_chat()
        elif cmd == "/export":
            if not self.conversation:
                self._add_system_message("No data in buffer to export.")
            else:
                stamp = time.strftime("%Y%m%d_%H%M%S")
                Path("exports").mkdir(exist_ok=True)
                out_path = Path("exports") / f"transcript_{stamp}.json"
                out_path.write_text(json.dumps(self.conversation, indent=2, ensure_ascii=False), encoding="utf-8")
                self._add_system_message(f"Transcript saved to {out_path}")
        elif cmd == "/timer":
            if not self._last_timer:
                self._add_system_message("Latency tracker empty.")
            else:
                lines = [f"  {_model_display(m)}: {t:.2f}s" for m, t in self._last_timer.items()]
                self._add_system_message("LAST EXECUTION LATENCY:\n" + "\n".join(lines))
        elif cmd == "/usage":
            total = sum(len(m["content"]) // 4 for m in self.conversation)
            self._add_system_message(f"Current session token count (est): {total:,}")
        else:
            self._add_system_message(f"Command not recognized: {cmd}")

    def _handle_room_command(self, action: str, arg: str) -> None:
        if not action:
            self._add_system_message("Usage: /room <create|join|status|leave|close>")
            return
        try:
            if action == "create":
                room_name = arg or "New Room"
                room = asyncio.run(room_manager.create_room(room_name))
                code = room.get("room_code", "N/A")
                self._add_system_message(f"Network sector established: \"{room_name}\"\nRoom Code: {code}")
                self._on_room_joined()
            elif action == "join":
                if not arg:
                    self._add_system_message("Usage: /room join <CODE>")
                    return
                room = asyncio.run(room_manager.join_room(arg))
                self._add_system_message(f"Uplink established: \"{room.get('name')}\"")
                self._on_room_joined()
            elif action == "leave":
                asyncio.run(room_manager.leave_room())
                self._add_system_message("Uplink terminated.")
                self._on_room_left()
            elif action == "status":
                if not room_manager.active_room:
                    self._add_system_message("Uplink: INACTIVE")
                else:
                    r = room_manager.active_room
                    self._add_system_message(
                        f"UPLINK STATUS:\n"
                        f"  Sector: {r.get('name')}\n"
                        f"  Code:   {r.get('room_code')}\n"
                        f"  Peers:  {len(r.get('members', []))}"
                    )
            else:
                self._add_system_message(f"Unknown room protocol: {action}")
        except Exception as e:
            self._add_system_message(f"Room error: {e}")

    def _handle_project_command(self, action: str, arg: str) -> None:
        if not action:
            self._add_system_message("Usage: /project <create|files|save|open|delete|history|info>")
            return
        prof = local_profile.get()
        try:
            if action == "create":
                if not room_manager.active_room:
                    self._add_system_message("Active room required for project instantiation.")
                    return
                project_name = arg or "Untitled Project"
                project_id = f"proj_{uuid.uuid4().hex[:12]}"
                store = ProjectStore(project_id)
                meta = {
                    "project_id": project_id, "name": project_name, "room_id": room_manager.active_room.get("room_id", ""),
                    "created_by": prof["user_id"], "created_at": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
                }
                store.save_metadata(meta)
                if room_manager.is_host and room_manager.host_service:
                    room_manager.host_service.project_store = store
                elif room_manager.client_service:
                    room_manager.client_service.project_store = store
                room_manager.send_message_raw({"type": "project_created", "project_id": project_id, "name": project_name, "created_by_name": prof["username"]})
                self._add_system_message(f"Project Workspace Initialized: \"{project_name}\"")
                self._refresh_project_files()
            elif action == "files":
                store = self._get_project_store()
                if not store:
                    self._add_system_message("No workspace active.")
                    return
                files = store.list_files()
                if not files:
                    self._add_system_message("Workspace is currently empty.")
                else:
                    lines = [f"Workspace Assets ({len(files)}):"]
                    for f in files:
                        lines.append(f"  > {f['relative_path']} ({f['size_bytes']/1024:.1f} KB)")
                    self._add_system_message("\n".join(lines))
            elif action == "save":
                if not arg:
                    self._add_system_message("Usage: /project save <local-path>")
                    return
                store = self._get_project_store()
                if not store:
                    self._add_system_message("No workspace active.")
                    return
                source = Path(arg).expanduser().resolve()
                if not source.exists():
                    self._add_system_message(f"Local file not found: {arg}")
                    return
                content = source.read_bytes()
                result = store.add_file(source.name, content, user_id=prof["user_id"], user_name=prof["username"])
                room_manager.send_message_raw({
                    "type": "project_file_push", "relative_path": source.name,
                    "content_b64": base64.b64encode(content).decode("ascii"), "user_name": prof["username"]
                })
                self._add_system_message(f"Asset synchronized: {source.name}")
                self._refresh_project_files()
            elif action == "open":
                if not arg:
                    self._add_system_message("Usage: /project open <path>")
                    return
                store = self._get_project_store()
                if not store: return
                content = store.read_file(arg)
                if content:
                    self._add_chat_message(f"FILE: {arg}", content.decode("utf-8", "replace"), msg_type="system")
            elif action == "history":
                store = self._get_project_store()
                if not store: return
                changes = store.get_history(limit=10)
                lines = ["Recent Workspace Modifications:"]
                for c in changes:
                    lines.append(f"  {c['timestamp'][:16]} | {c['action']:6s} | {c['relative_path']} | {c['user_name']}")
                self._add_system_message("\n".join(lines))
            else:
                self._add_system_message(f"Protocol '{action}' not recognized for project.")
        except Exception as e:
            self._add_system_message(f"Workspace error: {e}")

    def _on_room_joined(self) -> None:
        self._refresh_room_list()
        self._refresh_member_list()
        self._update_center_header()
        self._start_ws_listener()
        if room_manager.is_host and room_manager.host_service:
            history = room_manager.host_service.chat_store.get_history()
            for msg in history:
                self._render_ws_chat_message(msg)
            original_broadcast = room_manager.host_service.broadcast
            async def _patched_broadcast(message):
                await original_broadcast(message)
                self.call_from_thread(self._handle_ws_message, message)
            room_manager.host_service.broadcast = _patched_broadcast

    def _on_room_left(self) -> None:
        self._ws_listener_active = False
        self._refresh_room_list()
        self._refresh_member_list()
        self._refresh_project_files()
        self._update_center_header()

    def _start_ws_listener(self):
        self._ws_listener_active = True
        self.run_worker(self._ws_listener_loop, thread=True, exclusive=False)

    def _ws_listener_loop(self):
        loop = asyncio.new_event_loop()
        async def _listen():
            while self._ws_listener_active:
                client = room_manager.client_service
                if client and client.ws and getattr(client, '_connected', False):
                    try:
                        msg = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
                        data = json.loads(msg)
                        self.call_from_thread(self._handle_ws_message, data)
                    except asyncio.TimeoutError: pass
                    except Exception: await asyncio.sleep(1)
                else: await asyncio.sleep(0.5)
        loop.run_until_complete(_listen())

    def _handle_ws_message(self, data: dict) -> None:
        msg_type = data.get("type")
        if msg_type == "chat":
            self._render_ws_chat_message(data)
        elif msg_type == "system":
            self._add_system_message(data.get("content", ""))
            self._refresh_member_list()
        elif msg_type == "project_sync" or msg_type.startswith("project_file_"):
            self._refresh_project_files()

    def _render_ws_chat_message(self, data: dict) -> None:
        prof = local_profile.get()
        sender_name = data.get("sender_name", "Unknown")
        sender_id = data.get("sender_id", "")
        content = data.get("content", "")
        if sender_id == prof.get("user_id"): return
        ai_names = {"Claude", "Gemini", "ChatGPT", "Llama", "Mistral", "AI"}
        if sender_name in ai_names or sender_name.startswith("["):
            self._add_chat_message(sender_name, content, msg_type="ai")
        else:
            self._add_chat_message(sender_name, content, msg_type="peer")

    def _route_to_ai(self, text: str) -> None:
        task_type, strategy = _classify(text)
        models = _pick_models(task_type, strategy, self.active_models)
        self._add_chat_message("YOU", text, msg_type="self")
        attached_files = _scan_file_paths(text)
        prompt = _prepend_file_contents(text, attached_files)
        if attached_files:
            self._add_system_message(f"CONTEXT ATTACHED: {len(attached_files)} file(s)")
        self.conversation.append({"role": "user", "content": text})
        
        if task_type == "action":
            result = _try_direct_action(text)
            if result:
                self._add_system_message(result)
                self.conversation.append({"role": "assistant", "content": result})
                return
                
        self.run_worker(lambda: self._execute_strategy(prompt, task_type, strategy, models), thread=True, exclusive=True)

    def _execute_strategy(self, user_input: str, task_type: str, strategy: str, models: list[str]) -> None:
        if not models:
            self._post_system_message("CRITICAL ERROR: No active models for execution.")
            return
        if strategy == "debate": self._run_debate(user_input, models)
        elif strategy == "refine": self._run_refine(user_input, models)
        elif strategy == "fan_out": self._run_fan_out(user_input, models)
        else: self._run_single(user_input, models)

    def _invoke_model(self, model: str, prompt: str) -> tuple[str, float]:
        from runtime.executor.pipeline.model_router import call_model
        self._set_model_state(model, "active")
        start = time.time()
        history = [(m.get("role", "user"), "Unknown", m.get("content", "")) for m in self.conversation]
        result = call_model(model, prompt, chat_history=history, config=self.config)
        elapsed = time.time() - start
        self._set_model_state(model, "done")
        self._last_timer[model] = elapsed
        return result, elapsed

    def _run_single(self, user_input: str, models: list[str]) -> None:
        model = models[0]
        self._post_system_message(f"STRATEGY: SINGLE-MODEL [{_model_display(model)}]")
        result, elapsed = self._invoke_model(model, user_input)
        self._post_chat_message(f"[{_model_display(model)}]", result, msg_type="ai")
        self.conversation.append({"role": "assistant", "content": result[:1000]})
        self._reset_model_states()

    def _run_debate(self, user_input: str, models: list[str]) -> None:
        model_a = models[0]
        model_b = models[1] if len(models) > 1 else models[0]
        judge = models[2] if len(models) > 2 else models[0]

        self._post_system_message(f"STRATEGY: DEBATE [{_model_display(model_a)} vs {_model_display(model_b)}]")
        q1 = queue.Queue()
        q2 = queue.Queue()

        def t1(): q1.put(self._invoke_model(model_a, f"Argue FOR the following prompt. Be concise.\n{user_input}"))
        def t2(): q2.put(self._invoke_model(model_b, f"Argue AGAINST the following prompt. Be concise.\n{user_input}"))

        th1, th2 = threading.Thread(target=t1), threading.Thread(target=t2)
        th1.start(); th2.start()
        th1.join(); th2.join()

        res_a, _ = q1.get(); res_b, _ = q2.get()
        self._post_chat_message(f"[{_model_display(model_a)} (PRO)]", res_a, msg_type="ai")
        self._post_chat_message(f"[{_model_display(model_b)} (CON)]", res_b, msg_type="ai")
        
        self._post_system_message(f"STRATEGY: SYNTHESIS via {_model_display(judge)}")
        res_j, _ = self._invoke_model(judge, f"Synthesize these perspectives on '{user_input}':\n\nPRO:\n{res_a}\n\nCON:\n{res_b}")
        self._post_chat_message(f"[{_model_display(judge)} (JUDGE)]", res_j, msg_type="ai")
        self.conversation.append({"role": "assistant", "content": res_j[:1000]})
        self._reset_model_states()

    def _run_refine(self, user_input: str, models: list[str]) -> None:
        drafter, critique, refiner = models[0], models[1] if len(models) > 1 else models[0], models[2] if len(models) > 2 else models[0]
        self._post_system_message(f"STRATEGY: REFINE [Drafting: {_model_display(drafter)}]")
        draft, _ = self._invoke_model(drafter, user_input)
        self._post_chat_message(f"[{_model_display(drafter)} (DRAFT)]", draft, msg_type="ai")
        
        self._post_system_message(f"STRATEGY: REFINE [Critique: {_model_display(critique)}]")
        crit, _ = self._invoke_model(critique, f"Critique this draft for '{user_input}':\n\n{draft}")
        self._post_chat_message(f"[{_model_display(critique)} (CRITIQUE)]", crit, msg_type="ai")
        
        self._post_system_message(f"STRATEGY: REFINE [Finalizing: {_model_display(refiner)}]")
        final, _ = self._invoke_model(refiner, f"Original goal: {user_input}\n\nDraft: {draft}\n\nCritique: {crit}\n\nProduce final version.")
        self._post_chat_message(f"[{_model_display(refiner)} (FINAL)]", final, msg_type="ai")
        self.conversation.append({"role": "assistant", "content": final[:1000]})
        self._reset_model_states()

    def _run_fan_out(self, user_input: str, models: list[str]) -> None:
        self._post_system_message(f"STRATEGY: FAN-OUT [{len(models)} models]")
        qs = [queue.Queue() for _ in models]
        def call_m(m: str, q: queue.Queue): q.put(self._invoke_model(m, user_input))
        threads = [threading.Thread(target=call_m, args=(m, q)) for m, q in zip(models, qs)]
        for th in threads: th.start()
        for th in threads: th.join()
        for m, q in zip(models, qs):
            res, _ = q.get()
            self._post_chat_message(f"[{_model_display(m)}]", res, msg_type="ai")
        self._reset_model_states()

def run_tui(config: RuntimeConfig) -> None:
    app = BrynqChat(config)
    app.run()

if __name__ == "__main__":
    _test_config = RuntimeConfig(ollama_url="http://localhost:11434", cloud_url="http://localhost:8000")
    run_tui(_test_config)
