package gateway

import (
	"context"
	"crypto/subtle"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"

	"github.com/nextlevelbuilder/goclaw/internal/agent"
	"github.com/nextlevelbuilder/goclaw/internal/bus"
	"github.com/nextlevelbuilder/goclaw/internal/config"
	httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
	mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
	"github.com/nextlevelbuilder/goclaw/internal/permissions"
	"github.com/nextlevelbuilder/goclaw/internal/providers"
	"github.com/nextlevelbuilder/goclaw/internal/store"
	"github.com/nextlevelbuilder/goclaw/internal/tools"
	"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)

// Server is the main gateway server handling WebSocket and HTTP connections.
type Server struct {
	cfg      *config.Config
	eventPub bus.EventPublisher
	agents   *agent.Router
	sessions store.SessionStore
	tools    *tools.Registry
	router   *MethodRouter

	policyEngine   *permissions.PolicyEngine
	pairingService store.PairingStore
	agentsHandler  *httpapi.AgentsHandler // agent CRUD API
	skillsHandler  *httpapi.SkillsHandler // skill management API
	tracesHandler  *httpapi.TracesHandler // LLM trace listing API
	wakeHandler    *httpapi.WakeHandler  // external wake/trigger API
	mcpHandler         *httpapi.MCPHandler         // MCP server management API
	customToolsHandler      *httpapi.CustomToolsHandler      // custom tool CRUD API
	channelInstancesHandler *httpapi.ChannelInstancesHandler // channel instance CRUD API
	providersHandler        *httpapi.ProvidersHandler        // provider CRUD API
	delegationsHandler      *httpapi.DelegationsHandler      // delegation history API
	teamEventsHandler       *httpapi.TeamEventsHandler       // team event history API
	builtinToolsHandler     *httpapi.BuiltinToolsHandler     // builtin tool management API
	pendingMessagesHandler  *httpapi.PendingMessagesHandler  // pending messages API
	secureCLIHandler       *httpapi.SecureCLIHandler        // secure CLI credential CRUD API
	packagesHandler        *httpapi.PackagesHandler         // runtime package management API
	memoryHandler           *httpapi.MemoryHandler           // memory management API
	kgHandler               *httpapi.KnowledgeGraphHandler   // knowledge graph API
	oauthHandler            *httpapi.OAuthHandler            // OAuth endpoints
	filesHandler            *httpapi.FilesHandler            // workspace file serving
	storageHandler          *httpapi.StorageHandler          // storage file management
	mediaUploadHandler      *httpapi.MediaUploadHandler      // media upload endpoint
	mediaServeHandler       *httpapi.MediaServeHandler       // media serve endpoint
	activityHandler         *httpapi.ActivityHandler         // activity audit log API
	usageHandler            *httpapi.UsageHandler            // usage analytics API
	apiKeysHandler     *httpapi.APIKeysHandler      // API key management
	apiKeyStore        store.APIKeyStore            // for API key auth lookup
	docsHandler        *httpapi.DocsHandler         // OpenAPI spec + Swagger UI
	agentStore         store.AgentStore             // for context injection in tools_invoke
	msgBus             *bus.MessageBus              // for MCP bridge media delivery

	upgrader    websocket.Upgrader
	rateLimiter *RateLimiter
	clients     map[string]*Client
	mu          sync.RWMutex

	startedAt time.Time
	version   string
	db        interface{ PingContext(context.Context) error } // for health check DB ping

	logTee *LogTee // optional; auto-unsubscribes clients on disconnect

	httpServer *http.Server
	mux        *http.ServeMux
}

// NewServer creates a new gateway server.
func NewServer(cfg *config.Config, eventPub bus.EventPublisher, agents *agent.Router, sess store.SessionStore, toolsReg ...*tools.Registry) *Server {
	s := &Server{
		cfg:       cfg,
		eventPub:  eventPub,
		agents:    agents,
		sessions:  sess,
		clients:   make(map[string]*Client),
		startedAt: time.Now(),
	}

	s.upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin:     s.checkOrigin,
	}

	if len(toolsReg) > 0 && toolsReg[0] != nil {
		s.tools = toolsReg[0]
	}

	// Initialize rate limiter.
	// rate_limit_rpm > 0  → enabled at that RPM
	// rate_limit_rpm == 0 → disabled (default, backward compat)
	// rate_limit_rpm < 0  → disabled explicitly
	s.rateLimiter = NewRateLimiter(cfg.Gateway.RateLimitRPM, 5)

	s.router = NewMethodRouter(s)
	return s
}

// RateLimiter returns the server's rate limiter for use by method handlers.
func (s *Server) RateLimiter() *RateLimiter { return s.rateLimiter }

// checkOrigin validates WebSocket connection origin against the allowed origins whitelist.
// If no origins are configured, all origins are allowed (backward compatibility / dev mode).
// Empty Origin header (non-browser clients like CLI/SDK) is always allowed.
func (s *Server) checkOrigin(r *http.Request) bool {
	allowed := s.cfg.Gateway.AllowedOrigins
	if len(allowed) == 0 {
		return true // no config = allow all (backward compat)
	}
	origin := r.Header.Get("Origin")
	if origin == "" {
		return true // non-browser clients (CLI, SDK, channels)
	}
	for _, a := range allowed {
		if origin == a || a == "*" {
			return true
		}
	}
	slog.Warn("security.cors_rejected", "origin", origin)
	return false
}

// BuildMux creates and caches the HTTP mux with all routes registered.
// Call this before Start() if you need the mux for additional listeners (e.g. Tailscale).
func (s *Server) BuildMux() *http.ServeMux {
	if s.mux != nil {
		return s.mux
	}

	mux := http.NewServeMux()

	// WebSocket endpoint
	mux.HandleFunc("/ws", s.handleWebSocket)

	// HTTP API endpoints
	mux.HandleFunc("/health", s.handleHealth)

	// OpenAI-compatible chat completions
	isManaged := s.agentStore != nil
	chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
	if s.rateLimiter.Enabled() {
		chatHandler.SetRateLimiter(s.rateLimiter.Allow)
	}
	mux.Handle("/v1/chat/completions", chatHandler)

	// OpenResponses protocol
	responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
	mux.Handle("/v1/responses", responsesHandler)

	// Direct tool invocation
	if s.tools != nil {
		toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
		mux.Handle("/v1/tools/invoke", toolsHandler)
	}

	// Agent CRUD + shares API
	if s.agentsHandler != nil {
		s.agentsHandler.RegisterRoutes(mux)
	}

	// Skill management API
	if s.skillsHandler != nil {
		s.skillsHandler.RegisterRoutes(mux)
	}

	// LLM trace listing API
	if s.tracesHandler != nil {
		s.tracesHandler.RegisterRoutes(mux)
	}

	// External wake/trigger API
	if s.wakeHandler != nil {
		s.wakeHandler.RegisterRoutes(mux)
	}

	// MCP server management API
	if s.mcpHandler != nil {
		s.mcpHandler.RegisterRoutes(mux)
	}

	// Custom tool CRUD API
	if s.customToolsHandler != nil {
		s.customToolsHandler.RegisterRoutes(mux)
	}

	// Secure CLI credential CRUD API
	if s.secureCLIHandler != nil {
		s.secureCLIHandler.RegisterRoutes(mux)
	}

	// Channel instance CRUD API
	if s.channelInstancesHandler != nil {
		s.channelInstancesHandler.RegisterRoutes(mux)
	}

	// Provider & model CRUD API
	if s.providersHandler != nil {
		s.providersHandler.RegisterRoutes(mux)
	}

	// Delegation history API
	if s.delegationsHandler != nil {
		s.delegationsHandler.RegisterRoutes(mux)
	}

	// Team event history API
	if s.teamEventsHandler != nil {
		s.teamEventsHandler.RegisterRoutes(mux)
	}

	// Builtin tool management API
	if s.builtinToolsHandler != nil {
		s.builtinToolsHandler.RegisterRoutes(mux)
	}

	// Pending messages API
	if s.pendingMessagesHandler != nil {
		s.pendingMessagesHandler.RegisterRoutes(mux)
	}

	// Memory management API
	if s.memoryHandler != nil {
		s.memoryHandler.RegisterRoutes(mux)
	}

	// Knowledge graph API
	if s.kgHandler != nil {
		s.kgHandler.RegisterRoutes(mux)
	}

	// Workspace file serving (available in all modes)
	if s.filesHandler != nil {
		s.filesHandler.RegisterRoutes(mux)
	}

	// Storage file management (browse/delete workspace files)
	if s.storageHandler != nil {
		s.storageHandler.RegisterRoutes(mux)
	}

	// Media upload endpoint (available in all modes)
	if s.mediaUploadHandler != nil {
		s.mediaUploadHandler.RegisterRoutes(mux)
	}

	// Media serve endpoint (available in all modes)
	if s.mediaServeHandler != nil {
		s.mediaServeHandler.RegisterRoutes(mux)
	}

	if s.apiKeysHandler != nil {
		s.apiKeysHandler.RegisterRoutes(mux)
	}

	if s.activityHandler != nil {
		s.activityHandler.RegisterRoutes(mux)
	}

	if s.usageHandler != nil {
		s.usageHandler.RegisterRoutes(mux)
	}

	if s.packagesHandler != nil {
		s.packagesHandler.RegisterRoutes(mux)
	}

	// API documentation (OpenAPI spec + Swagger UI)
	if s.docsHandler != nil {
		s.docsHandler.RegisterRoutes(mux)
	}

	// OAuth endpoints (available in all modes)
	if s.oauthHandler != nil {
		s.oauthHandler.RegisterRoutes(mux)
	}

	// MCP bridge: expose GoClaw tools to Claude CLI via streamable-http.
	// Only listens on localhost (CLI runs on the same machine).
	// Protected by gateway token when configured.
	// Agent context (X-Agent-ID, X-User-ID) is injected from request headers.
	if s.tools != nil {
		bridgeHandler := mcpbridge.NewBridgeServer(s.tools, "1.0.0", s.msgBus)
		var handler http.Handler = bridgeContextMiddleware(s.cfg.Gateway.Token, bridgeHandler)
		if s.cfg.Gateway.Token != "" {
			handler = tokenAuthMiddleware(s.cfg.Gateway.Token, handler)
		} else {
			slog.Warn("security.mcp_bridge: no gateway token configured, MCP bridge tools are unauthenticated")
		}
		mux.Handle("/mcp/bridge", handler)
	}

	s.mux = mux
	return mux
}

// bridgeContextMiddleware extracts X-Agent-ID and X-User-ID headers from the
// MCP bridge request and injects them into the context so bridge tools can
// access agent/user scope. When a gateway token is configured, the context
// headers must be accompanied by a valid X-Bridge-Sig HMAC to prevent forgery.
func bridgeContextMiddleware(gatewayToken string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		agentIDStr := r.Header.Get("X-Agent-ID")
		userID := r.Header.Get("X-User-ID")
		channel := r.Header.Get("X-Channel")
		chatID := r.Header.Get("X-Chat-ID")
		peerKind := r.Header.Get("X-Peer-Kind")

		if agentIDStr != "" || userID != "" {
			// Reject context headers when no gateway token — prevents unauthenticated impersonation.
			if gatewayToken == "" {
				slog.Warn("security.mcp_bridge: no gateway token, ignoring context headers",
					"agent_id", agentIDStr, "user_id", userID)
				next.ServeHTTP(w, r)
				return
			}

			// Verify HMAC signature over all context fields.
			sig := r.Header.Get("X-Bridge-Sig")
			if !providers.VerifyBridgeContext(gatewayToken, agentIDStr, userID, channel, chatID, peerKind, sig) {
				slog.Warn("security.mcp_bridge: invalid bridge context signature",
					"agent_id", agentIDStr, "user_id", userID)
				http.Error(w, `{"error":"invalid bridge context signature"}`, http.StatusForbidden)
				return
			}

			if agentIDStr != "" {
				if id, err := uuid.Parse(agentIDStr); err == nil {
					ctx = store.WithAgentID(ctx, id)
				}
			}
			if userID != "" {
				ctx = store.WithUserID(ctx, userID)
			}
		}

		// Inject channel routing context for tools like message, cron, etc.
		if channel != "" {
			ctx = tools.WithToolChannel(ctx, channel)
		}
		if chatID != "" {
			ctx = tools.WithToolChatID(ctx, chatID)
		}
		if peerKind != "" {
			ctx = tools.WithToolPeerKind(ctx, peerKind)
		}

		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// tokenAuthMiddleware wraps an http.Handler with Bearer token authentication.
func tokenAuthMiddleware(token string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		auth := r.Header.Get("Authorization")
		provided := strings.TrimPrefix(auth, "Bearer ")
		if !strings.HasPrefix(auth, "Bearer ") || subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
			http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Start begins listening for WebSocket and HTTP connections.
func (s *Server) Start(ctx context.Context) error {
	mux := s.BuildMux()

	addr := fmt.Sprintf("%s:%d", s.cfg.Gateway.Host, s.cfg.Gateway.Port)
	s.httpServer = &http.Server{
		Addr:    addr,
		Handler: mux,
	}

	slog.Info("gateway starting", "addr", addr)

	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		s.httpServer.Shutdown(shutdownCtx)
	}()

	if err := s.httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
		return fmt.Errorf("gateway server: %w", err)
	}
	return nil
}

// handleWebSocket upgrades HTTP to WebSocket and manages the connection.
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := s.upgrader.Upgrade(w, r, nil)
	if err != nil {
		slog.Error("websocket upgrade failed", "error", err)
		return
	}

	client := NewClient(conn, s, clientIP(r))
	s.registerClient(client)

	defer func() {
		s.unregisterClient(client)
		client.Close()
	}()

	client.Run(r.Context())
}

// handleHealth returns a simple health check response.
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, `{"status":"ok","protocol":%d}`, protocol.ProtocolVersion)
}

// clientIP extracts the real client IP from the request, checking proxy headers first.
func clientIP(r *http.Request) string {
	if ip := r.Header.Get("X-Real-IP"); ip != "" {
		return ip
	}
	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
		if i := strings.IndexByte(fwd, ','); i > 0 {
			return strings.TrimSpace(fwd[:i])
		}
		return strings.TrimSpace(fwd)
	}
	host, _, _ := net.SplitHostPort(r.RemoteAddr)
	return host
}

// Router returns the method router for registering additional handlers.
func (s *Server) Router() *MethodRouter { return s.router }

// SetPolicyEngine sets the permission policy engine for RPC method authorization.
func (s *Server) SetPolicyEngine(pe *permissions.PolicyEngine) { s.policyEngine = pe }

// SetPairingService sets the pairing service for channel authentication.
func (s *Server) SetPairingService(ps store.PairingStore) { s.pairingService = ps }

// SetAgentsHandler sets the agent CRUD handler.
func (s *Server) SetAgentsHandler(h *httpapi.AgentsHandler) { s.agentsHandler = h }

// SetSkillsHandler sets the skill management handler.
func (s *Server) SetSkillsHandler(h *httpapi.SkillsHandler) { s.skillsHandler = h }

// SetTracesHandler sets the LLM trace listing handler.
func (s *Server) SetTracesHandler(h *httpapi.TracesHandler) { s.tracesHandler = h }

// SetWakeHandler sets the external wake/trigger handler.
func (s *Server) SetWakeHandler(h *httpapi.WakeHandler) { s.wakeHandler = h }

// SetMCPHandler sets the MCP server management handler.
func (s *Server) SetMCPHandler(h *httpapi.MCPHandler) { s.mcpHandler = h }

// SetCustomToolsHandler sets the custom tool CRUD handler.
func (s *Server) SetCustomToolsHandler(h *httpapi.CustomToolsHandler) { s.customToolsHandler = h }

// SetChannelInstancesHandler sets the channel instance CRUD handler.
func (s *Server) SetChannelInstancesHandler(h *httpapi.ChannelInstancesHandler) {
	s.channelInstancesHandler = h
}

// SetProvidersHandler sets the provider CRUD handler.
func (s *Server) SetProvidersHandler(h *httpapi.ProvidersHandler) { s.providersHandler = h }

// SetDelegationsHandler sets the delegation history handler.
func (s *Server) SetDelegationsHandler(h *httpapi.DelegationsHandler) { s.delegationsHandler = h }

// SetTeamEventsHandler sets the team event history handler.
func (s *Server) SetTeamEventsHandler(h *httpapi.TeamEventsHandler) { s.teamEventsHandler = h }

// SetPendingMessagesHandler sets the pending messages handler.
func (s *Server) SetPendingMessagesHandler(h *httpapi.PendingMessagesHandler) {
	s.pendingMessagesHandler = h
}

// SetBuiltinToolsHandler sets the builtin tool management handler.
func (s *Server) SetBuiltinToolsHandler(h *httpapi.BuiltinToolsHandler) {
	s.builtinToolsHandler = h
}

// SetSecureCLIHandler sets the secure CLI credential CRUD handler.
func (s *Server) SetSecureCLIHandler(h *httpapi.SecureCLIHandler) { s.secureCLIHandler = h }

// SetPackagesHandler sets the runtime package management handler.
func (s *Server) SetPackagesHandler(h *httpapi.PackagesHandler) { s.packagesHandler = h }

// SetOAuthHandler sets the OAuth handler (available in all modes).
func (s *Server) SetOAuthHandler(h *httpapi.OAuthHandler) { s.oauthHandler = h }

// SetAPIKeysHandler sets the API key management handler.
func (s *Server) SetAPIKeysHandler(h *httpapi.APIKeysHandler) { s.apiKeysHandler = h }

// SetAPIKeyStore sets the API key store for token-based auth lookup.
func (s *Server) SetAPIKeyStore(st store.APIKeyStore) { s.apiKeyStore = st }

// SetFilesHandler sets the workspace file serving handler.
func (s *Server) SetFilesHandler(h *httpapi.FilesHandler) { s.filesHandler = h }

// SetStorageHandler sets the storage file management handler.
func (s *Server) SetStorageHandler(h *httpapi.StorageHandler) { s.storageHandler = h }

// SetMediaUploadHandler sets the media upload handler.
func (s *Server) SetMediaUploadHandler(h *httpapi.MediaUploadHandler) { s.mediaUploadHandler = h }

// SetMediaServeHandler sets the media serve handler.
func (s *Server) SetMediaServeHandler(h *httpapi.MediaServeHandler) { s.mediaServeHandler = h }

// SetMemoryHandler sets the memory management handler.
func (s *Server) SetMemoryHandler(h *httpapi.MemoryHandler) { s.memoryHandler = h }

// SetKnowledgeGraphHandler sets the knowledge graph handler.
func (s *Server) SetKnowledgeGraphHandler(h *httpapi.KnowledgeGraphHandler) { s.kgHandler = h }

// SetActivityHandler sets the activity audit log handler.
func (s *Server) SetActivityHandler(h *httpapi.ActivityHandler) { s.activityHandler = h }

// SetUsageHandler sets the usage analytics handler.
func (s *Server) SetUsageHandler(h *httpapi.UsageHandler) { s.usageHandler = h }

// SetDocsHandler sets the OpenAPI spec + Swagger UI handler.
func (s *Server) SetDocsHandler(h *httpapi.DocsHandler) { s.docsHandler = h }

// SetAgentStore sets the agent store for context injection in tools_invoke.
func (s *Server) SetAgentStore(as store.AgentStore) { s.agentStore = as }

// SetMessageBus sets the message bus for MCP bridge media delivery.
func (s *Server) SetMessageBus(mb *bus.MessageBus) { s.msgBus = mb }

// SetVersion sets the server version for health responses.
func (s *Server) SetVersion(v string) { s.version = v }

// SetDB sets the database connection for health check pings.
func (s *Server) SetDB(db interface{ PingContext(context.Context) error }) { s.db = db }

// StartedAt returns the server start time.
func (s *Server) StartedAt() time.Time { return s.startedAt }

// Version returns the server version string.
func (s *Server) Version() string { return s.version }

// ClientList returns a snapshot of all connected clients.
func (s *Server) ClientList() []*Client {
	s.mu.RLock()
	defer s.mu.RUnlock()
	list := make([]*Client, 0, len(s.clients))
	for _, c := range s.clients {
		list = append(list, c)
	}
	return list
}

// BroadcastEvent sends an event to all connected clients.
func (s *Server) BroadcastEvent(event protocol.EventFrame) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, client := range s.clients {
		client.SendEvent(event)
	}
}

// DisconnectByPairing force-closes WebSocket connections authenticated via the
// given pairing senderID and channel. Called after revoking a paired device so
// that the revoked client cannot continue operating with its old role.
func (s *Server) DisconnectByPairing(senderID, channel string) {
	s.mu.RLock()
	var targets []*Client
	for _, c := range s.clients {
		if c.pairedSenderID == senderID && c.pairedChannel == channel {
			targets = append(targets, c)
		}
	}
	s.mu.RUnlock()

	for _, c := range targets {
		slog.Info("disconnecting revoked paired device", "client", c.id, "sender_id", senderID, "channel", channel)
		c.conn.Close()
	}
}

func (s *Server) registerClient(c *Client) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.clients[c.id] = c

	// Subscribe to bus events for this client (skip internal cache events)
	s.eventPub.Subscribe(c.id, func(event bus.Event) {
		if strings.HasPrefix(event.Name, "cache.") {
			return // internal event, don't forward to WS clients
		}
		c.SendEvent(*protocol.NewEvent(event.Name, event.Payload))
	})

	slog.Info("client connected", "id", c.id)
}

func (s *Server) unregisterClient(c *Client) {
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.clients, c.id)
	s.eventPub.Unsubscribe(c.id)
	if s.logTee != nil {
		s.logTee.Unsubscribe(c.id)
	}
	slog.Info("client disconnected", "id", c.id)
}

// SetLogTee attaches a LogTee so that disconnecting clients are auto-unsubscribed.
func (s *Server) SetLogTee(lt *LogTee) {
	s.logTee = lt
}

// StartTestServer creates a listener on :0 (random port) and returns the
// actual address and a start function. Used for integration tests.
func StartTestServer(s *Server, ctx context.Context) (addr string, start func()) {
	mux := http.NewServeMux()
	mux.HandleFunc("/ws", s.handleWebSocket)
	mux.HandleFunc("/health", s.handleHealth)

	isManaged := s.agentStore != nil
	chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
	if s.rateLimiter.Enabled() {
		chatHandler.SetRateLimiter(s.rateLimiter.Allow)
	}
	mux.Handle("/v1/chat/completions", chatHandler)

	responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
	mux.Handle("/v1/responses", responsesHandler)

	if s.tools != nil {
		toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
		mux.Handle("/v1/tools/invoke", toolsHandler)
	}

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic("listen: " + err.Error())
	}

	s.httpServer = &http.Server{Handler: mux}
	addr = ln.Addr().String()

	start = func() {
		go func() {
			<-ctx.Done()
			shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			s.httpServer.Shutdown(shutdownCtx)
		}()
		s.httpServer.Serve(ln)
	}

	return addr, start
}
