package core

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

// ---------------------------------------------------------------------------
// BridgeServer — global WebSocket server shared across all engines
// ---------------------------------------------------------------------------

// BridgeServer exposes a WebSocket endpoint for external platform adapters.
// A single instance is created globally; each project engine receives a
// lightweight BridgePlatform handle that delegates to this server.
type BridgeServer struct {
	port        int
	token       string
	path        string
	corsOrigins []string
	insecure    bool // allow running without token (local dev only)
	server      *http.Server

	mu       sync.RWMutex
	adapters map[string]*bridgeAdapter // platform name → adapter

	enginesMu sync.RWMutex
	engines   map[string]*bridgeEngineRef // project name → engine ref
}

type bridgeEngineRef struct {
	engine   *Engine
	platform *BridgePlatform
}

type bridgeAdapter struct {
	platform     string
	capabilities map[string]bool
	metadata     map[string]any
	conn         *websocket.Conn
	writeMu      sync.Mutex
	server       *BridgeServer

	previewMu       sync.Mutex
	previewRequests map[string]chan string // ref_id → channel receiving preview_handle
}

// bridgeReplyCtx carries the information needed to route replies back to the adapter.
type bridgeReplyCtx struct {
	Platform   string `json:"platform"`
	SessionKey string `json:"session_key"`
	ReplyCtx   string `json:"reply_ctx"`

	progressStyle               string `json:"-"`
	supportsProgressCardPayload bool   `json:"-"`
}

func (rc *bridgeReplyCtx) progressStyleHint() string {
	if rc == nil {
		return progressStyleLegacy
	}
	return rc.progressStyle
}

func (rc *bridgeReplyCtx) supportsProgressCardPayloadHint() bool {
	if rc == nil {
		return false
	}
	return rc.supportsProgressCardPayload
}

const bridgeReconstructReplyCtxKind = "bridge_reconstruct"

// bridgeReconstructReplyCtxPayload is a forward-compatible reply envelope for
// reconstruct_reply adapters. Receivers should ignore unknown fields.
type bridgeReconstructReplyCtxPayload struct {
	Kind                string `json:"kind"`
	Version             int    `json:"v"`
	SenderProject       string `json:"sender_project"`
	TransportChatID     string `json:"transport_chat_id"`
	TransportSessionKey string `json:"transport_session_key,omitempty"`
}

// --- Wire protocol messages ---

type bridgeMsg struct {
	Type string `json:"type"`
}

type bridgeRegister struct {
	Type         string         `json:"type"`
	Platform     string         `json:"platform"`
	Capabilities []string       `json:"capabilities"`
	Project      string         `json:"project,omitempty"`
	Metadata     map[string]any `json:"metadata,omitempty"`
}

type bridgeMessage struct {
	Type       string            `json:"type"`
	MsgID      string            `json:"msg_id"`
	SessionKey string            `json:"session_key"`
	UserID     string            `json:"user_id"`
	UserName   string            `json:"user_name,omitempty"`
	Content    string            `json:"content"`
	ReplyCtx   string            `json:"reply_ctx"`
	Project    string            `json:"project,omitempty"`
	Images     []bridgeImageData `json:"images,omitempty"`
	Files      []bridgeFileData  `json:"files,omitempty"`
	Audio      *bridgeAudioData  `json:"audio,omitempty"`
}

type bridgeCardAction struct {
	Type       string `json:"type"`
	SessionKey string `json:"session_key"`
	Action     string `json:"action"`
	ReplyCtx   string `json:"reply_ctx"`
	Project    string `json:"project,omitempty"`
}

type bridgePreviewAck struct {
	Type          string `json:"type"`
	RefID         string `json:"ref_id"`
	PreviewHandle string `json:"preview_handle"`
}

type bridgeImageData struct {
	MimeType string `json:"mime_type"`
	Data     string `json:"data"` // base64
	FileName string `json:"file_name,omitempty"`
}

type bridgeFileData struct {
	MimeType string `json:"mime_type"`
	Data     string `json:"data"` // base64
	FileName string `json:"file_name"`
}

type bridgeAudioData struct {
	MimeType string `json:"mime_type"`
	Data     string `json:"data"` // base64
	Format   string `json:"format"`
	Duration int    `json:"duration,omitempty"`
}

func NewBridgeServer(port int, token, path string, corsOrigins []string) *BridgeServer {
	return newBridgeServer(port, token, path, corsOrigins, false)
}

// NewBridgeServerInsecure creates a BridgeServer that allows running without token.
// This should only be used for local development.
func NewBridgeServerInsecure(port int, token, path string, corsOrigins []string) *BridgeServer {
	return newBridgeServer(port, token, path, corsOrigins, true)
}

func newBridgeServer(port int, token, path string, corsOrigins []string, insecure bool) *BridgeServer {
	if port <= 0 {
		port = 9810
	}
	if path == "" {
		path = "/bridge/ws"
	}
	if !strings.HasPrefix(path, "/") {
		path = "/" + path
	}

	// Validate security settings
	if token == "" && !insecure {
		slog.Error("bridge: token is required when insecure mode is not enabled",
			"help", "set bridge.token in config, or use insecure=true for local development only")
		return nil
	}
	if insecure && token == "" {
		slog.Warn("bridge: running in INSECURE mode without authentication - only use for local development!")
	}

	return &BridgeServer{
		port:        port,
		token:       token,
		path:        path,
		corsOrigins: corsOrigins,
		insecure:    insecure,
		adapters:    make(map[string]*bridgeAdapter),
		engines:     make(map[string]*bridgeEngineRef),
	}
}

// NewPlatform creates a BridgePlatform for a specific project engine.
func (bs *BridgeServer) NewPlatform(projectName string) *BridgePlatform {
	return &BridgePlatform{server: bs, project: projectName}
}

// RegisterEngine associates a project engine with its BridgePlatform.
func (bs *BridgeServer) RegisterEngine(projectName string, engine *Engine, bp *BridgePlatform) {
	bs.enginesMu.Lock()
	defer bs.enginesMu.Unlock()
	if err := bp.Start(engine.handleMessage); err != nil {
		slog.Warn("bridge: platform start failed", "project", projectName, "error", err)
	}
	bp.SetCardNavigationHandler(engine.handleCardNav)
	bs.engines[projectName] = &bridgeEngineRef{engine: engine, platform: bp}
}

// Start launches the HTTP/WebSocket server.
func (bs *BridgeServer) Start() {
	mux := http.NewServeMux()
	mux.HandleFunc(bs.path, bs.handleWS)

	// Session management REST endpoints (with CORS support)
	mux.HandleFunc("/bridge/sessions", bs.corsHTTP(bs.authHTTP(bs.handleSessions)))
	mux.HandleFunc("/bridge/sessions/", bs.corsHTTP(bs.authHTTP(bs.handleSessionRoutes)))

	addr := fmt.Sprintf(":%d", bs.port)
	bs.server = &http.Server{Addr: addr, Handler: mux}

	go func() {
		slog.Info("bridge: server started", "addr", addr, "path", bs.path)
		if err := bs.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			slog.Error("bridge: server error", "error", err)
		}
	}()
}

// corsHTTP wraps a handler with CORS headers. OPTIONS preflight is handled directly.
func (bs *BridgeServer) corsHTTP(handler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		bs.setCORS(w, r)
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		handler(w, r)
	}
}

// setCORS sets Access-Control-* headers when the request origin matches cors_origins.
func (bs *BridgeServer) setCORS(w http.ResponseWriter, r *http.Request) {
	if len(bs.corsOrigins) == 0 {
		return
	}
	origin := r.Header.Get("Origin")
	for _, o := range bs.corsOrigins {
		if o == "*" || o == origin {
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
			w.Header().Set("Access-Control-Max-Age", "86400")
			break
		}
	}
}

// Stop shuts down the server and closes all adapter connections.
func (bs *BridgeServer) Stop() {
	bs.mu.Lock()
	for _, a := range bs.adapters {
		a.conn.Close()
	}
	bs.adapters = make(map[string]*bridgeAdapter)
	bs.mu.Unlock()

	if bs.server != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := bs.server.Shutdown(ctx); err != nil && err != http.ErrServerClosed {
			slog.Debug("bridge: server shutdown failed", "error", err)
		}
	}
}

// ConnectedAdapters returns the names of currently connected adapters.
func (bs *BridgeServer) ConnectedAdapters() []string {
	bs.mu.RLock()
	defer bs.mu.RUnlock()
	names := make([]string, 0, len(bs.adapters))
	for name := range bs.adapters {
		names = append(names, name)
	}
	return names
}

// ---------------------------------------------------------------------------
// BridgePlatform — per-engine Platform that delegates to BridgeServer
// ---------------------------------------------------------------------------

// BridgePlatform implements core.Platform for a single project.
// It is a lightweight handle; the actual WebSocket server lives in BridgeServer.
type BridgePlatform struct {
	server     *BridgeServer
	project    string
	handler    MessageHandler
	navHandler CardNavigationHandler
}

// Compile-time interface checks.
var (
	_ Platform                  = (*BridgePlatform)(nil)
	_ CardSender                = (*BridgePlatform)(nil)
	_ InlineButtonSender        = (*BridgePlatform)(nil)
	_ MessageUpdater            = (*BridgePlatform)(nil)
	_ PreviewStarter            = (*BridgePlatform)(nil)
	_ PreviewCleaner            = (*BridgePlatform)(nil)
	_ TypingIndicator           = (*BridgePlatform)(nil)
	_ AudioSender               = (*BridgePlatform)(nil)
	_ VideoSender               = (*BridgePlatform)(nil)
	_ ImageSender               = (*BridgePlatform)(nil)
	_ FileSender                = (*BridgePlatform)(nil)
	_ CardNavigable             = (*BridgePlatform)(nil)
	_ ReplyContextReconstructor = (*BridgePlatform)(nil)
)

func (bp *BridgePlatform) Name() string { return "bridge" }

func (bp *BridgePlatform) Start(handler MessageHandler) error {
	bp.handler = handler
	return nil
}

func (bp *BridgePlatform) Stop() error { return nil }

func (bp *BridgePlatform) Reply(ctx context.Context, replyCtx any, content string) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context type %T", replyCtx)
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "reply",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"content":     content,
		"format":      "text",
	})
}

func (bp *BridgePlatform) Send(ctx context.Context, replyCtx any, content string) error {
	return bp.Reply(ctx, replyCtx, content)
}

func (bp *BridgePlatform) ReconstructReplyCtx(sessionKey string) (any, error) {
	platform := bp.server.platformFromSessionKey(sessionKey)
	if platform == "" {
		return nil, fmt.Errorf("bridge: cannot determine adapter from session key %q", sessionKey)
	}
	a := bp.server.getAdapter(platform)
	if a == nil {
		return nil, fmt.Errorf("bridge: adapter %q not connected", platform)
	}
	if !a.capabilities["reconstruct_reply"] {
		return nil, fmt.Errorf("bridge: adapter %q does not support reconstruct_reply", platform)
	}
	replyCtx, err := buildBridgeReconstructReplyCtx(bp.project, sessionKey)
	if err != nil {
		return nil, err
	}
	return newBridgeReplyCtx(a, sessionKey, replyCtx), nil
}

func newBridgeReplyCtx(a *bridgeAdapter, sessionKey, replyCtx string) *bridgeReplyCtx {
	rc := &bridgeReplyCtx{
		SessionKey: sessionKey,
		ReplyCtx:   replyCtx,
	}
	if a == nil {
		return rc
	}
	rc.Platform = a.platform
	rc.progressStyle = bridgeProgressStyleForAdapter(a)
	rc.supportsProgressCardPayload = bridgeSupportsProgressCardPayloadForAdapter(a)
	return rc
}

func bridgeProgressStyleForAdapter(a *bridgeAdapter) string {
	if a == nil {
		return progressStyleLegacy
	}
	if style, ok := bridgeMetadataString(a.metadata, "progress_style"); ok {
		return normalizeProgressStyle(style)
	}
	if a.capabilities["preview"] && a.capabilities["update_message"] {
		if a.capabilities["card"] {
			return progressStyleCard
		}
		return progressStyleCompact
	}
	return progressStyleLegacy
}

func bridgeSupportsProgressCardPayloadForAdapter(a *bridgeAdapter) bool {
	if a == nil {
		return false
	}
	if supported, ok := bridgeMetadataBool(a.metadata, "supports_progress_card_payload"); ok {
		return supported
	}
	adapterName, _ := bridgeMetadataString(a.metadata, "adapter")
	return adapterName == "bot-gateway" && a.capabilities["preview"] && a.capabilities["update_message"]
}

func bridgeMetadataString(metadata map[string]any, key string) (string, bool) {
	if metadata == nil {
		return "", false
	}
	raw, ok := metadata[key]
	if !ok {
		return "", false
	}
	value, ok := raw.(string)
	if !ok {
		return "", false
	}
	value = strings.TrimSpace(value)
	if value == "" {
		return "", false
	}
	return value, true
}

func bridgeMetadataBool(metadata map[string]any, key string) (bool, bool) {
	if metadata == nil {
		return false, false
	}
	raw, ok := metadata[key]
	if !ok {
		return false, false
	}
	value, ok := raw.(bool)
	if !ok {
		return false, false
	}
	return value, true
}

func buildBridgeReconstructReplyCtx(project, sessionKey string) (string, error) {
	chatID, err := bridgeTransportChatID(sessionKey)
	if err != nil {
		return "", err
	}
	payload := bridgeReconstructReplyCtxPayload{
		Kind:                bridgeReconstructReplyCtxKind,
		Version:             1,
		SenderProject:       project,
		TransportChatID:     chatID,
		TransportSessionKey: sessionKey,
	}
	data, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("bridge: marshal reconstruct reply ctx: %w", err)
	}
	return string(data), nil
}

func bridgeTransportChatID(sessionKey string) (string, error) {
	parts := strings.SplitN(sessionKey, ":", 3)
	if len(parts) < 2 || parts[1] == "" {
		return "", fmt.Errorf("bridge: invalid session key %q", sessionKey)
	}
	return parts[1], nil
}

func (bp *BridgePlatform) SendCard(ctx context.Context, replyCtx any, card *Card) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["card"] {
		return bp.Reply(ctx, replyCtx, card.RenderText())
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "card",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"card":        serializeCard(card),
	})
}

func (bp *BridgePlatform) ReplyCard(ctx context.Context, replyCtx any, card *Card) error {
	return bp.SendCard(ctx, replyCtx, card)
}

func (bp *BridgePlatform) SendWithButtons(ctx context.Context, replyCtx any, content string, buttons [][]ButtonOption) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["buttons"] {
		return bp.Reply(ctx, replyCtx, content)
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "buttons",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"content":     content,
		"buttons":     buttons,
	})
}

func (bp *BridgePlatform) UpdateMessage(ctx context.Context, replyCtx any, content string) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["update_message"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":           "update_message",
		"session_key":    rc.SessionKey,
		"preview_handle": rc.ReplyCtx,
		"content":        content,
	})
}

func (bp *BridgePlatform) SendPreviewStart(ctx context.Context, replyCtx any, content string) (previewHandle any, err error) {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return nil, fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["preview"] {
		return nil, ErrNotSupported
	}

	refID := fmt.Sprintf("prev-%d", time.Now().UnixNano())
	ch := make(chan string, 1)

	a.previewMu.Lock()
	a.previewRequests[refID] = ch
	a.previewMu.Unlock()

	if err := bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "preview_start",
		"ref_id":      refID,
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"content":     content,
	}); err != nil {
		a.previewMu.Lock()
		delete(a.previewRequests, refID)
		a.previewMu.Unlock()
		return nil, err
	}

	select {
	case handle := <-ch:
		return newBridgeReplyCtx(a, rc.SessionKey, handle), nil
	case <-time.After(10 * time.Second):
		a.previewMu.Lock()
		delete(a.previewRequests, refID)
		a.previewMu.Unlock()
		return nil, fmt.Errorf("bridge: preview_ack timeout")
	case <-ctx.Done():
		a.previewMu.Lock()
		delete(a.previewRequests, refID)
		a.previewMu.Unlock()
		return nil, ctx.Err()
	}
}

func (bp *BridgePlatform) DeletePreviewMessage(ctx context.Context, previewHandle any) error {
	rc, ok := previewHandle.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid preview handle")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["delete_message"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":           "delete_message",
		"session_key":    rc.SessionKey,
		"preview_handle": rc.ReplyCtx,
	})
}

func (bp *BridgePlatform) StartTyping(ctx context.Context, replyCtx any) (stop func()) {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return func() {}
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["typing"] {
		return func() {}
	}
	_ = bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "typing_start",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
	})
	return func() {
		_ = bp.server.sendToAdapter(rc.Platform, map[string]any{
			"type":        "typing_stop",
			"session_key": rc.SessionKey,
			"reply_ctx":   rc.ReplyCtx,
		})
	}
}

func (bp *BridgePlatform) SendAudio(ctx context.Context, replyCtx any, audio []byte, format string) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["audio"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "audio",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"data":        base64.StdEncoding.EncodeToString(audio),
		"format":      format,
	})
}

func (bp *BridgePlatform) SendVideo(ctx context.Context, replyCtx any, video []byte, format string, fileName string) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["video"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "video",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"data":        base64.StdEncoding.EncodeToString(video),
		"format":      format,
		"file_name":   fileName,
	})
}

func (bp *BridgePlatform) SendImage(ctx context.Context, replyCtx any, img ImageAttachment) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["image"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "image",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"data":        base64.StdEncoding.EncodeToString(img.Data),
		"mime_type":   img.MimeType,
		"file_name":   img.FileName,
	})
}

func (bp *BridgePlatform) SendFile(ctx context.Context, replyCtx any, file FileAttachment) error {
	rc, ok := replyCtx.(*bridgeReplyCtx)
	if !ok {
		return fmt.Errorf("bridge: invalid reply context")
	}
	a := bp.server.getAdapter(rc.Platform)
	if a == nil || !a.capabilities["file"] {
		return ErrNotSupported
	}
	return bp.server.sendToAdapter(rc.Platform, map[string]any{
		"type":        "file",
		"session_key": rc.SessionKey,
		"reply_ctx":   rc.ReplyCtx,
		"data":        base64.StdEncoding.EncodeToString(file.Data),
		"mime_type":   file.MimeType,
		"file_name":   file.FileName,
	})
}

func (bp *BridgePlatform) SetCardNavigationHandler(h CardNavigationHandler) {
	bp.navHandler = h
}

// ---------------------------------------------------------------------------
// WebSocket connection handling (on BridgeServer)
// ---------------------------------------------------------------------------

// checkOrigin validates the WebSocket origin against CORS origins.
// In insecure mode, it allows all origins. Otherwise, it checks against CORS origins or same host.
func (bs *BridgeServer) checkOrigin(r *http.Request) bool {
	// In insecure mode, allow all origins (for local development)
	if bs.insecure {
		return true
	}

	origin := r.Header.Get("Origin")
	if origin == "" {
		// No origin header (e.g., non-browser client) - allow only if authenticated
		// The authentication check happens before this, so we allow
		return true
	}

	// If CORS origins are configured, check against them
	if len(bs.corsOrigins) > 0 {
		for _, o := range bs.corsOrigins {
			if o == "*" || o == origin {
				return true
			}
		}
		slog.Warn("bridge: websocket origin rejected", "origin", origin, "allowed", bs.corsOrigins)
		return false
	}

	// No CORS configured - require same-host (origin must match host)
	host := r.Host
	if host == "" {
		host = r.URL.Host
	}
	// Parse origin to get host
	if idx := strings.Index(origin, "://"); idx > 0 {
		originHost := origin[idx+3:]
		if originHost == host {
			return true
		}
	}
	slog.Warn("bridge: websocket origin mismatch", "origin", origin, "host", host)
	return false
}

func (bs *BridgeServer) handleWS(w http.ResponseWriter, r *http.Request) {
	if !bs.authenticate(r) {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	// Use a custom upgrader with origin checking
	upgrader := websocket.Upgrader{
		CheckOrigin: bs.checkOrigin,
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		slog.Error("bridge: websocket upgrade failed", "error", err)
		return
	}

	slog.Info("bridge: new connection", "remote", conn.RemoteAddr())
	bs.handleConnection(conn)
}

func (bs *BridgeServer) handleConnection(conn *websocket.Conn) {
	defer conn.Close()

	if err := conn.SetReadDeadline(time.Now().Add(90 * time.Second)); err != nil {
		slog.Debug("bridge: set read deadline failed", "error", err)
		return
	}
	conn.SetPongHandler(func(string) error {
		return conn.SetReadDeadline(time.Now().Add(90 * time.Second))
	})

	// First message must be "register"
	_, raw, err := conn.ReadMessage()
	if err != nil {
		slog.Error("bridge: read register failed", "error", err)
		return
	}

	var reg bridgeRegister
	if err := json.Unmarshal(raw, &reg); err != nil || reg.Type != "register" {
		if err := writeJSON(conn, nil, map[string]any{"type": "register_ack", "ok": false, "error": "first message must be register"}); err != nil {
			slog.Debug("bridge: write register ack failed", "error", err)
		}
		return
	}

	if reg.Platform == "" {
		if err := writeJSON(conn, nil, map[string]any{"type": "register_ack", "ok": false, "error": "platform name is required"}); err != nil {
			slog.Debug("bridge: write register ack failed", "error", err)
		}
		return
	}

	caps := make(map[string]bool, len(reg.Capabilities))
	for _, c := range reg.Capabilities {
		caps[c] = true
	}
	caps["text"] = true

	adapter := &bridgeAdapter{
		platform:        reg.Platform,
		capabilities:    caps,
		metadata:        reg.Metadata,
		conn:            conn,
		server:          bs,
		previewRequests: make(map[string]chan string),
	}

	bs.mu.Lock()
	if old, exists := bs.adapters[reg.Platform]; exists {
		old.conn.Close()
		slog.Info("bridge: replaced existing adapter", "platform", reg.Platform)
	}
	bs.adapters[reg.Platform] = adapter
	bs.mu.Unlock()

	if err := writeJSON(conn, &adapter.writeMu, map[string]any{"type": "register_ack", "ok": true}); err != nil {
		slog.Debug("bridge: write register ack failed", "error", err)
		return
	}

	if bridgeMetadataStringListContains(reg.Metadata, "control_plane", bridgeCapabilitiesSnapshotProto) {
		if err := writeJSON(conn, &adapter.writeMu, bs.buildCapabilitiesSnapshot()); err != nil {
			slog.Debug("bridge: write capabilities snapshot failed", "platform", reg.Platform, "error", err)
			return
		}
	}

	slog.Info("bridge: adapter registered", "platform", reg.Platform, "capabilities", reg.Capabilities)

	defer func() {
		bs.mu.Lock()
		if bs.adapters[reg.Platform] == adapter {
			delete(bs.adapters, reg.Platform)
		}
		bs.mu.Unlock()
		slog.Info("bridge: adapter disconnected", "platform", reg.Platform)
	}()

	for {
		if err := conn.SetReadDeadline(time.Now().Add(90 * time.Second)); err != nil {
			slog.Debug("bridge: set read deadline failed", "platform", reg.Platform, "error", err)
			return
		}
		_, raw, err := conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
				slog.Debug("bridge: read error", "platform", reg.Platform, "error", err)
			}
			return
		}

		var base bridgeMsg
		if err := json.Unmarshal(raw, &base); err != nil {
			slog.Debug("bridge: invalid JSON", "platform", reg.Platform, "error", err)
			continue
		}

		switch base.Type {
		case "message":
			adapter.handleMessage(raw)
		case "card_action":
			adapter.handleCardAction(raw)
		case "preview_ack":
			adapter.handlePreviewAck(raw)
		case "ping":
			if err := writeJSON(conn, &adapter.writeMu, map[string]any{"type": "pong", "ts": time.Now().UnixMilli()}); err != nil {
				slog.Debug("bridge: write pong failed", "platform", reg.Platform, "error", err)
				return
			}
		default:
			slog.Debug("bridge: unknown message type", "platform", reg.Platform, "type", base.Type)
		}
	}
}

// ---------------------------------------------------------------------------
// Adapter message handlers
// ---------------------------------------------------------------------------

func (a *bridgeAdapter) handleMessage(raw json.RawMessage) {
	var m bridgeMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		slog.Debug("bridge: invalid message payload", "error", err)
		return
	}

	if m.SessionKey == "" || m.UserID == "" {
		slog.Debug("bridge: message missing required fields", "platform", a.platform)
		return
	}

	ref := a.server.resolveEngine(m.SessionKey, m.Project)
	if ref == nil {
		slog.Warn("bridge: no engine for session", "platform", a.platform, "session_key", m.SessionKey, "project", m.Project)
		return
	}

	msg := &Message{
		SessionKey: m.SessionKey,
		Platform:   a.platform,
		MessageID:  m.MsgID,
		UserID:     m.UserID,
		UserName:   m.UserName,
		Content:    m.Content,
		ReplyCtx:   newBridgeReplyCtx(a, m.SessionKey, m.ReplyCtx),
	}

	for _, img := range m.Images {
		data, err := base64.StdEncoding.DecodeString(img.Data)
		if err != nil {
			slog.Debug("bridge: invalid image base64", "error", err)
			continue
		}
		msg.Images = append(msg.Images, ImageAttachment{
			MimeType: img.MimeType, Data: data, FileName: img.FileName,
		})
	}

	for _, f := range m.Files {
		data, err := base64.StdEncoding.DecodeString(f.Data)
		if err != nil {
			slog.Debug("bridge: invalid file base64", "error", err)
			continue
		}
		msg.Files = append(msg.Files, FileAttachment{
			MimeType: f.MimeType, Data: data, FileName: f.FileName,
		})
	}

	if m.Audio != nil {
		if data, err := base64.StdEncoding.DecodeString(m.Audio.Data); err == nil {
			msg.Audio = &AudioAttachment{
				MimeType: m.Audio.MimeType, Data: data,
				Format: m.Audio.Format, Duration: m.Audio.Duration,
			}
		}
	}

	slog.Info("bridge: message received",
		"platform", a.platform, "session_key", m.SessionKey,
		"user", m.UserID, "content_len", len(m.Content),
	)

	if ref.platform.handler != nil {
		ref.platform.handler(ref.platform, msg)
	}
}

func (a *bridgeAdapter) handleCardAction(raw json.RawMessage) {
	var ca bridgeCardAction
	if err := json.Unmarshal(raw, &ca); err != nil {
		slog.Debug("bridge: invalid card_action payload", "error", err)
		return
	}

	slog.Debug("bridge: card_action", "platform", a.platform, "action", ca.Action, "session_key", ca.SessionKey, "project", ca.Project)

	ref := a.server.resolveEngine(ca.SessionKey, ca.Project)
	if ref == nil {
		return
	}

	// perm: — permission response; convert to a regular message for the engine
	if strings.HasPrefix(ca.Action, "perm:") {
		var responseText string
		switch ca.Action {
		case "perm:allow":
			responseText = "allow"
		case "perm:deny":
			responseText = "deny"
		case "perm:allow_all":
			responseText = "allow all"
		default:
			return
		}
		a.dispatchAsPermissionResponse(ref, ca.SessionKey, ca.ReplyCtx, responseText)
		return
	}

	// askq: — AskUserQuestion answer; forward as a regular message
	if strings.HasPrefix(ca.Action, "askq:") {
		a.dispatchAsMessage(ref, ca.SessionKey, ca.ReplyCtx, ca.Action)
		return
	}

	// cmd: — command shortcut from a card button; forward as a message
	if strings.HasPrefix(ca.Action, "cmd:") {
		cmdText := strings.TrimPrefix(ca.Action, "cmd:")
		a.dispatchAsMessage(ref, ca.SessionKey, ca.ReplyCtx, cmdText)
		return
	}

	// nav: / act: — card navigation and in-place updates
	if ref.platform.navHandler == nil {
		return
	}

	card := ref.platform.navHandler(ca.Action, ca.SessionKey)
	if card == nil {
		return
	}

	if a.capabilities["card"] {
		_ = a.server.sendToAdapter(a.platform, map[string]any{
			"type":        "card",
			"session_key": ca.SessionKey,
			"reply_ctx":   ca.ReplyCtx,
			"card":        serializeCard(card),
		})
	} else {
		rc := newBridgeReplyCtx(a, ca.SessionKey, ca.ReplyCtx)
		_ = ref.platform.Reply(context.Background(), rc, card.RenderText())
	}
}

// dispatchAsMessage converts a card action into a regular user message
// and dispatches it to the engine's message handler.
func (a *bridgeAdapter) dispatchAsMessage(ref *bridgeEngineRef, sessionKey, replyCtx, content string) {
	if ref.platform.handler == nil {
		return
	}
	msg := &Message{
		SessionKey: sessionKey,
		Platform:   a.platform,
		UserID:     "web-admin",
		UserName:   "Web Admin",
		Content:    content,
		ReplyCtx:   newBridgeReplyCtx(a, sessionKey, replyCtx),
	}
	go ref.platform.handler(ref.platform, msg)
}

// dispatchAsPermissionResponse is the permission-callback sibling of
// dispatchAsMessage. It sets IsPermissionResponse on the synthesized
// Message so the engine's handlePendingPermission can drop stale clicks
// (e.g. user tapped an old "Allow" card after the session was reset) —
// preventing the literal "allow"/"deny" string from reaching the agent's
// prompt stream (issue #826).
func (a *bridgeAdapter) dispatchAsPermissionResponse(ref *bridgeEngineRef, sessionKey, replyCtx, content string) {
	if ref.platform.handler == nil {
		return
	}
	msg := &Message{
		SessionKey:           sessionKey,
		Platform:             a.platform,
		UserID:               "web-admin",
		UserName:             "Web Admin",
		Content:              content,
		ReplyCtx:             newBridgeReplyCtx(a, sessionKey, replyCtx),
		IsPermissionResponse: true,
	}
	go ref.platform.handler(ref.platform, msg)
}

func (a *bridgeAdapter) handlePreviewAck(raw json.RawMessage) {
	var ack bridgePreviewAck
	if err := json.Unmarshal(raw, &ack); err != nil {
		return
	}

	a.previewMu.Lock()
	ch, ok := a.previewRequests[ack.RefID]
	if ok {
		delete(a.previewRequests, ack.RefID)
	}
	a.previewMu.Unlock()

	if ok {
		ch <- ack.PreviewHandle
	}
}

// ---------------------------------------------------------------------------
// Session management REST API (on BridgeServer)
// ---------------------------------------------------------------------------

// authHTTP wraps an HTTP handler with token authentication.
func (bs *BridgeServer) authHTTP(handler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !bs.authenticate(r) {
			bridgeError(w, http.StatusUnauthorized, "unauthorized")
			return
		}
		handler(w, r)
	}
}

func bridgeJSON(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(map[string]any{"ok": true, "data": data}); err != nil {
		slog.Debug("bridge: write JSON failed", "error", err)
	}
}

func bridgeError(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(map[string]any{"ok": false, "error": msg}); err != nil {
		slog.Debug("bridge: write JSON failed", "error", err)
	}
}

// resolveEngineForSessionKey returns the engine ref for a given session key and optional project.
func (bs *BridgeServer) resolveEngineForSessionKey(sessionKey, project string) *bridgeEngineRef {
	return bs.resolveEngine(sessionKey, project)
}

// handleSessions handles GET /bridge/sessions and POST /bridge/sessions.
func (bs *BridgeServer) handleSessions(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		sessionKey := r.URL.Query().Get("session_key")
		if sessionKey == "" {
			bridgeError(w, http.StatusBadRequest, "session_key query parameter is required")
			return
		}
		project := r.URL.Query().Get("project")
		ref := bs.resolveEngineForSessionKey(sessionKey, project)
		if ref == nil {
			bridgeError(w, http.StatusNotFound, "no engine found for session key")
			return
		}

		sessions := ref.engine.sessions.ListSessions(sessionKey)
		activeID := ref.engine.sessions.ActiveSessionID(sessionKey)

		list := make([]map[string]any, len(sessions))
		for i, s := range sessions {
			list[i] = map[string]any{
				"id":            s.ID,
				"name":          s.GetName(),
				"history_count": len(s.History),
			}
		}

		bridgeJSON(w, http.StatusOK, map[string]any{
			"sessions":          list,
			"active_session_id": activeID,
		})

	case http.MethodPost:
		var body struct {
			SessionKey string `json:"session_key"`
			Name       string `json:"name"`
			Project    string `json:"project,omitempty"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			bridgeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
			return
		}
		if body.SessionKey == "" {
			bridgeError(w, http.StatusBadRequest, "session_key is required")
			return
		}
		ref := bs.resolveEngineForSessionKey(body.SessionKey, body.Project)
		if ref == nil {
			bridgeError(w, http.StatusNotFound, "no engine found for session key")
			return
		}
		name := body.Name
		if name == "" {
			name = "default"
		}
		s := ref.engine.sessions.NewSession(body.SessionKey, name)
		bridgeJSON(w, http.StatusOK, map[string]any{
			"id":      s.ID,
			"name":    s.GetName(),
			"message": "session created",
		})

	default:
		bridgeError(w, http.StatusMethodNotAllowed, "GET or POST only")
	}
}

// handleSessionRoutes dispatches /bridge/sessions/{sub} routes.
func (bs *BridgeServer) handleSessionRoutes(w http.ResponseWriter, r *http.Request) {
	sub := strings.TrimPrefix(r.URL.Path, "/bridge/sessions/")
	if sub == "" {
		bridgeError(w, http.StatusBadRequest, "session id required")
		return
	}

	// POST /bridge/sessions/switch
	if sub == "switch" {
		bs.handleSessionSwitch(w, r)
		return
	}

	// GET or DELETE /bridge/sessions/{id}
	sessionKey := r.URL.Query().Get("session_key")
	if sessionKey == "" {
		bridgeError(w, http.StatusBadRequest, "session_key query parameter is required")
		return
	}
	project := r.URL.Query().Get("project")
	ref := bs.resolveEngineForSessionKey(sessionKey, project)
	if ref == nil {
		bridgeError(w, http.StatusNotFound, "no engine found for session key")
		return
	}

	switch r.Method {
	case http.MethodGet:
		s := ref.engine.sessions.FindByID(sub)
		if s == nil {
			bridgeError(w, http.StatusNotFound, "session not found")
			return
		}
		histLimit := 50
		if v := r.URL.Query().Get("history_limit"); v != "" {
			if n, err := strconv.Atoi(v); err == nil && n > 0 {
				histLimit = n
			}
		}
		hist := s.GetHistory(histLimit)
		histJSON := make([]map[string]any, len(hist))
		for i, h := range hist {
			histJSON[i] = map[string]any{
				"role":    h.Role,
				"content": h.Content,
			}
		}
		bridgeJSON(w, http.StatusOK, map[string]any{
			"id":      s.ID,
			"name":    s.GetName(),
			"history": histJSON,
		})

	case http.MethodDelete:
		if ref.engine.sessions.DeleteByID(sub) {
			bridgeJSON(w, http.StatusOK, map[string]string{"message": "session deleted"})
		} else {
			bridgeError(w, http.StatusNotFound, "session not found")
		}

	default:
		bridgeError(w, http.StatusMethodNotAllowed, "GET or DELETE only")
	}
}

// handleSessionSwitch handles POST /bridge/sessions/switch.
func (bs *BridgeServer) handleSessionSwitch(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		bridgeError(w, http.StatusMethodNotAllowed, "POST only")
		return
	}
	var body struct {
		SessionKey string `json:"session_key"`
		Target     string `json:"target"`
		Project    string `json:"project,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		bridgeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	if body.SessionKey == "" || body.Target == "" {
		bridgeError(w, http.StatusBadRequest, "session_key and target are required")
		return
	}
	ref := bs.resolveEngineForSessionKey(body.SessionKey, body.Project)
	if ref == nil {
		bridgeError(w, http.StatusNotFound, "no engine found for session key")
		return
	}
	s, err := ref.engine.sessions.SwitchSession(body.SessionKey, body.Target)
	if err != nil {
		bridgeError(w, http.StatusNotFound, err.Error())
		return
	}
	bridgeJSON(w, http.StatusOK, map[string]any{
		"message":           "session switched",
		"active_session_id": s.ID,
	})
}

// ---------------------------------------------------------------------------
// Internal helpers (on BridgeServer)
// ---------------------------------------------------------------------------

func (bs *BridgeServer) authenticate(r *http.Request) bool {
	// If token is not set, only allow in insecure mode
	if bs.token == "" {
		return bs.insecure
	}
	if auth := r.Header.Get("Authorization"); auth != "" {
		if strings.HasPrefix(auth, "Bearer ") {
			return subtle.ConstantTimeCompare([]byte(auth[7:]), []byte(bs.token)) == 1
		}
	}
	if tok := r.Header.Get("X-Bridge-Token"); tok != "" {
		return subtle.ConstantTimeCompare([]byte(tok), []byte(bs.token)) == 1
	}
	if tok := r.URL.Query().Get("token"); tok != "" {
		return subtle.ConstantTimeCompare([]byte(tok), []byte(bs.token)) == 1
	}
	return false
}

func (bs *BridgeServer) getAdapter(platform string) *bridgeAdapter {
	bs.mu.RLock()
	defer bs.mu.RUnlock()
	return bs.adapters[platform]
}

func (bs *BridgeServer) sendToAdapter(platform string, msg map[string]any) error {
	bs.mu.RLock()
	a, ok := bs.adapters[platform]
	bs.mu.RUnlock()
	if !ok {
		return fmt.Errorf("bridge: adapter %q not connected", platform)
	}
	return writeJSON(a.conn, &a.writeMu, msg)
}

func bridgeMetadataStringListContains(metadata map[string]any, key, want string) bool {
	if metadata == nil || key == "" || want == "" {
		return false
	}
	raw, ok := metadata[key]
	if !ok {
		return false
	}
	items, ok := raw.([]any)
	if !ok {
		if stringsList, ok := raw.([]string); ok {
			for _, item := range stringsList {
				if strings.TrimSpace(item) == want {
					return true
				}
			}
		}
		return false
	}
	for _, item := range items {
		if s, ok := item.(string); ok && strings.TrimSpace(s) == want {
			return true
		}
	}
	return false
}

func (bs *BridgeServer) platformFromSessionKey(sessionKey string) string {
	if idx := strings.Index(sessionKey, ":"); idx > 0 {
		candidate := sessionKey[:idx]
		bs.mu.RLock()
		_, ok := bs.adapters[candidate]
		bs.mu.RUnlock()
		if ok {
			return candidate
		}
	}
	return ""
}

// resolveEngine finds the engine to handle a message.
// It first tries to match by project name, then by session_key ownership,
// and finally falls back to the single-engine case.
func (bs *BridgeServer) resolveEngine(sessionKey, project string) *bridgeEngineRef {
	bs.enginesMu.RLock()
	defer bs.enginesMu.RUnlock()

	if project != "" {
		if ref, ok := bs.engines[project]; ok {
			return ref
		}
	}

	if len(bs.engines) == 1 {
		for _, ref := range bs.engines {
			return ref
		}
	}

	// Try to find the engine that owns sessions for this key.
	for _, ref := range bs.engines {
		if sessions := ref.engine.sessions.ListSessions(sessionKey); len(sessions) > 0 {
			return ref
		}
	}

	return nil
}

func writeJSON(conn *websocket.Conn, mu *sync.Mutex, v any) error {
	if mu != nil {
		mu.Lock()
		defer mu.Unlock()
	}
	return conn.WriteJSON(v)
}

// serializeCard converts a Card into a JSON-friendly map for the bridge protocol.
func serializeCard(c *Card) map[string]any {
	result := make(map[string]any)
	if c.Header != nil {
		result["header"] = map[string]string{
			"title": c.Header.Title,
			"color": c.Header.Color,
		}
	}
	var elements []map[string]any
	for _, elem := range c.Elements {
		switch e := elem.(type) {
		case CardMarkdown:
			elements = append(elements, map[string]any{"type": "markdown", "content": e.Content})
		case CardDivider:
			elements = append(elements, map[string]any{"type": "divider"})
		case CardActions:
			var btns []map[string]any
			for _, b := range e.Buttons {
				btns = append(btns, map[string]any{
					"text": b.Text, "btn_type": b.Type, "value": b.Value,
				})
			}
			elements = append(elements, map[string]any{
				"type": "actions", "buttons": btns, "layout": string(e.Layout),
			})
		case CardNote:
			m := map[string]any{"type": "note", "text": e.Text}
			if e.Tag != "" {
				m["tag"] = e.Tag
			}
			elements = append(elements, m)
		case CardListItem:
			elements = append(elements, map[string]any{
				"type": "list_item", "text": e.Text,
				"btn_text": e.BtnText, "btn_type": e.BtnType, "btn_value": e.BtnValue,
			})
		case CardSelect:
			var opts []map[string]string
			for _, o := range e.Options {
				opts = append(opts, map[string]string{"text": o.Text, "value": o.Value})
			}
			elements = append(elements, map[string]any{
				"type": "select", "placeholder": e.Placeholder,
				"options": opts, "init_value": e.InitValue,
			})
		}
	}
	result["elements"] = elements
	return result
}
