package app

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

func (store *Store) EditMessageForUser(ctx context.Context, user model.User, workspaceID string, channelID string, messageID string, content string, options message.HumanMessageEditOptions) (message.AppMessage, error) {
	trimmedContent := strings.TrimSpace(content)
	if trimmedContent == "" {
		return message.AppMessage{}, badRequest("content is required")
	}
	trimmedMessageID := strings.TrimSpace(messageID)
	if trimmedMessageID == "" {
		return message.AppMessage{}, badRequest("messageId is required")
	}

	var appMessage message.AppMessage
	var viewerMemberID string
	var moderationBlock *moderationBlockRecord
	if err := coordinator.New(store.db).Run(ctx, "edit app message workflow", func(tx coredb.Tx) error {
		if ok, err := isActiveTaskScope(ctx, tx, workspaceID, channelID); err != nil {
			return err
		} else if ok {
			membership, err := access.LoadHumanWorkspaceMember(ctx, tx, user.ID, workspaceID)
			if err != nil {
				return err
			}
			viewerMemberID = strings.TrimSpace(membership.Member.ID)
			taskScopeID := strings.TrimSpace(channelID)
			target := message.ThreadAccess{
				ThreadType: domain.ThreadTypeChannel,
				ChannelID:  taskScopeID,
			}
			edited, err := store.editScopeMessageForMemberTx(ctx, tx, user, workspaceID, target, taskScopeID, trimmedMessageID, viewerMemberID, membership.Member.DisplayName, trimmedContent, options)
			if err != nil {
				return err
			}
			appMessage = edited.appMessage
			moderationBlock = edited.moderationBlock
			return nil
		}

		membership, target, err := store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, tx, user.ID, workspaceID, channelID, channelPostAction, dmPostAction, threadPostAction)
		if err != nil {
			return err
		}
		viewerMemberID = strings.TrimSpace(membership.Member.ID)
		scopeID := message.ScopeIDForThreadAccess(target)
		if scopeID == "" {
			return badRequest("scope is required")
		}
		edited, err := store.editScopeMessageForMemberTx(ctx, tx, user, workspaceID, target, scopeID, trimmedMessageID, viewerMemberID, membership.Member.DisplayName, trimmedContent, options)
		if err != nil {
			return err
		}
		appMessage = edited.appMessage
		moderationBlock = edited.moderationBlock
		return nil
	}); err != nil {
		if errors.Is(err, message.ErrContentModerationBlocked) && moderationBlock != nil {
			if insertErr := store.insertModerationBlock(ctx, store.db, *moderationBlock); insertErr != nil {
				return message.AppMessage{}, insertErr
			}
		}
		return message.AppMessage{}, err
	}
	if appMessage.ID == "" {
		return message.AppMessage{}, notFound("message not found")
	}
	messages := []message.AppMessage{appMessage}
	if err := store.attachAppMessageReadDecorations(ctx, workspaceID, viewerMemberID, messages); err != nil {
		return message.AppMessage{}, err
	}
	return messages[0], nil
}

type editedScopeMessageResult struct {
	appMessage      message.AppMessage
	moderationBlock *moderationBlockRecord
}

func (store *Store) editScopeMessageForMemberTx(ctx context.Context, tx coredb.Tx, user model.User, workspaceID string, target message.ThreadAccess, scopeID string, trimmedMessageID string, viewerMemberID string, viewerMemberDisplayName string, trimmedContent string, options message.HumanMessageEditOptions) (editedScopeMessageResult, error) {
	repos := message.NewScopeRepositories(tx)
	record, err := repos.Messages.LoadByID(ctx, workspaceID, scopeID, trimmedMessageID)
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	readCheck, err := message.NewScopeAPIServiceForQueryer(tx).CanReadMessage(ctx, message.ScopeAPIReadCheckRequest{
		WorkspaceID:    workspaceID,
		MessageID:      trimmedMessageID,
		ViewerMemberID: viewerMemberID,
		ViewerType:     message.ScopeActorHuman,
		RequestKind:    message.ScopeVisibilityRequestHistory,
	})
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	if !readCheck.Allowed {
		return editedScopeMessageResult{}, notFound("message not found")
	}
	if strings.TrimSpace(record.AuthorType) != "human" || strings.TrimSpace(record.AuthorMemberID) != viewerMemberID {
		return editedScopeMessageResult{}, forbidden("only the original author can edit this message")
	}
	if !isEditableUserTextMessageType(record.MessageType) {
		return editedScopeMessageResult{}, badRequest("only text messages can be edited")
	}
	if strings.TrimSpace(record.MessageType) != message.MessageTypeTaskDiscussion {
		if err := validateMessageEditRoutingContent(target, trimmedContent); err != nil {
			return editedScopeMessageResult{}, err
		}
	}
	hasAttachments, err := scopeMessageHasAttachments(ctx, tx, workspaceID, scopeID, trimmedMessageID)
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	if hasAttachments {
		return editedScopeMessageResult{}, badRequest("messages with attachments cannot be edited")
	}

	match, matched, err := store.matchEnabledModerationKeyword(ctx, trimmedContent)
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	if matched {
		return editedScopeMessageResult{
			moderationBlock: &moderationBlockRecord{
				WorkspaceID:    strings.TrimSpace(workspaceID),
				ScopeID:        scopeID,
				ScopeType:      moderationScopeType(target),
				UserID:         strings.TrimSpace(user.ID),
				MemberID:       viewerMemberID,
				Content:        trimmedContent,
				MatchedKeyword: match.Keyword,
				CreatedAt:      time.Now().UTC(),
			},
		}, message.ContentModerationBlocked(contentModerationBlockedMessage)
	}

	encodedMetadata, normalizedMetadata, err := editedMessageMetadata(ctx, tx, workspaceID, target, viewerMemberID, record.Metadata, trimmedContent, options.MentionHints)
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	editedAt := time.Now().UTC()
	updated, err := message.UpdateScopeMessageContentAndSearchDoc(ctx, tx, message.ScopeMessageContentUpdate{
		WorkspaceID:      workspaceID,
		ScopeID:          scopeID,
		MessageID:        trimmedMessageID,
		Content:          trimmedContent,
		Metadata:         encodedMetadata,
		EditedByMemberID: viewerMemberID,
		EditedAt:         editedAt,
	})
	if err != nil {
		return editedScopeMessageResult{}, err
	}
	if err := repos.SyncDirty.MarkMessageDirtyForScope(ctx, workspaceID, scopeID, updated.Seq, "message_edit", editedAt); err != nil {
		return editedScopeMessageResult{}, err
	}
	return editedScopeMessageResult{
		appMessage: appMessageFromEditedRecord(appChannelIDForTarget(target), target, humanAuthorProfileFromUser(user, viewerMemberID, viewerMemberDisplayName), updated, normalizedMetadata),
	}, nil
}

func isActiveTaskScope(ctx context.Context, queryer coredb.Queryer, workspaceID string, scopeID string) (bool, error) {
	var marker int
	if err := queryer.QueryRow(ctx, `
SELECT 1
FROM scopes
WHERE workspace_id = $1
  AND id = $2
  AND type = 'task'
  AND status = 'active'
  AND archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID)).Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return false, nil
		}
		return false, fmt.Errorf("load task scope: %w", err)
	}
	return true, nil
}

func isEditableUserTextMessageType(messageType string) bool {
	switch strings.TrimSpace(messageType) {
	case "", message.MessageTypeTaskDiscussion:
		return true
	default:
		return false
	}
}

func validateMessageEditRoutingContent(target message.ThreadAccess, content string) error {
	routingContent := mentionRoutingContent("", content)
	if _, err := authorizeControlledBroadcastForUserMessage(target, routingContent); err != nil {
		return err
	}
	if target.ReplyThread != nil {
		return message.ValidateReplyThreadRoutingContent(routingContent)
	}
	return nil
}

func editedMessageMetadata(ctx context.Context, tx coredb.Tx, workspaceID string, target message.ThreadAccess, viewerMemberID string, rawMetadata []byte, content string, mentionHints []message.MessageMentionHint) ([]byte, map[string]any, error) {
	metadata, err := message.DecodeMessageMetadata(rawMetadata)
	if err != nil {
		return nil, nil, err
	}
	if err := message.ValidateWorkflowContextMetadata(metadata, false); err != nil {
		return nil, nil, badRequest("workflow-authored messages cannot be edited")
	}
	metadata = message.CopyMessageMetadata(metadata)
	delete(metadata, message.MentionMetadataKey)

	mentionContent := mentionRoutingContent("", content)
	resolvedMentionLinks := message.ParseResolvedMentionLinks(mentionContent)
	mentionHints = message.MentionHintsWithResolvedLinks(mentionHints, resolvedMentionLinks)
	directMentionTokens := message.ParseDirectMentionTokens(mentionContent)
	canonicalMentionTokens := resolvedMentionTokensForDirectTokens(resolvedMentionLinks, directMentionTokens)
	mentionMetadataTokens := directMentionTokens
	var mentionMetadataRecipients []message.AgentRecipient
	if target.ThreadType != domain.ThreadTypeDM && len(directMentionTokens) > 0 {
		mentionChannelID := strings.TrimSpace(target.ChannelID)
		if target.ReplyThread != nil {
			mentionChannelID = strings.TrimSpace(target.ReplyThread.ParentChannelID)
		}
		hintRouting, err := resolveMentionHintAgentRecipientsForUser(ctx, tx, workspaceID, mentionChannelID, viewerMemberID, mentionHints, directMentionTokens, resolvedMentionLinks)
		if err != nil {
			return nil, nil, err
		}
		excludedMentionTokens := append(append([]string(nil), hintRouting.claimedTokens...), canonicalMentionTokens...)
		liveDirectMentionTokens := directMentionTokensExcluding(directMentionTokens, excludedMentionTokens)
		if len(liveDirectMentionTokens) > 0 {
			mentionedRecipients, err := message.LoadScopeMentionedAgentRecipientsForTokens(ctx, tx, mentionChannelID, viewerMemberID, liveDirectMentionTokens)
			if err != nil {
				return nil, nil, err
			}
			mentionMetadataRecipients = message.MergeAgentRecipients(mentionMetadataRecipients, mentionedRecipients)
		}
		mentionMetadataRecipients = message.MergeAgentRecipients(mentionMetadataRecipients, hintRouting.recipients)
	}
	if mentions := message.BuildPersistedMentionMetadata(mentionHints, mentionMetadataRecipients, mentionMetadataTokens); len(mentions) > 0 {
		if metadata == nil {
			metadata = make(map[string]any, 1)
		}
		metadata[message.MentionMetadataKey] = mentions
	}
	return message.EncodeMessageMetadata(metadata)
}

func scopeMessageHasAttachments(ctx context.Context, queryer coredb.Queryer, workspaceID string, scopeID string, messageID string) (bool, error) {
	var hasAttachments bool
	if err := queryer.QueryRow(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM attachments
  WHERE workspace_id = $1
    AND scope_id = $2
    AND message_id = $3
  LIMIT 1
)
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(messageID)).Scan(&hasAttachments); err != nil {
		return false, fmt.Errorf("load message attachment marker: %w", err)
	}
	return hasAttachments, nil
}

func appMessageFromEditedRecord(channelID string, target message.ThreadAccess, authorProfile message.AuthorProfile, record message.ScopeMessageRecord, metadata map[string]any) message.AppMessage {
	appMessage := message.AppMessage{
		ID:               strings.TrimSpace(record.ID),
		Seq:              record.Seq,
		ChannelID:        strings.TrimSpace(channelID),
		ClientMessageID:  strings.TrimSpace(record.ClientMessageID),
		Content:          record.Content,
		Metadata:         message.CopyMessageMetadata(metadata),
		CreatedAt:        record.CreatedAt,
		EditedAt:         record.EditedAt,
		EditedByMemberID: strings.TrimSpace(record.EditedByMemberID),
		EditRevision:     record.EditRevision,
		MessageType:      strings.TrimSpace(record.MessageType),
	}
	if target.ReplyThread != nil {
		appMessage.ChannelType = "thread"
		appMessage.ParentChannelID = strings.TrimSpace(target.ReplyThread.ParentChannelID)
		appMessage.ParentChannelType = strings.TrimSpace(string(target.ThreadType))
		appMessage.ParentMessageID = strings.TrimSpace(target.ReplyThread.ParentMessageID)
	} else if target.ThreadType == domain.ThreadTypeDM {
		appMessage.ChannelType = string(domain.ThreadTypeDM)
	} else if target.ThreadType == domain.ThreadTypeChannel {
		appMessage.ChannelType = string(domain.ThreadTypeChannel)
	}
	appMessage.ApplyAuthorProfile(authorProfile)
	message.ApplyDefaultContributionSourceToAppMessage(&appMessage)
	return appMessage
}
