package channel import ( "context" "fmt" "sync" "github.com/gochat/gochat/internal/model" ) // ChannelRegistry is the plugin registration center for channel providers. // Reference: P2D §4 — replaces Chatwoot's channel_type enum with runtime registration // New channels just implement ChannelProvider + call Register(), no core code changes needed. type ChannelRegistry struct { mu sync.RWMutex providers map[ChannelType]ChannelProvider } // Global registry instance — all providers register here via init() or explicit call var globalRegistry = &ChannelRegistry{ providers: make(map[ChannelType]ChannelProvider), } // Register adds a channel provider to the global registry. // Returns error if a provider of the same type is already registered. // Pattern: each channel package calls channel.Register() in its init() function. func Register(provider ChannelProvider) error { globalRegistry.mu.Lock() defer globalRegistry.mu.Unlock() ct := provider.Type() if _, exists := globalRegistry.providers[ct]; exists { return fmt.Errorf("channel type %s already registered", ct) } globalRegistry.providers[ct] = provider return nil } // MustRegister is like Register but panics on duplicate registration. // Used in init() functions where failure is a programming error. func MustRegister(provider ChannelProvider) { if err := Register(provider); err != nil { panic(err) } } // Get retrieves a channel provider by type from the global registry. func Get(ct ChannelType) (ChannelProvider, error) { globalRegistry.mu.RLock() defer globalRegistry.mu.RUnlock() provider, ok := globalRegistry.providers[ct] if !ok { return nil, fmt.Errorf("channel type %s not registered", ct) } return provider, nil } // MustGet is like Get but panics if provider not found. func MustGet(ct ChannelType) ChannelProvider { provider, err := Get(ct) if err != nil { panic(err) } return provider } // GetRegistry returns the global channel registry instance. // Used by MessageBroker and other components that need access to the registry struct. func GetRegistry() *ChannelRegistry { return globalRegistry } // RegisterOnInstance adds a channel provider to this registry instance. // Returns error if a provider of the same type is already registered. // This is the instance-level counterpart of the global Register() function. func (r *ChannelRegistry) RegisterOnInstance(provider ChannelProvider) error { r.mu.Lock() defer r.mu.Unlock() ct := provider.Type() if _, exists := r.providers[ct]; exists { return fmt.Errorf("channel type %s already registered", ct) } r.providers[ct] = provider return nil } // GetOnRegistry retrieves a channel provider by type from a specific registry instance. func (r *ChannelRegistry) Get(ct ChannelType) (ChannelProvider, error) { r.mu.RLock() defer r.mu.RUnlock() provider, ok := r.providers[ct] if !ok { return nil, fmt.Errorf("channel type %s not registered", ct) } return provider, nil } // List returns all registered channel types. func List() []ChannelType { globalRegistry.mu.RLock() defer globalRegistry.mu.RUnlock() types := make([]ChannelType, 0, len(globalRegistry.providers)) for ct := range globalRegistry.providers { types = append(types, ct) } return types } // ListProviders returns all registered provider instances. func ListProviders() []ChannelProvider { globalRegistry.mu.RLock() defer globalRegistry.mu.RUnlock() providers := make([]ChannelProvider, 0, len(globalRegistry.providers)) for _, p := range globalRegistry.providers { providers = append(providers, p) } return providers } // IsRegistered checks if a channel type is registered. func IsRegistered(ct ChannelType) bool { globalRegistry.mu.RLock() defer globalRegistry.mu.RUnlock() _, exists := globalRegistry.providers[ct] return exists } // ProcessIncomingMessage is the unified inbound message processing pipeline. // Reference: P2D §8 — all inbound messages flow through this pipeline // This is the primary entry point for webhook handlers and polling workers. func ProcessIncomingMessage(ctx context.Context, ct ChannelType, inbox *model.Inbox, rawPayload []byte) (*IncomingMessage, error) { provider, err := Get(ct) if err != nil { return nil, fmt.Errorf("no provider for channel type %s: %w", ct, err) } return provider.ProcessIncoming(ctx, inbox, rawPayload) } // SendMessageToChannel is the unified outbound message sending pipeline. // Reference: P2D §8 — all outbound messages flow through this pipeline func SendMessageToChannel(ctx context.Context, ct ChannelType, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*SendResult, error) { provider, err := Get(ct) if err != nil { return nil, fmt.Errorf("no provider for channel type %s: %w", ct, err) } return provider.SendMessage(ctx, inbox, message, contact) } // ValidateWebhookRequest delegates webhook validation to the appropriate provider. func ValidateWebhookRequest(ctx context.Context, ct ChannelType, inbox *model.Inbox, request *WebhookRequest) error { provider, err := Get(ct) if err != nil { return fmt.Errorf("no provider for channel type %s: %w", ct, err) } return provider.ValidateWebhookRequest(ctx, inbox, request) }