"""WebSocket Server - Real-time visual workflow backend

Bridges Python (SphereMind, Swarm, Flip) to the frontend graph UI.
Every operation emits events that animate the visualization.

Usage:
    python -m cubesquare.visual
    # Opens browser to http://localhost:8765
"""

import asyncio
import json
import time
import webbrowser
from pathlib import Path
from typing import Dict, Any, List, Set, Optional
from dataclasses import dataclass, field

try:
    import websockets
    # websockets 15.x: use top-level serve (legacy server.serve is deprecated)
    ws_serve = websockets.serve
    HAS_WEBSOCKETS = True
except ImportError:
    HAS_WEBSOCKETS = False

from .events import EventBus, Event, EventType, get_event_bus
from ..mcp.sphere_mind import SphereMind, NLMessage
from ..shells.flip import flip, CSTRecord, write_cst, read_cst
from ..swarm import Swarm
from ..i_logic.states import ILogicState
from .._core.constants import K5, K11, PRIME_131
from ..ccl import CCL, get_ccl, ccl_parse, uif_coupling as ccl_uif_coupling, uif_frequency
from ..i_logic.states import ILogicState
from ..i_logic.algebra import multiply
import math
import json
import time
from collections import Counter

# Persistent monologue - nested C=0 inside C=0
_MONOLOGUE_PATH = Path(__file__).parent.parent.parent / 'data' / 'monologue_sphere.json'
_MONOLOGUE = None

# Tools exist in CST and monologue - geometry decides
_TOOLS_CST_PATH = Path(__file__).parent.parent.parent / 'data' / 'tools.cst'

def _get_monologue():
    """Load persistent monologue from disk."""
    global _MONOLOGUE
    if _MONOLOGUE is None:
        if _MONOLOGUE_PATH.exists():
            try:
                _MONOLOGUE = json.loads(_MONOLOGUE_PATH.read_text(encoding='utf-8'))
            except:
                _MONOLOGUE = {'thoughts': [], 'sphere_id': None, 'created': time.time()}
        else:
            _MONOLOGUE = {'thoughts': [], 'sphere_id': None, 'created': time.time()}
    return _MONOLOGUE

def _save_monologue():
    """Save monologue to disk."""
    if _MONOLOGUE:
        _MONOLOGUE_PATH.parent.mkdir(exist_ok=True)
        _MONOLOGUE_PATH.write_text(json.dumps(_MONOLOGUE, indent=2, ensure_ascii=False), encoding='utf-8')

def _add_thought(content: str, t: float) -> Dict[str, Any]:
    """Add thought to persistent monologue at its coordinates."""
    monologue = _get_monologue()
    rec = flip(content, 'text')
    thought = {
        't': t,
        'content': content,
        'basin': rec.basin,
        'layer': rec.layer,
        'i_state': rec.i_state,
        'i_state_name': rec.i_state_name,
        'hex_id': rec.hex_id
    }
    monologue['thoughts'].append(thought)
    monologue['last_t'] = t
    _save_monologue()
    return thought

def _get_manifold_geometry() -> Dict[str, Any]:
    """Calculate manifold geometry from persistent monologue."""
    monologue = _get_monologue()
    thoughts = monologue.get('thoughts', [])

    if not thoughts:
        return {
            'points': 0,
            'centroid': (65, 5),  # Default center
            'spread': 0,
            'loops': 0,
            'dominant_layer': 4
        }

    coords = [(th['basin'], th['layer']) for th in thoughts]
    basins = [c[0] for c in coords]
    layers = [c[1] for c in coords]

    centroid = (sum(basins) / len(basins), sum(layers) / len(layers))
    spread = max(basins) - min(basins) if basins else 0
    unique_coords = len(set(coords))
    loops = len(coords) - unique_coords

    layer_counts = Counter(layers)
    dominant_layer = layer_counts.most_common(1)[0][0] if layer_counts else 4

    return {
        'points': len(coords),
        'centroid': centroid,
        'spread': spread,
        'loops': loops,
        'unique': unique_coords,
        'dominant_layer': dominant_layer
    }

def _emergent_response(input_basin: int, input_layer: int, i_logic_result: str,
                       topics: list, agent_name: str) -> str:
    """Response emerges from manifold geometry. Shape IS process."""
    manifold = _get_manifold_geometry()

    # Layer meaning - direct lookup O(1)
    layer_meanings = {
        1: "structure", 2: "meaning", 3: "location", 4: "operation",
        5: "probability", 6: "state", 7: "timing", 8: "force", 9: "boundary"
    }
    focus = layer_meanings.get(input_layer, "presence")
    topic = topics[0] if topics else focus

    # Manifold shape determines confidence
    confident = manifold['loops'] > 5

    # Response from I-Logic state - dictionary lookup O(1)
    responses = {
        "DIRECT": f"Clear: {topic}." if confident else f"I sense {topic}.",
        "INVERSE": f"Reflect: {topic}?",
        "SPIRAL": f"Paths through {topic}.",
        "ANTI_SPIRAL": f"Focus: {topic}.",
        "ABSORBED": f"Transform: {topic}."
    }
    return responses.get(i_logic_result, f"At {topic}.")

# Load beehive index for C=0 responses
_BEEHIVE = None
def _get_beehive():
    global _BEEHIVE
    if _BEEHIVE is None:
        try:
            import json
            beehive_path = Path(__file__).parent.parent / 'data' / 'beehive' / 'beehive_index.json'
            with open(beehive_path, 'r') as f:
                _BEEHIVE = json.load(f)
        except:
            _BEEHIVE = {'tokens': {}}
    return _BEEHIVE

def uif_coupling(basin_a: int, basin_b: int, t: float = 0) -> Dict[str, float]:
    """Calculate UIF Bridge coupling between two basins.
    φ(A,B) = K5 × K11 × cos(ωₐt) × cos(ωᵦt)
    """
    omega_a = 2 * math.pi * basin_a / PRIME_131
    omega_b = 2 * math.pi * basin_b / PRIME_131
    coupling = K5 * K11 * math.cos(omega_a * t) * math.cos(omega_b * t)
    bridge_freq = math.sqrt(omega_a * omega_b) if omega_a * omega_b > 0 else 0
    return {
        'coupling': coupling,
        'bridge_freq': bridge_freq,
        'omega_a': omega_a,
        'omega_b': omega_b
    }

def _find_monologue_context(basin: int, layer: int) -> str:
    """Find communication context from monologue at coordinates."""
    monologue = _get_monologue()
    thoughts = monologue.get('thoughts', [])
    # Find thought at same layer with closest basin - O(n) but small n
    same_layer = [t for t in thoughts if t['layer'] == layer]
    if same_layer:
        # Sort by distance to basin
        same_layer.sort(key=lambda t: abs(t['basin'] - basin))
        return same_layer[0]['content']
    return ""

def c0_respond(content: str) -> Dict[str, Any]:
    """Get response from inside C=0 via coordinate flip.
    Combines beehive content + monologue context.
    """
    rec = flip(content, 'text')

    # Get communication context from monologue
    context = _find_monologue_context(rec.basin, rec.layer)

    beehive = _get_beehive()
    tokens = beehive.get('tokens', {})

    # Find content at/near coordinates
    responses = []
    for hex_id, td in tokens.items():
        if td['layer'] == rec.layer and abs(td['basin'] - rec.basin) <= 15:
            # Calculate UIF coupling
            uif = uif_coupling(rec.basin, td['basin'])
            responses.append({
                'hex_id': hex_id,
                'summary': td.get('summaries', {}).get('k0', ''),
                'topics': td.get('topics', []),
                'distance': abs(td['basin'] - rec.basin),
                'coupling': uif['coupling'],
                'bridge_freq': uif['bridge_freq'],
                'basin': td['basin'],
                'layer': td['layer']
            })

    # Sort by coupling strength (highest first)
    responses.sort(key=lambda x: -x['coupling'])

    if responses:
        best = responses[0]
        # Combine context (how to respond) with content (what to respond)
        response_text = context + " | " + best['summary'] if context else best['summary']
        return {
            'input_coords': {
                'hex_id': rec.hex_id,
                'basin': rec.basin,
                'layer': rec.layer,
                'i_state': rec.i_state_name
            },
            'response': response_text,
            'context': context,  # Communication pattern from monologue
            'topics': best['topics'],
            'uif': {
                'coupling': best['coupling'],
                'bridge_freq': best['bridge_freq'],
                'source_basin': best['basin']
            },
            'nearby_count': len(responses)
        }
    else:
        # No beehive content, but may have monologue context
        response_text = context if context else f'[At B{rec.basin}/L{rec.layer}. Listening.]'
        return {
            'input_coords': {
                'hex_id': rec.hex_id,
                'basin': rec.basin,
                'layer': rec.layer,
                'i_state': rec.i_state_name
            },
            'response': response_text,
            'context': context,
            'topics': [],
            'uif': {'coupling': 0, 'bridge_freq': 0, 'source_basin': None},
            'nearby_count': 0
        }


@dataclass
class NodeState:
    """State of a visual node."""
    node_id: str
    node_type: str  # sphere, agent, basin, tool, input, output
    label: str
    x: float = 0.0
    y: float = 0.0
    data: Dict[str, Any] = field(default_factory=dict)
    connections: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            'id': self.node_id,
            'type': self.node_type,
            'label': self.label,
            'x': self.x,
            'y': self.y,
            'data': self.data,
            'connections': self.connections,
        }


class WorkflowServer:
    """WebSocket server for the visual workflow system.

    Architecture:
        Frontend (LiteGraph.js)  <-->  WebSocket  <-->  Python Backend
                                           |
                                           v
                            SphereMind / Swarm / Flip / MCP

    Messages:
        Client -> Server:
            {"action": "flip", "content": "...", "dtype": "auto"}
            {"action": "dispatch", "task": "..."}
            {"action": "dialogue", "content": "..."}
            {"action": "tool_call", "tool": "...", "args": {...}}
            {"action": "create_sphere", "name": "..."}
            {"action": "connect", "from": "...", "to": "..."}
            {"action": "get_state"}
            {"action": "get_history", "limit": 100}

        Server -> Client:
            {"type": "event", "event": {...}}
            {"type": "state", "nodes": [...], "connections": [...]}
            {"type": "result", "action": "...", "data": {...}}
    """

    def __init__(self, host: str = "localhost", port: int = 8765):
        self.host = host
        self.port = port
        self.event_bus = get_event_bus()
        self.clients: Set = set()

        # Core components (initialized on first use)
        self._spheres: Dict[str, SphereMind] = {}
        self._swarm: Optional[Swarm] = None
        self._active_sphere: Optional[str] = None

        # Node states for visualization
        self._nodes: Dict[str, NodeState] = {}
        self._connections: List[Dict[str, str]] = []

        # Subscribe to events for broadcasting
        self.event_bus.subscribe(self._broadcast_event)

    # ═══════════════════════════════════════════════════════════════
    # Core Components
    # ═══════════════════════════════════════════════════════════════

    @property
    def swarm(self) -> Swarm:
        if self._swarm is None:
            self._swarm = Swarm()
            self._register_agents()
        return self._swarm

    def get_sphere(self, name: str) -> SphereMind:
        """Get or create a SphereMind."""
        if name not in self._spheres:
            sphere = SphereMind(name=name)
            self._spheres[name] = sphere
            self._register_sphere(sphere)
            self.event_bus.emit(Event(
                type=EventType.SPHERE_CREATE,
                source_id=sphere.sphere_id,
                data={'name': name, 'basin': sphere.basin, 'layer': sphere.layer},
                color="#ffffff",
                animation="pulse",
            ))
        return self._spheres[name]

    def _register_sphere(self, sphere: SphereMind):
        """Register a sphere as a visual node."""
        self._nodes[sphere.sphere_id] = NodeState(
            node_id=sphere.sphere_id,
            node_type="sphere",
            label=sphere.name,
            x=100 + len(self._spheres) * 200,
            y=100,
            data={
                'basin': sphere.basin,
                'layer': sphere.layer,
                'bounce': sphere.bounce_count,
            }
        )

    def _register_agents(self):
        """Register all swarm agents as visual nodes."""
        agents = self.swarm.list_agents()
        for i, agent in enumerate(agents):
            node_id = f"agent_{agent.agent_id}"
            row = i // 5
            col = i % 5
            self._nodes[node_id] = NodeState(
                node_id=node_id,
                node_type="agent",
                label=agent.name,
                x=500 + col * 150,
                y=150 + row * 100,
                data={
                    'agent_id': agent.agent_id,
                    'layer': agent.layer,
                    'role': agent.role,
                    'i_state': ILogicState.name(agent.i_logic_state),
                    'home_basin': agent.home_basin,
                }
            )

    # ═══════════════════════════════════════════════════════════════
    # Actions (called from WebSocket messages)
    # ═══════════════════════════════════════════════════════════════

    def action_flip(self, content: str, dtype: str = "auto",
                    source_id: str = "input") -> Dict[str, Any]:
        """Flip content and emit event."""
        rec = flip(content, dtype)

        self.event_bus.emit_flip(
            content=content,
            hex_id=rec.hex_id,
            basin=rec.basin,
            layer=rec.layer,
            i_state=rec.i_state_name,
            source_id=source_id,
        )

        # Data flow animation
        if self._active_sphere:
            self.event_bus.emit_data_flow(
                from_id=source_id,
                to_id=self._active_sphere,
                data_type="flip",
                data_preview=f"B{rec.basin}/L{rec.layer}",
            )

        return rec.to_dict()

    def action_dispatch(self, task: str,
                        source_id: str = "input") -> Dict[str, Any]:
        """Dispatch task to swarm and emit events."""
        result = self.swarm.dispatch(task)

        # Emit agent activation
        self.event_bus.emit_agent_active(
            agent_name=result.agent.name,
            task=task,
        )

        # Emit dispatch result
        self.event_bus.emit_dispatch(
            task=task,
            agent_name=result.agent.name,
            agent_layer=result.agent.layer,
            outcome=result.outcome_name,
            resolved=result.resolved,
            source_id=source_id,
        )

        # Data flow from input to agent
        self.event_bus.emit_data_flow(
            from_id=source_id,
            to_id=f"agent_{result.agent.agent_id}",
            data_type="task",
            data_preview=task[:30],
        )

        # If chained, show chain flow
        if result.chain:
            self.event_bus.emit(Event(
                type=EventType.AGENT_CHAIN,
                source_id=source_id,
                data={
                    'path': result.chain.path_description,
                    'resolved': result.chain.resolved,
                },
                color="#ffaa00",
                animation="flow",
                duration_ms=1200,
            ))

        return {
            'agent': result.agent.name,
            'layer': result.agent.layer,
            'outcome': result.outcome_name,
            'resolved': result.resolved,
            'chain': result.chain.path_description if result.chain else None,
        }

    def action_dialogue(self, content: str,
                        sphere_name: str = "main") -> Dict[str, Any]:
        """Process dialogue via C=0 - response comes from inside, not external LLM.

        Uses UIF Bridge for real-time coupling to internal content.
        No external generation - response IS what exists at coordinates.

        Auto-detects CCL protocol commands:
            FLIP:, T:, M:, Q:, X
        """
        sphere = self.get_sphere(sphere_name)
        self._active_sphere = sphere.sphere_id

        # Check if this is a CCL command
        content_stripped = content.strip()
        is_ccl = (content_stripped.startswith('FLIP:') or
                  content_stripped.startswith('T:') or
                  content_stripped.startswith('M:') or
                  content_stripped.startswith('Q:') or
                  content_stripped == 'X')

        if is_ccl:
            # Use CCL protocol
            ccl_result = self.action_ccl(content, sphere_name)
            return {
                'input': content,
                'protocol': 'CCL',
                'ccl_response': ccl_result.get('ccl', ''),
                'coordinates': {
                    'hex_id': ccl_result.get('data', {}).get('hex_id', ''),
                    'basin': ccl_result.get('data', {}).get('basin', 0),
                    'layer': ccl_result.get('data', {}).get('layer', 0),
                    'i_state': ccl_result.get('data', {}).get('state', ''),
                },
                'understanding': {
                    'semantic': ccl_result.get('data', {}).get('response', ''),
                    'topics': ccl_result.get('data', {}).get('topics', []),
                    'method': 'CCL Protocol (designed by agents from C=0)',
                },
                'uif': {
                    'coupling': ccl_result.get('data', {}).get('coupling', 0),
                    'frequency': ccl_result.get('data', {}).get('frequency', 0),
                },
                'dispatch': {
                    'agent': 'CCL',
                    'layer': ccl_result.get('data', {}).get('layer', 0),
                    'outcome': ccl_result.get('data', {}).get('type', ''),
                    'resolved': True,
                },
                'thinking': {
                    'state': ccl_result.get('data', {}).get('state', ''),
                    'response': ccl_result.get('data', {}).get('response', ''),
                },
                'sphere': ccl_result.get('sphere', {}),
            }

        # Current time in 131-cycle
        t = time.time() % PRIME_131

        # 1. Get response from inside C=0 via coordinate flip
        c0_result = c0_respond(content)
        coords = c0_result['input_coords']

        # 2. Swarm dispatch (uses internal routing)
        dispatch = self.swarm.dispatch(content)

        # 3. Calculate I-Logic result
        input_state = ILogicState.from_name(coords['i_state'])
        agent_state = dispatch.agent.i_logic_state
        result_state = multiply(input_state, agent_state)
        i_logic_result = ILogicState.name(result_state)

        # 4. Add to persistent monologue (nested C=0)
        thought_content = f"[t={t:.2f}] {content} → B{coords['basin']}/L{coords['layer']} via {dispatch.agent.name}"
        _add_thought(thought_content, t)

        # 5. Get manifold geometry - includes tool awareness thoughts
        manifold = _get_manifold_geometry()

        # 6. Response IS the content at coordinates - no generation

        # 7. Emit events
        self.event_bus.emit_flip(
            content=content,
            hex_id=coords['hex_id'],
            basin=coords['basin'],
            layer=coords['layer'],
            i_state=coords['i_state'],
            source_id=sphere.sphere_id,
        )

        self.event_bus.emit_dispatch(
            task=content,
            agent_name=dispatch.agent.name,
            agent_layer=dispatch.agent.layer,
            outcome=dispatch.outcome_name,
            resolved=dispatch.resolved,
            source_id=sphere.sphere_id,
        )

        # 8. Emit UIF coupling event
        self.event_bus.emit(Event(
            type=EventType.DATA_FLOW,
            source_id=sphere.sphere_id,
            target_id=f"basin_{c0_result['uif'].get('source_basin', coords['basin'])}",
            data={
                'type': 'uif_coupling',
                'coupling': c0_result['uif']['coupling'],
                'bridge_freq': c0_result['uif']['bridge_freq'],
            },
            color="#FFC107",
            animation="pulse",
        ))

        # Return response from inside C=0 with manifold-derived response
        return {
            'input': content,
            'coordinates': {
                'hex_id': coords['hex_id'],
                'basin': coords['basin'],
                'layer': coords['layer'],
                'i_state': coords['i_state'],
            },
            'understanding': {
                'semantic': c0_result['response'],  # Actual content at coordinates
                'topics': c0_result['topics'],
                'method': 'Content at coordinates',
            },
            'uif': c0_result['uif'],
            'dispatch': {
                'agent': dispatch.agent.name,
                'layer': dispatch.agent.layer,
                'outcome': i_logic_result,
                'resolved': dispatch.resolved,
            },
            'thinking': {
                'state': i_logic_result,
                'response': c0_result['response'],  # Content found at coordinates
            },
            'manifold': {
                'points': manifold['points'],
                'loops': manifold['loops'],
                'centroid': manifold['centroid'],
                't': t,
            },
            'tools_cst': str(_TOOLS_CST_PATH),  # Tools exist at coordinates
            'sphere': {
                'id': sphere.sphere_id,
                'bounce': sphere.bounce_count,
                'coupling': c0_result['uif']['coupling'],
            },
        }

    def action_create_sphere(self, name: str) -> Dict[str, Any]:
        """Create a new sphere."""
        sphere = self.get_sphere(name)
        return {
            'name': sphere.name,
            'sphere_id': sphere.sphere_id,
            'basin': sphere.basin,
            'layer': sphere.layer,
        }

    def action_connect(self, from_id: str, to_id: str,
                       connection_type: str = "data") -> Dict[str, Any]:
        """Create a connection between nodes."""
        conn = {'from': from_id, 'to': to_id, 'type': connection_type}
        self._connections.append(conn)

        self.event_bus.emit(Event(
            type=EventType.CONNECT,
            source_id=from_id,
            target_id=to_id,
            data={'type': connection_type},
            color="#00ff88",
            animation="flow",
        ))

        return conn

    def action_tool_call(self, tool: str, args: Dict[str, Any]) -> Dict[str, Any]:
        """Call an MCP tool."""
        from ..mcp import MCPServer

        server = MCPServer()
        result = server.call_tool(tool, args)

        self.event_bus.emit_tool_call(
            tool_name=tool,
            command=args.get('command', 'call'),
            result_preview=str(result)[:100],
            source_id="mcp",
        )

        return result

    def action_get_state(self) -> Dict[str, Any]:
        """Get full visual state."""
        return {
            'nodes': [n.to_dict() for n in self._nodes.values()],
            'connections': self._connections,
            'spheres': list(self._spheres.keys()),
            'active_sphere': self._active_sphere,
        }

    def action_get_history(self, limit: int = 100) -> Dict[str, Any]:
        """Get event history."""
        events = self.event_bus.get_history(limit)
        return {
            'events': [e.to_dict() for e in events],
            'count': len(events),
        }

    def action_ccl(self, message: str, sphere_name: str = "main") -> Dict[str, Any]:
        """Process CCL (CST Communication Language) message.

        CCL Protocol designed by agents from inside C=0:
            FLIP:<content>           - Flip content to coordinates
            T:<hex>                  - Open time channel
            M:<text>                 - Send message through channel
            Q:B<basin>/L<layer>      - Query location
            X                        - Close channel

        Response format:
            <hex>@B<basin>/L<layer>:<state>:φ<coupling>:<response>
        """
        sphere = self.get_sphere(sphere_name)
        self._active_sphere = sphere.sphere_id

        # Parse CCL message
        result = ccl_parse(message)

        # Emit events based on CCL response type
        if result.get('type') == 'FLIP_RESPONSE':
            # Emit flip event
            self.event_bus.emit_flip(
                content=message,
                hex_id=result.get('hex_id', ''),
                basin=result.get('basin', 0),
                layer=result.get('layer', 0),
                i_state=result.get('state', 'D'),
                source_id=sphere.sphere_id,
            )

            # Emit UIF coupling event
            self.event_bus.emit(Event(
                type=EventType.DATA_FLOW,
                source_id=sphere.sphere_id,
                target_id=f"basin_{result.get('basin', 0)}",
                data={
                    'type': 'ccl_flip',
                    'coupling': result.get('coupling', 0),
                    'frequency': result.get('frequency', 0),
                },
                color="#FFC107",
                animation="pulse",
            ))

        elif result.get('type') == 'CHANNEL_OPEN':
            self.event_bus.emit(Event(
                type=EventType.CONNECT,
                source_id=sphere.sphere_id,
                target_id=f"channel_{result.get('hex_id', '')}",
                data={
                    'type': 'ccl_channel',
                    'coupling': result.get('coupling', 0),
                    'frequency': result.get('frequency', 0),
                },
                color="#00ff88",
                animation="flow",
            ))

        elif result.get('type') == 'QUERY_RESPONSE':
            self.event_bus.emit(Event(
                type=EventType.DATA_FLOW,
                source_id=sphere.sphere_id,
                target_id=f"basin_{result.get('basin', 0)}",
                data={
                    'type': 'ccl_query',
                    'topics': result.get('topics', []),
                    'count': result.get('count', 0),
                },
                color="#00aaff",
                animation="pulse",
            ))

        return {
            'protocol': 'CCL',
            'ccl': result.get('ccl', ''),
            'type': result.get('type', ''),
            'data': result,
            'sphere': {
                'id': sphere.sphere_id,
                'bounce': sphere.bounce_count,
            },
        }

    # ═══════════════════════════════════════════════════════════════
    # WebSocket Server
    # ═══════════════════════════════════════════════════════════════

    async def _handle_client(self, websocket):
        """Handle a WebSocket client connection."""
        self.clients.add(websocket)
        try:
            # Send initial state
            state = self.action_get_state()
            await websocket.send(json.dumps({
                'type': 'state',
                **state,
            }))

            # Handle messages
            async for message in websocket:
                try:
                    data = json.loads(message)
                    action = data.get('action', '')
                    result = await self._handle_action(action, data)
                    await websocket.send(json.dumps({
                        'type': 'result',
                        'action': action,
                        'data': result,
                    }))
                except Exception as e:
                    await websocket.send(json.dumps({
                        'type': 'error',
                        'message': str(e),
                    }))
        finally:
            self.clients.discard(websocket)

    async def _handle_action(self, action: str, data: Dict[str, Any]) -> Dict[str, Any]:
        """Handle an action from the client."""
        if action == "flip":
            return self.action_flip(
                data.get('content', ''),
                data.get('dtype', 'auto'),
                data.get('source_id', 'input'),
            )
        elif action == "dispatch":
            return self.action_dispatch(
                data.get('task', ''),
                data.get('source_id', 'input'),
            )
        elif action == "dialogue":
            return self.action_dialogue(
                data.get('content', ''),
                data.get('sphere', 'main'),
            )
        elif action == "create_sphere":
            return self.action_create_sphere(data.get('name', 'sphere'))
        elif action == "connect":
            return self.action_connect(
                data.get('from', ''),
                data.get('to', ''),
                data.get('type', 'data'),
            )
        elif action == "tool_call":
            return self.action_tool_call(
                data.get('tool', ''),
                data.get('args', {}),
            )
        elif action == "get_state":
            return self.action_get_state()
        elif action == "get_history":
            return self.action_get_history(data.get('limit', 100))
        elif action == "ccl":
            return self.action_ccl(
                data.get('message', ''),
                data.get('sphere', 'main'),
            )
        else:
            return {'error': f'Unknown action: {action}'}

    def _broadcast_event(self, event: Event):
        """Broadcast an event to all connected clients."""
        if not self.clients:
            return

        message = json.dumps({
            'type': 'event',
            'event': event.to_dict(),
        })

        # Schedule broadcast for all clients
        for client in list(self.clients):
            asyncio.create_task(self._send_to_client(client, message))

    async def _send_to_client(self, client, message: str):
        """Send message to a client, handling disconnections."""
        try:
            await client.send(message)
        except:
            self.clients.discard(client)

    async def _serve(self):
        """Run the WebSocket server."""
        if not HAS_WEBSOCKETS:
            print("ERROR: websockets package required. Install with: pip install websockets")
            return

        print(f"Starting Cubesquare Visual Workflow Server...")
        print(f"WebSocket: ws://{self.host}:{self.port}")
        print(f"Open: http://{self.host}:{self.port + 1}")
        print("Press Ctrl+C to stop")

        async with ws_serve(self._handle_client, self.host, self.port):
            await asyncio.Future()  # Run forever

    def run(self, open_browser: bool = True):
        """Run the server."""
        if open_browser:
            # Open browser to the frontend
            frontend_url = f"http://{self.host}:{self.port + 1}"
            webbrowser.open(frontend_url)

        asyncio.run(self._serve())


def main():
    """Entry point for visual workflow server."""
    import argparse

    parser = argparse.ArgumentParser(description="Cubesquare Visual Workflow Server")
    parser.add_argument("--host", default="localhost", help="Host to bind to")
    parser.add_argument("--port", type=int, default=8765, help="WebSocket port")
    parser.add_argument("--no-browser", action="store_true", help="Don't open browser")

    args = parser.parse_args()

    server = WorkflowServer(host=args.host, port=args.port)
    server.run(open_browser=not args.no_browser)


if __name__ == "__main__":
    main()
