package im

import (
	"context"
	"net/http"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

const maxConversationLookupRequestBodyBytes int64 = 16 * 1024

func limitConversationLookupRequestBody(ctx *gin.Context) {
	ctx.Request.Body = http.MaxBytesReader(
		ctx.Writer,
		ctx.Request.Body,
		maxConversationLookupRequestBodyBytes,
	)
	ctx.Next()
}

// RegisterRoutes mounts HTTP Workspace IM APIs under /api/v1/workspaces/:workspaceId.
func RegisterRoutes(router gin.IRouter, dependencies Dependencies) {
	if router == nil {
		return
	}
	workspaceScoped := routeWorkspaceScopedGroup(router, dependencies)
	newWorkspaceIMRouteHandlers(dependencies).register(workspaceScoped)
}

type workspaceIMRouteHandlers struct {
	snapshot      *SnapshotHandler
	scopes        *ScopesHandler
	scopeSnapshot *ScopeSnapshotHandler
	scopeExtras   *ScopeExtrasHandler
	messages      *MessagesHandler
	dms           *DMHandler
	rewind        *RewindHandler
	conversations *ConversationsHandler
	reactions     *ReactionsHandler
	pins          *PinsHandler
}

func newWorkspaceIMRouteHandlers(dependencies Dependencies) workspaceIMRouteHandlers {
	messages := &MessagesHandler{
		messages:           dependencies.Messages,
		queries:            dependencies.MessageQueries,
		streamWriter:       dependencies.StreamWriter,
		writer:             dependencies.HumanMessages,
		agentDeliveries:    dependencies.AgentDeliveries,
		agentNotifier:      dependencies.AgentNotifier,
		replyRootPreloader: dependencies.ReplyThreadRootPreloader,
		scopeFanoutMarker:  dependencies.ScopeFanoutMarker,
		taskScopeLifecycle: dependencies.TaskScopeLifecycle,
		unreadRuntime:      dependencies.MessageUnreadRuntime,
		unreadSink:         dependencies.UnreadDeltaSink,
		logger:             dependencies.Logger,
	}
	rewind := &RewindHandler{
		store:         dependencies.RewindStore,
		hub:           dependencies.RewindHub,
		streamWriter:  dependencies.StreamWriter,
		noticeWriter:  dependencies.HumanMessages,
		unreadRuntime: dependencies.MessageUnreadRuntime,
		unreadSink:    dependencies.UnreadDeltaSink,
		logger:        dependencies.Logger,
	}
	messages.rewind = rewind
	return workspaceIMRouteHandlers{
		snapshot: &SnapshotHandler{
			messages:                   dependencies.Messages,
			channels:                   dependencies.Channels,
			dms:                        dependencies.DMs,
			conversations:              dependencies.Conversations,
			conversationSidebarThreads: dependencies.ConversationSidebarThreads,
			conversationIndex:          dependencies.ConversationIndex,
			streamEvents:               dependencies.StreamEvents,
			streamCursors:              dependencies.StreamCursors,
			messageSearch:              dependencies.MessageSearch,
			unreadRuntime:              dependencies.UnreadRuntime,
		},
		scopes: &ScopesHandler{},
		scopeSnapshot: &ScopeSnapshotHandler{
			access:         dependencies.ChannelAccess,
			messages:       dependencies.Messages,
			channelMembers: dependencies.ChannelMembers,
			taskBoard:      dependencies.ScopeSnapshotTaskBoard,
		},
		scopeExtras: &ScopeExtrasHandler{
			ownerContexts:        dependencies.OwnerContexts,
			channelNotifications: dependencies.ChannelNotifications,
			threadNotifications:  dependencies.ThreadNotifications,
			conversations:        dependencies.Conversations,
			streamWriter:         dependencies.StreamWriter,
			unreadRuntime:        dependencies.UnreadRuntime,
			unreadSink:           dependencies.UnreadDeltaSink,
		},
		messages: messages,
		dms:      &DMHandler{dms: dependencies.DMs, streamWriter: dependencies.StreamWriter, scopeSync: dependencies.ScopeMembershipSync},
		rewind:   rewind,
		conversations: &ConversationsHandler{
			conversations:     dependencies.Conversations,
			conversationIndex: dependencies.ConversationIndex,
			streamWriter:      dependencies.StreamWriter,
			unreadRuntime:     dependencies.UnreadRuntime,
			unreadSink:        dependencies.UnreadDeltaSink,
		},
		reactions: &ReactionsHandler{
			reactions:          dependencies.Reactions,
			streamWriter:       dependencies.StreamWriter,
			taskScopeLifecycle: dependencies.TaskScopeLifecycle,
		},
		pins: &PinsHandler{pins: dependencies.Pins, streamWriter: dependencies.StreamWriter},
	}
}

func (handlers workspaceIMRouteHandlers) register(workspaceScoped *gin.RouterGroup) {
	workspaceScoped.GET("/snapshot", api.HandleWorkspaceScoped(handlers.snapshot.Get))
	workspaceScoped.GET("/scopes", api.HandleWorkspaceScoped(handlers.scopes.List))
	workspaceScoped.GET("/scopes/:scopeId", api.HandleWorkspaceScoped(handlers.scopes.Get))
	workspaceScoped.GET("/scopes/:scopeId/snapshot", api.HandleWorkspaceScoped(handlers.scopeSnapshot.Get))
	workspaceScoped.POST("/scopes/:scopeId/owner-context-dismiss", api.HandleWorkspaceScopedJSON[dto.ChannelDismissOwnerContextRequest](handlers.scopeExtras.DismissOwnerContext))
	workspaceScoped.GET("/scopes/:scopeId/notifications", api.HandleWorkspaceScoped(handlers.scopeExtras.NotificationsGet))
	workspaceScoped.POST("/scopes/:scopeId/notifications/self", api.HandleWorkspaceScopedJSON[dto.NotificationStateRequest](handlers.scopeExtras.NotificationsSelfSet))
	workspaceScoped.POST("/scopes/:scopeId/notifications/agents/:agentId", api.HandleWorkspaceScopedJSON[dto.NotificationWakeStateRequest](handlers.scopeExtras.NotificationsAgentSet))
	workspaceScoped.POST("/scopes/:scopeId/notifications/agents/all", api.HandleWorkspaceScopedJSON[dto.NotificationWakeStateRequest](handlers.scopeExtras.NotificationsAgentsSet))

	workspaceScoped.GET("/dm-channels", api.HandleWorkspaceScoped(handlers.dms.List))
	workspaceScoped.POST("/dm-channels", api.HandleWorkspaceScopedJSON[dto.OpenDMRequest](handlers.dms.Open))
	workspaceScoped.GET("/dm-channels/:channelId", api.HandleWorkspaceScoped(handlers.dms.Get))

	workspaceScoped.GET("/messages/search", api.HandleWorkspaceScoped(handlers.messages.Search))
	workspaceScoped.GET("/messages/:messageId/delivery", api.HandleWorkspaceScoped(handlers.messages.Delivery))
	workspaceScoped.POST("/messages/:messageId/agent-delivery", api.HandleWorkspaceScopedJSON[dto.AgentDeliveryCreateOptions](handlers.messages.SubmitAgentDelivery))
	workspaceScoped.POST("/messages/:messageId/rewind", api.HandleWorkspaceScoped(handlers.rewind.Rewind))

	workspaceScoped.GET("/scopes/:scopeId/messages", api.HandleWorkspaceScoped(handlers.messages.List))
	workspaceScoped.POST("/scopes/:scopeId/messages", api.HandleWorkspaceScopedJSON[messageCreateRequest](handlers.messages.Create))
	workspaceScoped.PATCH("/scopes/:scopeId/messages/:messageId", api.HandleWorkspaceScopedJSON[messagePatchRequest](handlers.messages.Update))
	workspaceScoped.POST("/scopes/:scopeId/messages/:messageId/recall", api.HandleWorkspaceScopedJSON[messageRecallRequest](handlers.messages.Recall))
	workspaceScoped.DELETE("/scopes/:scopeId/messages/:messageId", api.HandleWorkspaceScoped(handlers.messages.Delete))

	workspaceScoped.GET("/conversations", api.HandleWorkspaceScoped(handlers.conversations.List))
	workspaceScoped.POST("/conversations/actions", api.HandleWorkspaceScopedJSON[conversationsBulkActionRequest](handlers.conversations.BulkAction))
	workspaceScoped.POST(
		"/conversations/lookup",
		limitConversationLookupRequestBody,
		api.HandleWorkspaceScopedJSON[dto.ConversationLookupRequest](handlers.conversations.Lookup),
	)
	workspaceScoped.GET("/conversations/:conversationId", api.HandleWorkspaceScoped(handlers.conversations.Get))
	workspaceScoped.POST("/conversations/:conversationId/actions", api.HandleWorkspaceScopedJSON[conversationActionRequest](handlers.conversations.ActionJSON))
	workspaceScoped.POST("/conversations/:conversationId/read", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionMarkRead)))
	workspaceScoped.POST("/conversations/:conversationId/unread", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionMarkUnread)))
	workspaceScoped.POST("/conversations/:conversationId/pin", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionPin)))
	workspaceScoped.DELETE("/conversations/:conversationId/pin", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionUnpin)))
	workspaceScoped.POST("/conversations/:conversationId/archive", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionHide)))
	workspaceScoped.POST("/conversations/:conversationId/mute", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionMute)))
	workspaceScoped.DELETE("/conversations/:conversationId/mute", api.HandleWorkspaceScoped(handlers.conversations.Action(message.ConversationActionUnmute)))

	workspaceScoped.GET("/scopes/:scopeId/messages/:messageId/reactions", api.HandleWorkspaceScoped(handlers.reactions.Get))
	workspaceScoped.PUT("/scopes/:scopeId/messages/:messageId/reactions/:emoji", api.HandleWorkspaceScoped(handlers.reactions.Add))
	workspaceScoped.DELETE("/scopes/:scopeId/messages/:messageId/reactions/:emoji", api.HandleWorkspaceScoped(handlers.reactions.Remove))

	workspaceScoped.GET("/scopes/:scopeId/pins", api.HandleWorkspaceScoped(handlers.pins.List))
	workspaceScoped.PUT("/scopes/:scopeId/messages/:messageId/pin", api.HandleWorkspaceScoped(handlers.pins.Pin))
	workspaceScoped.DELETE("/scopes/:scopeId/messages/:messageId/pin", api.HandleWorkspaceScoped(handlers.pins.UnpinForEveryone))
	workspaceScoped.GET("/scopes/:scopeId/top-notice", api.HandleWorkspaceScoped(handlers.pins.GetTop))
	workspaceScoped.PUT("/scopes/:scopeId/messages/:messageId/top-notice", api.HandleWorkspaceScoped(handlers.pins.Top))
	workspaceScoped.DELETE("/scopes/:scopeId/messages/:messageId/top-notice", api.HandleWorkspaceScoped(handlers.pins.UntopForEveryone))
	workspaceScoped.PUT("/scopes/:scopeId/messages/:messageId/top-notice/banner-hide", api.HandleWorkspaceScoped(handlers.pins.HideTopBannerForMe))
	workspaceScoped.DELETE("/scopes/:scopeId/messages/:messageId/top-notice/banner-hide", api.HandleWorkspaceScoped(handlers.pins.ShowTopBannerForMe))
	workspaceScoped.DELETE("/scopes/:scopeId/top-notice-hides", api.HandleWorkspaceScoped(handlers.pins.ResetTopBannerHidesForMe))
}

// RegisterWebSocketRoutes mounts the WebSocket-only event stream. Ack, resume,
// subscribeVisibleScope, and unsubscribeVisibleScope are accepted only as frames
// on this route; no HTTP/SSE fallback is registered.
func RegisterWebSocketRoutes(router gin.IRouter, dependencies Dependencies) {
	if router == nil {
		return
	}
	handler := &EventsWSHandler{
		sessionAuth:               dependencies.SessionAuth,
		workspaceStore:            dependencies.WorkspaceStore,
		emailVerificationRequired: dependencies.EmailVerificationRequired,
		streamEvents:              dependencies.StreamEvents,
		streamCursors:             dependencies.StreamCursors,
		streamSubscriptions:       dependencies.StreamSubscriptions,
		visibleScopes:             dependencies.VisibleScopes,
		logger:                    dependencies.Logger,
		runtimeBootstrap:          dependencies.RuntimeBootstrap,
		unreadRuntime:             dependencies.UnreadRuntime,
		liveEmitter:               dependencies.LiveEmitter,
		channelLister:             bootstrapChannelListerAdapter{channels: dependencies.Channels, dms: dependencies.DMs},
	}
	router.GET("/api/v1/workspaces/:workspaceId/events/stream", handler.Handle)
}

// bootstrapChannelListerAdapter fans the WS bootstrap channel listing out across
// the existing channel and DM stores so the handler does not need a new
// collaborator type.
type bootstrapChannelListerAdapter struct {
	channels ChannelStore
	dms      DMStore
}

func (adapter bootstrapChannelListerAdapter) ListChannelsForUser(ctx context.Context, userID string, workspaceID string) ([]message.ChannelSummary, error) {
	if adapter.channels == nil {
		return nil, nil
	}
	return adapter.channels.ListChannelsForUser(ctx, userID, workspaceID)
}

func (adapter bootstrapChannelListerAdapter) ListDMChannelsForUserWithLimit(ctx context.Context, userID string, workspaceID string, limit int) ([]message.DMChannel, error) {
	if adapter.dms == nil {
		return nil, nil
	}
	return adapter.dms.ListDMChannelsForUserWithLimit(ctx, userID, workspaceID, limit)
}

func routeWorkspaceScopedGroup(router gin.IRouter, dependencies Dependencies) *gin.RouterGroup {
	return router.Group(
		"/api/v1/workspaces/:workspaceId",
		api.RequireAuthenticatedUser(dependencies.SessionAuth),
		api.RequireVerifiedIdentity(dependencies.EmailVerificationRequired),
		api.RequireRouteWorkspace(dependencies.WorkspaceStore),
	)
}
