package im

import (
	"context"
	"log/slog"
	"net/http"
	"strings"

	"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/api/im/fanout"
	messageservice "github.com/tidbcloud/AgentTeam/internal/service/message"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/unread"
)

// MessagesHandler owns workspace-prefixed message history and mutation routes.
type MessagesHandler struct {
	messages           MessageStore
	queries            MessageQueryStore
	streamWriter       StreamEventWriter
	writer             HumanMessageWriter
	agentDeliveries    AgentDeliveryStore
	agentNotifier      AgentNotifier
	replyRootPreloader ReplyThreadRootPreloader
	scopeFanoutMarker  ScopeFanoutDeliveryMarker
	taskScopeLifecycle fanout.TaskScopeLifecycleGuard
	unreadRuntime      fanout.UnreadRuntime
	unreadSink         fanout.UnreadDeltaSink
	rewind             *RewindHandler
	logger             *slog.Logger
}

type messageCreateRequest struct {
	Content          string                   `json:"content"`
	Body             string                   `json:"body,omitempty"`
	ClientMessageID  string                   `json:"clientMessageId,omitempty"`
	MessageType      string                   `json:"messageType,omitempty"`
	SourceMessageIDs []string                 `json:"sourceMessageIds,omitempty"`
	AttachmentIDs    []string                 `json:"attachmentIds,omitempty"`
	Metadata         map[string]any           `json:"metadata,omitempty"`
	StartAgentThread bool                     `json:"startAgentThread,omitempty"`
	Mentions         []dto.MessageMentionHint `json:"mentions,omitempty"`
}

type messagePatchRequest struct {
	Content  string                   `json:"content,omitempty"`
	Body     string                   `json:"body,omitempty"`
	Patch    map[string]any           `json:"patch,omitempty"`
	Mentions []dto.MessageMentionHint `json:"mentions,omitempty"`
}

type messageRecallRequest struct {
	RewindConversation bool `json:"rewindConversation,omitempty"`
}

// List handles GET /api/v1/workspaces/:workspaceId/scopes/:scopeId/messages.
func (h *MessagesHandler) List(r api.WorkspaceScopedRequest) (api.Response, error) {
	if h.messages == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Internal server error")
	}
	scopeID, err := routeScopeID(r)
	if err != nil {
		return api.Response{}, err
	}
	limit, err := parseLimit(r.Gin.Query("limit"), defaultIMListLimit, maxIMListLimit)
	if err != nil {
		return api.Response{}, err
	}
	aroundMessageID := strings.TrimSpace(r.Gin.Query("around"))
	if aroundMessageID != "" {
		page, err := h.messages.ListMessagesAroundForUser(r.Context(), r.UserID(), r.WorkspaceID(), scopeID, aroundMessageID, limit)
		if err != nil {
			return api.Response{}, err
		}
		dtoPage := converter.ToAppMessagePage(page)
		return api.OK(newMessagePageResponse(r.WorkspaceID(), scopeID, dtoPage)), nil
	}
	before, err := parseOptionalInt64(r.Gin.Query("before"), "before")
	if err != nil {
		return api.Response{}, err
	}
	after, err := parseOptionalInt64(r.Gin.Query("after"), "after")
	if err != nil {
		return api.Response{}, err
	}
	page, err := h.messages.ListMessagesForUser(r.Context(), r.UserID(), r.WorkspaceID(), scopeID, limit, before, after)
	if err != nil {
		return api.Response{}, err
	}
	dtoPage := converter.ToAppMessagePage(page)
	return api.OK(newMessagePageResponse(r.WorkspaceID(), scopeID, dtoPage)), nil
}

// Create handles POST /api/v1/workspaces/:workspaceId/scopes/:scopeId/messages.
func (h *MessagesHandler) Create(r api.WorkspaceScopedJSON[messageCreateRequest]) (api.Response, error) {
	if h.writer == nil {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "IM message creation is not wired")
	}
	scopeID, err := routeScopeID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}
	content := strings.TrimSpace(r.Body.Content)
	if content == "" {
		content = strings.TrimSpace(r.Body.Body)
	}
	if content == "" && len(r.Body.AttachmentIDs) == 0 {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "content is required")
	}
	messageType, err := normalizeIMMessageType(r.Body.MessageType)
	if err != nil {
		return api.Response{}, err
	}
	if err := message.ValidateWorkflowContextMetadata(r.Body.Metadata, false); err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	mentionHints := messageMentionHintsFromDTO(r.Body.Mentions)
	if isTaskScopeMessageTarget(scopeID) {
		if messageType != "" {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "messageType is not supported for task scope messages")
		}
		if r.Body.StartAgentThread {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "task scope messages cannot start agent threads")
		}
	}
	ownerQuoteSourceMessageIDs := []string(nil)
	if messageType == message.MessageTypeOwnerQuote {
		ownerQuoteSourceMessageIDs = r.Body.SourceMessageIDs
	}
	appMessage, delivery, err := h.writer.SendHumanMessage(r.Context(), r.User.User, r.WorkspaceID(), scopeID, content, r.Body.AttachmentIDs, messageservice.SendOptions{
		MessageType:                messageType,
		Metadata:                   r.Body.Metadata,
		StartAgentThread:           r.Body.StartAgentThread,
		ClientMessageID:            strings.TrimSpace(r.Body.ClientMessageID),
		MentionHints:               mentionHints,
		OwnerQuoteSourceMessageIDs: ownerQuoteSourceMessageIDs,
	})
	if err != nil {
		return api.Response{}, err
	}
	if !delivery.IdempotentReplay {
		if err := h.deliverCommittedMessage(r.Context(), r.WorkspaceID(), appMessage, delivery); err != nil {
			if h.logger != nil {
				h.logger.Warn("deliver committed IM message", slog.String("message_id", appMessage.ID), slog.Any("error", err))
			}
		}
	}
	if decorator, ok := h.writer.(SentMessageDecorator); ok {
		if decorated, decorateErr := decorator.DecorateAppMessageForUser(r.Context(), r.UserID(), r.WorkspaceID(), appMessage); decorateErr == nil {
			appMessage = decorated
		}
	}
	dtoMessage := converter.ToAppMessage(appMessage)
	if delivery.StartedReplyThread == nil {
		return api.OK(dtoMessage), nil
	}
	replyThread := converter.ToReplyThreadSummary(*delivery.StartedReplyThread)
	return api.OK(dto.AppMessageCreateResponse{Message: dtoMessage, ReplyThread: &replyThread}), nil
}

// Update handles PATCH /api/v1/workspaces/:workspaceId/scopes/:scopeId/messages/:messageId.
func (h *MessagesHandler) Update(r api.WorkspaceScopedJSON[messagePatchRequest]) (api.Response, error) {
	if h.writer == nil {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "IM message editing is not wired")
	}
	editor, ok := h.writer.(HumanMessageEditor)
	if !ok {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "IM message editing is not wired")
	}
	scopeID, err := routeScopeID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}
	messageID := strings.TrimSpace(r.Gin.Param("messageId"))
	if messageID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "messageId is required")
	}
	content := strings.TrimSpace(r.Body.Content)
	if content == "" {
		content = strings.TrimSpace(r.Body.Body)
	}
	if content == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "content is required")
	}
	appMessage, err := editor.EditHumanMessage(r.Context(), r.User.User, r.WorkspaceID(), scopeID, messageID, content, messageservice.EditOptions{
		MentionHints: messageMentionHintsFromDTO(r.Body.Mentions),
	})
	if err != nil {
		return api.Response{}, err
	}
	if _, err := h.withActiveTaskScope(r.Context(), r.WorkspaceID(), scopeID, func(effectCtx context.Context) error {
		return publishMessageUpdatedEvent(effectCtx, h.streamWriter, r.WorkspaceID(), scopeID, appMessage)
	}); err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppMessage(appMessage)), nil
}

// Recall handles POST /api/v1/workspaces/:workspaceId/scopes/:scopeId/messages/:messageId/recall.
func (h *MessagesHandler) Recall(r api.WorkspaceScopedJSON[messageRecallRequest]) (api.Response, error) {
	scopeID, err := routeScopeID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}
	messageID, err := routeMessageID(r.WorkspaceScopedRequest)
	if err != nil {
		return api.Response{}, err
	}
	if r.Body.RewindConversation {
		if h.rewind == nil {
			return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "rewind is not supported")
		}
		plan, err := h.rewind.rewindMessage(r.Context(), r.User.User, r.WorkspaceID(), messageID, scopeID)
		if err != nil {
			return api.Response{}, err
		}
		return api.OK(dto.AppMessageRecallResponse{
			OK:     true,
			Mode:   "rewind",
			Rewind: recallRewindStateFromPlan(plan),
		}), nil
	}
	if h.writer == nil {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "message recall is not supported")
	}
	recaller, ok := h.writer.(HumanMessageRecaller)
	if !ok {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "message recall is not supported")
	}
	result, err := recaller.RecallHumanMessage(r.Context(), r.User.User, r.WorkspaceID(), scopeID, messageID)
	if err != nil {
		return api.Response{}, err
	}
	dtoMessage := converter.ToAppMessage(result.Message)
	if result.Changed {
		if _, err := h.withActiveTaskScope(r.Context(), r.WorkspaceID(), scopeID, func(effectCtx context.Context) error {
			if err := publishMessageRecalledEvent(effectCtx, h.streamWriter, r.WorkspaceID(), scopeID, result.Message, result.IsLatestVisibleMessage); err != nil {
				return err
			}
			if err := publishMessagePinsInvalidatedEvent(effectCtx, h.streamWriter, r.WorkspaceID(), scopeID, []string{messageID}); err != nil {
				return err
			}
			h.applyMessageRecallInvalidated(r.WorkspaceID(), scopeID)
			return nil
		}); err != nil {
			return api.Response{}, err
		}
	}
	return api.OK(dto.AppMessageRecallResponse{
		OK:      true,
		Mode:    "recall",
		Changed: result.Changed,
		Message: &dtoMessage,
	}), nil
}

func (h *MessagesHandler) applyMessageRecallInvalidated(workspaceID string, scopeID string) {
	if h == nil || h.unreadRuntime == nil {
		return
	}
	deltas, _ := h.unreadRuntime.ApplyScopeInvalidatedWithDeltas(unread.ScopeInvalidated{
		WorkspaceID: strings.TrimSpace(workspaceID),
		ScopeID:     strings.TrimSpace(scopeID),
		Reason:      "message_recall",
	})
	h.emitUnreadDeltas(workspaceID, deltas)
}

// Delete is intentionally not implemented until message delete semantics are owned
// by the message store; the target-state route remains reserved here.
func (h *MessagesHandler) Delete(r api.WorkspaceScopedRequest) (api.Response, error) {
	return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "message delete is not implemented")
}

func routeScopeID(r api.WorkspaceScopedRequest) (string, error) {
	scopeID := strings.TrimSpace(r.Gin.Param("scopeId"))
	if scopeID == "" {
		return "", api.NewHTTPError(http.StatusBadRequest, "scopeId is required")
	}
	return scopeID, nil
}

func normalizeIMMessageType(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", nil
	}
	normalized := message.NormalizeAppMessageType(trimmed)
	if normalized == "" {
		return "", api.NewHTTPError(http.StatusBadRequest, "unsupported messageType")
	}
	// The target-aware writer admits this restricted card type only after it
	// resolves a service-account scope and publisher authority.
	if normalized == message.MessageTypeServiceAccountPost {
		return normalized, nil
	}
	if !message.IsPublicHumanCreateMessageTypeAllowed(normalized) {
		return "", api.NewHTTPError(http.StatusBadRequest, "unsupported messageType")
	}
	return normalized, nil
}

func isTaskScopeMessageTarget(scopeID string) bool {
	return strings.HasPrefix(strings.TrimSpace(scopeID), "tsk_")
}

func messageMentionHintsFromDTO(mentions []dto.MessageMentionHint) []message.MessageMentionHint {
	// messaging.SendOptions intentionally uses store/message mention hints. Keep
	// this adapter local so IM does not import the legacy message API package.
	if len(mentions) == 0 {
		return nil
	}
	out := make([]message.MessageMentionHint, 0, min(len(mentions), message.MaxPersistedMentionHints))
	for _, mention := range mentions {
		if len(out) >= message.MaxPersistedMentionHints {
			break
		}
		mentionType := strings.TrimSpace(mention.Type)
		id := strings.TrimSpace(mention.ID)
		token := strings.TrimSpace(mention.Token)
		if mentionType == "" || id == "" || token == "" {
			continue
		}
		out = append(out, message.MessageMentionHint{Type: mentionType, ID: id, Token: token, Metadata: mention.Metadata})
	}
	return out
}
