package events

import "encoding/json"

// Event is the universal envelope for all bus communication.
type Event struct {
	Label   string          `json:"event"`
	Room    string          `json:"room,omitempty"`
	Payload json.RawMessage `json:"payload"`
	Ts      int64           `json:"ts"`

	// v0.32.0: Cross-pod targeted delivery. When set, only connections
	// belonging to this user receive the event. Serialized for pg_broadcast
	// (harmless if clients see it — they ignore unknown fields).
	TargetUserID string `json:"target_user_id,omitempty"`

	// Server-side metadata (not serialized to clients)
	SenderID string `json:"-"` // user ID of sender, empty for system events
	ConnID   string `json:"-"` // WebSocket connection ID, empty for server-origin
}

// Handler is a callback for bus subscriptions.
type Handler func(Event)

// Direction controls which events cross the WebSocket bridge.
type Direction int

const (
	DirLocal    Direction = iota // bus-internal only (plugins, DB hooks)
	DirToClient                 // BE → FE
	DirFromClient               // FE → BE
	DirBoth                     // bidirectional
)

// routeTable defines the default routing for known event prefixes.
// Events not listed default to DirLocal (server-only).
var routeTable = map[string]Direction{
	// User/presence
	"user.presence":     DirToClient,
	"user.status":       DirToClient,
	"user.mentioned":    DirToClient, // v0.23.2: targeted @mention notification
	"typing.user":       DirToClient, // v0.23.2: human typing in DM/channel

	// System
	"system.notify":     DirToClient,
	"system.broadcast":  DirToClient,

	// Model/provider status
	"model.status":      DirToClient,

	// Role alerts (v0.17.0)
	"role.fallback":     DirToClient,

	// Notifications (v0.20.0)
	"notification.new":  DirToClient, // targeted via Hub.PublishToUser, not room-based
	"notification.read": DirToClient, // badge sync across tabs

	// Workspace (v0.21.5)

	// Workflow (v0.27.0, v0.3.2, v0.3.3)
	"workflow.started":    DirToClient, // instance started
	"workflow.assigned":   DirToClient, // new assignment → team members
	"workflow.claimed":    DirToClient, // assignment claimed → team + claimer
	"workflow.advanced":   DirToClient, // stage advanced → instance participants
	"workflow.completed":  DirToClient, // workflow finished → instance participants
	"workflow.cancelled":  DirToClient, // instance cancelled
	"workflow.error":      DirToClient, // engine/hook error
	"workflow.sla_breach": DirToClient, // SLA exceeded → team + admins (v0.3.3)
	"workflow.stale":      DirToClient, // instance marked stale (v0.3.3)
	"workflow.signoff":    DirToClient, // signoff submitted (v0.3.4)
	"workflow.rejected":   DirToClient, // rejection triggered cancel/reroute (v0.3.4)

	// Plugin hooks — never cross the wire
	"plugin.hook.":      DirLocal,
	"internal.":         DirLocal,
	"db.":               DirLocal,

	// Tool execution (browser tools bridge)
	"tool.call.":    DirToClient,    // Server → specific client (browser tool invocation)
	"tool.result.":  DirBoth,        // v0.32.0: DirBoth — result must cross pods for WaitFor

	// Extension lifecycle
	"extension.loaded":  DirLocal,   // Client-only
	"extension.error":   DirLocal,

	// Trigger system (v0.2.2)
	"trigger.fired":     DirLocal,   // Trigger invocation event (observability)
	"trigger.error":     DirLocal,   // Trigger execution error

	// Heartbeat
	"ping":  DirFromClient,
	"pong":  DirToClient,
}

// RouteFor returns the routing direction for a given event label.
func RouteFor(label string) Direction {
	// Check exact match first
	if dir, ok := routeTable[label]; ok {
		return dir
	}
	// Check prefix match (longest prefix wins)
	best := ""
	bestDir := DirLocal
	for prefix, dir := range routeTable {
		if len(prefix) > len(best) && len(label) >= len(prefix) && label[:len(prefix)] == prefix {
			best = prefix
			bestDir = dir
		}
	}
	return bestDir
}

// ShouldSendToClient returns true if this event should be forwarded to FE.
func ShouldSendToClient(label string) bool {
	d := RouteFor(label)
	return d == DirToClient || d == DirBoth
}

// ShouldAcceptFromClient returns true if this event is allowed from FE.
func ShouldAcceptFromClient(label string) bool {
	d := RouteFor(label)
	return d == DirFromClient || d == DirBoth
}

// MustJSON marshals v to json.RawMessage, returning {} on error.
func MustJSON(v any) json.RawMessage {
	data, err := json.Marshal(v)
	if err != nil {
		return json.RawMessage(`{}`)
	}
	return data
}
