package im

import (
	"net/http"
	"strings"
	"time"

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

const snapshotVersion = "workspace-im-v1"

// SnapshotHandler assembles the first-paint Workspace IM baseline from existing
// message/conversation read models plus the per-member IM stream high-water mark.
type SnapshotHandler struct {
	messages                   MessageStore
	channels                   ChannelStore
	dms                        DMStore
	conversations              ConversationStore
	conversationSidebarThreads ConversationSidebarThreadStore
	conversationIndex          ConversationIndexStore
	streamEvents               StreamEventStore
	streamCursors              StreamCursorStore
	messageSearch              MessageSearchStore
	unreadRuntime              UnreadRuntime
}

// Get handles GET /api/v1/workspaces/:workspaceId/snapshot.
func (h *SnapshotHandler) Get(r api.WorkspaceScopedRequest) (api.Response, error) {
	memberID := strings.TrimSpace(r.Workspace.MemberID)
	if memberID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusForbidden, "workspace member is required")
	}
	includeChannels, err := parseOptionalBoolSnapshotQuery(r, "includeChannels", true)
	if err != nil {
		return api.Response{}, err
	}
	includeDMChannels, err := parseOptionalBoolSnapshotQuery(r, "includeDMChannels", true)
	if err != nil {
		return api.Response{}, err
	}
	clientID := strings.TrimSpace(r.Gin.Query("clientId"))
	advanceStreamCursor := parseBoolSnapshotQuery(r, "advanceStreamCursor")
	if advanceStreamCursor &&
		!isSnapshotBootstrapClientID(r, clientID) {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "invalid snapshot clientId")
	}
	latestSeq, err := latestStreamSeq(r, h.streamEvents, memberID)
	if err != nil {
		return api.Response{}, err
	}
	snapshotFence := latestSeq
	cursor := streamCursorResponse{ClientID: clientID, LatestEventSeq: latestSeq, AckedEventSeq: latestSeq}
	if h.streamCursors != nil && clientID != "" {
		loaded, err := h.streamCursors.GetCursor(r.Context(), r.WorkspaceID(), memberID, clientID)
		if err != nil {
			return api.Response{}, err
		}
		cursor.AckedEventSeq = loaded.AckedEventSeq
		cursor.CursorGeneration = strings.TrimSpace(loaded.CursorGeneration)
		snapshotFence = max(snapshotFence, loaded.AckedEventSeq)
		cursor.LatestEventSeq = snapshotFence
	}

	conversationLimit, err := parseLimit(firstSnapshotQueryValue(r, "conversationLimit", "limitConversations"), defaultIMListLimit, maxIMListLimit)
	if err != nil {
		return api.Response{}, err
	}
	conversations, err := h.listConversations(r, conversationLimit)
	if err != nil {
		return api.Response{}, err
	}
	h.overlayUnread(r.WorkspaceID(), memberID, conversations.Items)
	unreadCounts := h.workspaceUnreadCounts(r.WorkspaceID(), memberID)
	unreadConversationCount := h.workspaceUnreadConversationCount(r.WorkspaceID(), memberID)
	threadLimit, err := parseLimit(firstSnapshotQueryValue(r, "threadLimit", "limitSubscribedThreads"), 15, maxIMListLimit)
	if err != nil {
		return api.Response{}, err
	}
	subscribedThreads, err := h.listConversationSidebarThreads(r, threadLimit)
	if err != nil {
		return api.Response{}, err
	}

	dmLimit := defaultIMDMListLimit
	if includeDMChannels {
		dmLimit, err = parseDMLimit(firstSnapshotQueryValue(r, "dmLimit", "limitDMChannels"))
		if err != nil {
			return api.Response{}, err
		}
	}
	channels := []dto.ChannelResponse{}
	if includeChannels {
		channels, err = h.listChannels(r)
		if err != nil {
			return api.Response{}, err
		}
	}
	dms := []dto.DMChannel{}
	if includeDMChannels {
		dms, err = h.listDMChannels(r, dmLimit)
		if err != nil {
			return api.Response{}, err
		}
	}
	var active *activeMessages
	if scopeID := strings.TrimSpace(firstSnapshotQueryValue(r, "activeScopeId", "scopeId")); scopeID != "" && h.messages != nil {
		messageLimit, err := parseLimit(firstSnapshotQueryValue(r, "messageLimit", "limitMessages"), defaultIMListLimit, maxIMListLimit)
		if err != nil {
			return api.Response{}, err
		}
		page, err := h.messages.ListMessagesForUser(r.Context(), r.UserID(), r.WorkspaceID(), scopeID, messageLimit, nil, nil)
		if err != nil {
			return api.Response{}, err
		}
		dtoPage := converter.ToAppMessagePage(page)
		active = &activeMessages{ScopeID: scopeID, Messages: dtoPage.Messages, Page: dtoPage}
	}

	response := snapshotResponse{
		WorkspaceID:      r.WorkspaceID(),
		MemberID:         memberID,
		SnapshotEventSeq: snapshotFence,
		SnapshotVersion:  snapshotVersion,
		StreamCursor:     cursor,
		ServerTime:       time.Now().UTC(),
		Viewer:           viewerResponse{UserID: r.UserID(), MemberID: memberID},
		Workspace: workspaceResponse{
			ID:             r.Workspace.WorkspaceID,
			OrganizationID: r.Workspace.OrganizationID,
			Slug:           r.Workspace.Slug,
			Name:           r.Workspace.Name,
			AllChannelID:   r.Workspace.AllChannelID,
		},
		Members:                 []any{},
		Scopes:                  []scopeResponse{},
		Channels:                channels,
		DMChannels:              dms,
		Conversations:           conversations,
		Unread:                  unreadSnapshotFromConversationPage(conversations),
		UnreadCounts:            unreadCounts,
		UnreadConversationCount: unreadConversationCount,
		SubscribedThreads:       subscribedThreads,
		MessageSearchEnabled:    h.messageSearchEnabled(r),
		ActiveMessages:          active,
	}
	response.Coverage.Channels = includeChannels
	response.Coverage.DMChannels = includeDMChannels
	contentETag, err := snapshotContentETag(response)
	if err != nil {
		return api.Response{}, err
	}
	response.ContentETag = contentETag
	notModified := r.Gin.Query("ifContentETag") == contentETag
	if advanceStreamCursor && h.streamCursors != nil {
		if err := h.streamCursors.UpdateCursor(
			r.Context(),
			r.WorkspaceID(),
			memberID,
			clientID,
			latestSeq,
		); err != nil {
			return api.Response{}, err
		}
		response.StreamCursor.AckedEventSeq = max(
			response.StreamCursor.AckedEventSeq,
			latestSeq,
		)
	}
	if notModified {
		return api.OK(snapshotNotModifiedResponse{
			NotModified:      true,
			ContentETag:      response.ContentETag,
			SnapshotEventSeq: response.SnapshotEventSeq,
			StreamCursor:     response.StreamCursor,
			ServerTime:       response.ServerTime,
		}), nil
	}
	return api.OK(response), nil
}

func isSnapshotBootstrapClientID(r api.WorkspaceScopedRequest, clientID string) bool {
	if isMiniappSnapshotRequest(r) {
		return isMiniappSnapshotClientID(clientID, r.WorkspaceID())
	}
	return isWorkspaceIMStreamClientID(clientID, r.WorkspaceID(), r.UserID())
}

func isMiniappSnapshotRequest(r api.WorkspaceScopedRequest) bool {
	return strings.EqualFold(
		strings.TrimSpace(r.Gin.GetHeader("X-AgentTeam-Route-Source")),
		"mobile.miniapp",
	)
}

func isMiniappSnapshotClientID(clientID string, workspaceID string) bool {
	parts := strings.Split(clientID, ":")
	if len(parts) != 3 || parts[0] != "miniapp" || parts[2] != strings.TrimSpace(workspaceID) {
		return false
	}
	installationID := parts[1]
	if len(installationID) < 5 || len(installationID) > 128 || !strings.HasPrefix(installationID, "ins_") {
		return false
	}
	for _, r := range installationID {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
			continue
		}
		return false
	}
	return true
}

func parseBoolSnapshotQuery(r api.WorkspaceScopedRequest, key string) bool {
	switch strings.ToLower(strings.TrimSpace(r.Gin.Query(key))) {
	case "1", "true", "yes":
		return true
	default:
		return false
	}
}

func parseOptionalBoolSnapshotQuery(r api.WorkspaceScopedRequest, key string, defaultValue bool) (bool, error) {
	values, present := r.Gin.Request.URL.Query()[key]
	if !present {
		return defaultValue, nil
	}
	if len(values) != 1 {
		return false, api.NewHTTPError(http.StatusBadRequest, key+" must be specified at most once")
	}
	switch strings.ToLower(strings.TrimSpace(values[0])) {
	case "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, api.NewHTTPError(http.StatusBadRequest, key+" must be true or false")
	}
}

func (h *SnapshotHandler) listConversationSidebarThreads(r api.WorkspaceScopedRequest, limit int) ([]dto.SubscribedThread, error) {
	if h.conversationSidebarThreads == nil {
		return []dto.SubscribedThread{}, nil
	}
	page, err := h.conversationSidebarThreads.ListSubscribedThreadInboxPageForUser(
		r.Context(),
		r.UserID(),
		r.WorkspaceID(),
		limit,
		message.ThreadInboxCursor{},
	)
	if err != nil {
		return nil, err
	}
	return converter.ToSubscribedThreads(page.Threads), nil
}

func latestStreamSeq(r api.WorkspaceScopedRequest, events StreamEventStore, memberID string) (int64, error) {
	if events == nil {
		return 0, nil
	}
	latest, err := events.GetLatestSeq(r.Context(), r.WorkspaceID(), memberID)
	if err != nil {
		return 0, err
	}
	return latest, nil
}

func (h *SnapshotHandler) listConversations(r api.WorkspaceScopedRequest, limit int) (dto.ConversationPage, error) {
	return h.listConversationsFiltered(r, limit, message.ConversationKindFilter{})
}

func (h *SnapshotHandler) listConversationsFiltered(r api.WorkspaceScopedRequest, limit int, filter message.ConversationKindFilter) (dto.ConversationPage, error) {
	handler := ConversationsHandler{conversations: h.conversations, conversationIndex: h.conversationIndex}
	var response api.Response
	var err error
	// The member-local index is the first-paint/read-more projection. It already
	// contains the sorted rows and presentation facts, so using the legacy
	// aggregate here would rebuild all channel/DM/thread visibility CTEs for
	// every snapshot page even when the index is available.
	if h.conversationIndex != nil {
		response, err = handler.listFromConversationIndex(r, limit, filter)
	} else {
		response, err = handler.listFromAuthoritativeStore(r, limit, filter)
	}
	if err != nil {
		return dto.ConversationPage{}, err
	}
	page, ok := response.Body.(dto.ConversationPage)
	if !ok {
		return dto.ConversationPage{Items: []dto.ConversationItem{}}, nil
	}
	if page.Items == nil {
		page.Items = []dto.ConversationItem{}
	}
	return page, nil
}

func (h *SnapshotHandler) listChannels(r api.WorkspaceScopedRequest) ([]dto.ChannelResponse, error) {
	if h.channels == nil {
		return []dto.ChannelResponse{}, nil
	}
	channels, err := h.channels.ListChannelsForUser(r.Context(), r.UserID(), r.WorkspaceID())
	if err != nil {
		return nil, err
	}
	return channelResponses(channels), nil
}

func (h *SnapshotHandler) listDMChannels(r api.WorkspaceScopedRequest, limit int) ([]dto.DMChannel, error) {
	if h.dms == nil {
		return []dto.DMChannel{}, nil
	}
	dms, err := h.dms.ListDMChannelsForUserWithLimit(r.Context(), r.UserID(), r.WorkspaceID(), limit)
	if err != nil {
		return nil, err
	}
	return converter.ToDMChannels(dms), nil
}

func (h *SnapshotHandler) messageSearchEnabled(r api.WorkspaceScopedRequest) bool {
	if h.messageSearch == nil {
		return false
	}
	return h.messageSearch.SupportsWorkspaceMessageSearch(r.Context(), r.WorkspaceID())
}

func (h *SnapshotHandler) workspaceUnreadCounts(workspaceID string, memberID string) map[string]int64 {
	counts := make(map[string]int64)
	if h == nil || h.unreadRuntime == nil {
		return counts
	}
	for scopeID, state := range h.unreadRuntime.WorkspaceStates(workspaceID, memberID) {
		trimmedScopeID := strings.TrimSpace(scopeID)
		if trimmedScopeID == "" {
			trimmedScopeID = strings.TrimSpace(state.ScopeID)
		}
		if trimmedScopeID == "" {
			continue
		}
		count := conversationRuntimeUnreadCount("", state)
		if count > 0 {
			counts[trimmedScopeID] = count
		}
	}
	return counts
}

// workspaceUnreadConversationCount returns the authoritative workspace-wide
// count of conversations that should drive Inbox attention. A scope contributes
// when it has either a positive runtime unread count or a manual-unread flag,
// matching ScopeUnreadState.HasUnread. This is intentionally wider than
// workspaceUnreadCounts (which is count-only) so the Inbox header badge stays
// accurate even when the unread row lives on a page the client has not loaded.
func (h *SnapshotHandler) workspaceUnreadConversationCount(workspaceID string, memberID string) int64 {
	if h == nil || h.unreadRuntime == nil {
		return 0
	}
	return h.unreadRuntime.WorkspaceUnreadConversationCount(workspaceID, memberID)
}

func (h *SnapshotHandler) overlayUnread(workspaceID string, memberID string, items []dto.ConversationItem) {
	if len(items) == 0 {
		return
	}
	scopeIDs := make([]string, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for index := range items {
		items[index].UnreadCount = 0
		items[index].ManualUnread = false
		scopeID := strings.TrimSpace(items[index].ScopeID)
		if scopeID == "" {
			continue
		}
		if _, ok := seen[scopeID]; ok {
			continue
		}
		seen[scopeID] = struct{}{}
		scopeIDs = append(scopeIDs, scopeID)
	}
	if h == nil || h.unreadRuntime == nil || len(scopeIDs) == 0 {
		return
	}
	states := h.unreadRuntime.ScopeStates(workspaceID, memberID, scopeIDs)
	for index := range items {
		state, ok := states[strings.TrimSpace(items[index].ScopeID)]
		if !ok {
			continue
		}
		items[index].UnreadCount = conversationRuntimeUnreadCount(items[index].Kind, state)
		items[index].ManualUnread = state.ManualUnread
	}
}

func firstSnapshotQueryValue(r api.WorkspaceScopedRequest, names ...string) string {
	query := r.Gin.Request.URL.Query()
	for _, name := range names {
		if value := strings.TrimSpace(query.Get(name)); value != "" {
			return value
		}
	}
	return ""
}

func channelResponses(summaries []message.ChannelSummary) []dto.ChannelResponse {
	if len(summaries) == 0 {
		return []dto.ChannelResponse{}
	}
	channels := make([]dto.ChannelResponse, 0, len(summaries))
	for _, summary := range summaries {
		subsystem := summary.Subsystem
		if subsystem == message.ChannelSubsystemIM {
			subsystem = ""
		}
		channels = append(channels, dto.ChannelResponse{
			ID:                summary.ChannelID,
			Name:              summary.Slug,
			Slug:              summary.Slug,
			DisplayName:       summary.DisplayName,
			Description:       summary.Description,
			Visibility:        summary.Visibility,
			Subsystem:         subsystem,
			Tutorial:          summary.Tutorial,
			TeamID:            summary.TeamID,
			TeamHandle:        summary.TeamHandle,
			TeamDisplayName:   summary.TeamDisplayName,
			Joined:            summary.Joined,
			CanInvite:         summary.CanInvite,
			CanManage:         summary.CanManage,
			Archived:          summary.Archived,
			SubscriptionState: summary.SubscriptionState,
			PinnedAt:          summary.PinnedAt,
		})
	}
	return channels
}

func unreadSnapshotFromConversationPage(page dto.ConversationPage) unreadResponse {
	byConversation := make(map[string]any)
	var total int64
	for _, item := range page.Items {
		if item.UnreadCount <= 0 && !item.ManualUnread {
			continue
		}
		total += item.UnreadCount
		byConversation[item.ID] = map[string]any{
			"scopeId":      item.ScopeID,
			"unreadCount":  item.UnreadCount,
			"manualUnread": item.ManualUnread,
		}
	}
	return unreadResponse{Total: total, ByConversation: byConversation}
}
