package message

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
)

const (
	DefaultSearchLimit                = 20
	MaxSearchLimit                    = 50
	TiDBMessageSearchProjectionName   = "message_search"
	TiDBMessageSearchFinalizeComplete = "complete"
)

func (txs TxStore) SupportsMessageSearch(ctx context.Context) bool {
	if txs.tx == nil {
		return false
	}
	return supportsMessageSearch(ctx, txs.tx)
}

func (txs TxStore) SupportsWorkspaceMessageSearch(ctx context.Context, workspaceID string) bool {
	workspaceID = strings.TrimSpace(workspaceID)
	if txs.tx == nil || workspaceID == "" || !coredb.IsTiDB(txs.tx) {
		return false
	}
	service := scopeAPIServiceForQueryer(txs.tx)
	return service.repos.Search.RequireReadReady(ctx, workspaceID, "") == nil
}

func BuildTiDBFTSMatchLiteral(query string) string {
	normalized := strings.Join(strings.Fields(query), " ")
	replacer := strings.NewReplacer(
		"\\", "\\\\",
		`'`, `''`,
	)
	return "'" + replacer.Replace(normalized) + "'"
}

func BuildTiDBFTSMatchCondition(query string, contentExpr string) string {
	return fmt.Sprintf("fts_match_word(%s, %s)", BuildTiDBFTSMatchLiteral(query), contentExpr)
}

func TiDBMessageSearchReadReady(ctx context.Context, queryer coredb.QueryRower) bool {
	if ctx == nil {
		ctx = context.Background()
	}
	if queryer == nil {
		return false
	}

	var readReady bool
	err := queryer.QueryRow(ctx, `
SELECT EXISTS(
    SELECT 1
    FROM scope_message_search_state
    WHERE projection_name = $1
      AND migration_status = 'complete'
      AND backfill_status = 'complete'
      AND finalize_status = 'complete'
      AND read_ready = TRUE
)
`, TiDBMessageSearchProjectionName).Scan(&readReady)
	return err == nil && readReady
}

func RequireTiDBMessageSearchFinalized(ctx context.Context, queryer coredb.QueryRower) error {
	var (
		migrationStatus string
		backfillStatus  string
		finalizeStatus  string
		readReady       bool
	)
	err := queryer.QueryRow(ctx, `
SELECT migration_status, backfill_status, finalize_status, read_ready
FROM scope_message_search_state
WHERE projection_name = $1
ORDER BY CASE WHEN scope_id = '' THEN 0 ELSE 1 END
LIMIT 1
`, TiDBMessageSearchProjectionName).Scan(&migrationStatus, &backfillStatus, &finalizeStatus, &readReady)
	if err != nil {
		if coredb.IsNotFound(err) {
			return requestError{kind: ErrMessageSearchUnavailable, message: "message search is unavailable because the TiDB projection state is missing"}
		}
		return fmt.Errorf("load scope message search projection state: %w", err)
	}
	if strings.TrimSpace(migrationStatus) != TiDBMessageSearchFinalizeComplete ||
		strings.TrimSpace(backfillStatus) != TiDBMessageSearchFinalizeComplete ||
		strings.TrimSpace(finalizeStatus) != TiDBMessageSearchFinalizeComplete ||
		!readReady {
		return requestError{kind: ErrMessageSearchUnavailable, message: "message search is unavailable until the TiDB projection is ready"}
	}
	return nil
}

func supportsMessageSearch(ctx context.Context, queryer coredb.QueryRower) bool {
	if queryer == nil || !coredb.IsTiDB(queryer) {
		return false
	}
	return TiDBMessageSearchReadReady(ctx, queryer)
}

// SearchMessagesForUser searches messages across channels the user is a member of.
func (txs TxStore) SearchMessagesForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	query string,
	scopeID string,
	options MessageSearchOptions,
) (MessageSearchResult, error) {
	query = strings.TrimSpace(query)
	if query == "" {
		return MessageSearchResult{Messages: []AppMessage{}}, nil
	}
	if !coredb.IsTiDB(txs.tx) {
		return MessageSearchResult{}, requestError{kind: ErrMessageSearchUnavailable, message: "message search is unavailable without a TiDB core database"}
	}
	return txs.searchTiDBMessagesForUser(ctx, userID, workspaceID, query, scopeID, options)
}

func (txs TxStore) searchTiDBMessagesForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	query string,
	scopeID string,
	options MessageSearchOptions,
) (MessageSearchResult, error) {
	membership, err := loadAuthorizedHumanWorkspaceMember(ctx, txs.tx, userID, workspaceID)
	if err != nil {
		return MessageSearchResult{}, err
	}
	options, err = normalizeMessageSearchOptions(options)
	if err != nil {
		return MessageSearchResult{}, err
	}
	service := scopeAPIServiceForQueryer(txs.tx)
	if err := service.repos.Search.RequireReadReady(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID)); err != nil {
		return MessageSearchResult{}, err
	}
	loadPage := func(ctx context.Context, beforeCursor MessageSearchCursor, pageLimit int) (VisibilityFilterResult, error) {
		return service.searchVisible(ctx, ScopeAPISearchRequest{
			WorkspaceID:    workspaceID,
			ScopeID:        strings.TrimSpace(scopeID),
			ViewerMemberID: membership.Member.ID,
			ViewerType:     ScopeActorHuman,
			Query:          query,
			Limit:          pageLimit,
			BeforeCursor:   beforeCursor,
			AuthorFilter:   options.AuthorFilter,
		}, pageLimit)
	}

	var (
		refs       []VisibleMessageRef
		nextCursor MessageSearchCursor
		hasMore    bool
	)
	if options.UseCursor {
		refs, nextCursor, hasMore, err = collectSearchVisibleRefsForCursor(ctx, options.Limit, options.Cursor, strings.TrimSpace(scopeID), loadPage)
	} else {
		refs, hasMore, err = collectSearchVisibleRefsForOffset(ctx, options.Limit, options.Offset, loadPage)
	}
	if err != nil {
		return MessageSearchResult{}, err
	}
	messages, err := txs.appMessagesFromVisibleRefs(ctx, workspaceID, refs)
	if err != nil {
		return MessageSearchResult{}, err
	}
	if len(messages) > options.Limit {
		messages = messages[:options.Limit]
		hasMore = true
	}
	if err := txs.attachServiceAuthorProfilePresentation(ctx, workspaceID, messages); err != nil {
		return MessageSearchResult{}, err
	}
	if err := RedactServiceAccountPublisherMetadataForViewer(ctx, txs.tx, workspaceID, membership.Member.ID, messages); err != nil {
		return MessageSearchResult{}, err
	}
	if err := AttachResolvedMentionsForAppMessages(ctx, txs.tx, workspaceID, membership.Member.ID, messages); err != nil {
		return MessageSearchResult{}, err
	}
	if err := AttachMessagePinStateForAppMessages(ctx, txs.tx, workspaceID, membership.Member.ID, messages); err != nil {
		return MessageSearchResult{}, err
	}
	totalCount := options.Offset + len(messages)
	if hasMore {
		totalCount++
	}
	return MessageSearchResult{
		Messages:   messages,
		TotalCount: totalCount,
		HasMore:    hasMore,
		NextCursor: nextCursor,
	}, nil
}

func normalizeMessageSearchOptions(options MessageSearchOptions) (MessageSearchOptions, error) {
	limit, offset := options.Limit, options.Offset
	if limit <= 0 {
		limit = DefaultSearchLimit
	}
	if limit > MaxSearchLimit {
		limit = MaxSearchLimit
	}
	if offset < 0 {
		offset = 0
	}
	cursor, err := normalizeMessageSearchCursor(options.Cursor)
	if err != nil {
		return MessageSearchOptions{}, err
	}
	if options.UseCursor && offset > 0 {
		return MessageSearchOptions{}, badRequest("search cursor and offset cannot both be set")
	}
	if !options.UseCursor {
		cursor = MessageSearchCursor{}
	}
	return MessageSearchOptions{
		Limit:        limit,
		Offset:       offset,
		Cursor:       cursor,
		UseCursor:    options.UseCursor,
		AuthorFilter: options.AuthorFilter,
	}, nil
}

type searchVisiblePageLoader func(context.Context, MessageSearchCursor, int) (VisibilityFilterResult, error)

func collectSearchVisibleRefsForOffset(ctx context.Context, limit int, offset int, load searchVisiblePageLoader) ([]VisibleMessageRef, bool, error) {
	resultLimit := limit + 1
	if resultLimit <= 0 {
		return []VisibleMessageRef{}, false, nil
	}

	refs := make([]VisibleMessageRef, 0, resultLimit)
	remainingOffset := offset
	beforeCursor := MessageSearchCursor{}
	hasMore := false

	for len(refs) < resultLimit {
		pageLimit := MaxSearchLimit
		if remainingOffset <= 0 {
			remainingNeeded := resultLimit - len(refs)
			if remainingNeeded < pageLimit {
				pageLimit = remainingNeeded
			}
		}
		if pageLimit <= 0 {
			break
		}

		page, err := load(ctx, beforeCursor, pageLimit)
		if err != nil {
			return nil, false, err
		}

		pageHasMore := page.HasMore || page.CandidateBudgetExhausted
		pageCursor := page.NextSearchCursor
		if pageCursor.IsZero() && page.NextCursor > 0 {
			pageCursor = MessageSearchCursor{GlobalSeq: page.NextCursor, Seq: page.NextCursor}
		}
		if len(page.Messages) == 0 {
			hasMore = pageHasMore
			if !pageHasMore || pageCursor.IsZero() || pageCursor == beforeCursor {
				break
			}
			beforeCursor = pageCursor
			continue
		}

		if remainingOffset >= len(page.Messages) {
			remainingOffset -= len(page.Messages)
		} else {
			start := remainingOffset
			remainingOffset = 0
			candidates := page.Messages[start:]
			needed := resultLimit - len(refs)
			if len(candidates) > needed {
				refs = append(refs, candidates[:needed]...)
				hasMore = true
				break
			}
			refs = append(refs, candidates...)
		}

		if len(refs) >= resultLimit {
			hasMore = true
			break
		}
		hasMore = pageHasMore
		if !pageHasMore || pageCursor.IsZero() || pageCursor == beforeCursor {
			break
		}
		beforeCursor = pageCursor
	}

	return refs, hasMore, nil
}

func collectSearchVisibleRefsForCursor(ctx context.Context, limit int, beforeCursor MessageSearchCursor, scopeID string, load searchVisiblePageLoader) ([]VisibleMessageRef, MessageSearchCursor, bool, error) {
	resultLimit := limit + 1
	if resultLimit <= 0 {
		return []VisibleMessageRef{}, MessageSearchCursor{}, false, nil
	}
	beforeCursor, err := normalizeCompleteMessageSearchCursor(beforeCursor, scopeID)
	if err != nil {
		return nil, MessageSearchCursor{}, false, err
	}

	page, err := load(ctx, beforeCursor, resultLimit)
	if err != nil {
		return nil, MessageSearchCursor{}, false, err
	}
	hasMore := page.HasMore || page.CandidateBudgetExhausted
	nextCursor := page.NextSearchCursor
	refs := page.Messages
	if len(refs) > limit {
		refs = refs[:limit]
		hasMore = true
		lastRef := refs[len(refs)-1]
		nextCursor = searchCandidateCursor(lastRef.Message, strings.TrimSpace(scopeID), lastRef.SearchRank)
	} else if hasMore && nextCursor.IsZero() && len(refs) > 0 {
		lastRef := refs[len(refs)-1]
		nextCursor = searchCandidateCursor(lastRef.Message, strings.TrimSpace(scopeID), lastRef.SearchRank)
	}
	if !hasMore {
		nextCursor = MessageSearchCursor{}
	}
	return refs, nextCursor, hasMore, nil
}

// ScanSearchMessageRow scans an AppMessage row with an additional
// parent_message_id column used by search results to support reply thread
// navigation.
func ScanSearchMessageRow(scanner interface {
	Scan(...any) error
}) (AppMessage, error) {
	var message AppMessage
	var memberType string
	var userID string
	var userPublicName string
	var agentID string
	var agentName string
	var authorDisplayName string
	var authorMemberID string
	var rawMetadata []byte
	var parentChannelID string
	var parentMessageID string
	if err := scanner.Scan(
		&message.ID,
		&message.Seq,
		&message.ChannelID,
		&memberType,
		&authorMemberID,
		&userID,
		&userPublicName,
		&agentID,
		&agentName,
		&authorDisplayName,
		&message.MessageType,
		&message.Content,
		&rawMetadata,
		&message.CreatedAt,
		&parentChannelID,
		&parentMessageID,
	); err != nil {
		if coredb.IsNotFound(err) {
			return AppMessage{}, ErrNotFound
		}
		return AppMessage{}, fmt.Errorf("scan search message: %w", err)
	}

	switch memberType {
	case "human":
		message.ApplyAuthorProfile(AuthorProfile{
			ActorType:   "human",
			PrincipalID: strings.TrimSpace(userID),
			MemberID:    strings.TrimSpace(authorMemberID),
			TargetID:    strings.TrimSpace(userPublicName),
			DisplayName: identity.PreferredDisplayName(authorDisplayName, userPublicName, authorMemberID),
		})
	case "agent":
		message.ApplyAuthorProfile(AuthorProfile{
			ActorType:   "agent",
			PrincipalID: strings.TrimSpace(agentID),
			MemberID:    strings.TrimSpace(authorMemberID),
			TargetID:    strings.TrimSpace(agentName),
			DisplayName: identity.PreferredDisplayName(authorDisplayName, agentName, authorMemberID),
		})
	case "service":
		message.ApplyAuthorProfile(AuthorProfile{
			ActorType:   "service",
			PrincipalID: strings.TrimSpace(authorMemberID),
			MemberID:    strings.TrimSpace(authorMemberID),
			TargetID:    strings.TrimSpace(authorMemberID),
			DisplayName: identity.PreferredDisplayName(authorDisplayName, authorMemberID),
		})
	default:
		return AppMessage{}, fmt.Errorf("invalid author type %q", memberType)
	}
	metadata, err := DecodeMessageMetadata(rawMetadata)
	if err != nil {
		return AppMessage{}, err
	}
	message.Metadata = metadata

	if parentMessageID != "" {
		message.ParentChannelID = strings.TrimSpace(parentChannelID)
		message.ParentMessageID = strings.TrimSpace(parentMessageID)
	}
	ApplyDefaultContributionSourceToAppMessage(&message)

	return message, nil
}

func loadAuthorizedHumanWorkspaceMember(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
	membership, err := access.LoadAuthorizedHumanWorkspaceMember(ctx, queryer, userID, workspaceID, access.HumanAction{
		Resource: access.ResourceWorkspace,
		Action:   access.ActionAccess,
	})
	if err != nil {
		if errors.Is(err, access.ErrForbidden) {
			return access.HumanWorkspaceMember{}, ErrForbidden
		}
		return access.HumanWorkspaceMember{}, fmt.Errorf("load authorized human workspace member: %w", err)
	}
	return membership, nil
}

func scopeAPIServiceForQueryer(queryer coredb.QueryExecer) ScopeAPIService {
	return ScopeAPIService{
		db:         queryer,
		repos:      NewScopeRepositories(queryer),
		visibility: NewVisibilityFilter(),
	}
}

func (txs TxStore) appMessagesFromVisibleRefs(ctx context.Context, workspaceID string, refs []VisibleMessageRef) ([]AppMessage, error) {
	presentations, err := loadScopeMemberPresentations(ctx, txs.tx, workspaceID, visibleRefAuthorMemberIDs(refs))
	if err != nil {
		return nil, err
	}
	routeContexts, err := loadAppMessageRouteContexts(ctx, txs.tx, workspaceID, visibleRefScopeIDs(refs))
	if err != nil {
		return nil, err
	}
	messageIDs := make([]string, 0, len(refs))
	for _, ref := range refs {
		messageIDs = append(messageIDs, ref.Message.ID)
	}
	attachmentsByMessageID, err := txs.loadAttachmentsForMessages(ctx, workspaceID, messageIDs)
	if err != nil {
		return nil, err
	}
	messages := make([]AppMessage, 0, len(refs))
	for _, ref := range refs {
		metadata, err := DecodeMessageMetadata(ref.Message.Metadata)
		if err != nil {
			return nil, err
		}
		presentation := presentations[strings.TrimSpace(ref.Message.AuthorMemberID)]
		appMessage := AppMessage{
			ID:               ref.Message.ID,
			Seq:              ref.Message.Seq,
			ChannelID:        ref.Message.ScopeID,
			Content:          ref.Message.Content,
			Metadata:         metadata,
			Attachments:      attachmentsByMessageID[ref.Message.ID],
			CreatedAt:        ref.Message.CreatedAt,
			EditedAt:         ref.Message.EditedAt,
			EditedByMemberID: ref.Message.EditedByMemberID,
			EditRevision:     ref.Message.EditRevision,
			MessageType:      ref.Message.MessageType,
		}
		if routeContext, ok := routeContexts[strings.TrimSpace(ref.Message.ScopeID)]; ok {
			applyAppMessageRouteContext(&appMessage, routeContext)
		}
		appMessage.ApplyAuthorProfile(presentation.AuthorProfile())
		messages = append(messages, appMessage)
	}
	if err := AttachContributionSourcesForAppMessages(ctx, txs.tx, workspaceID, messages); err != nil {
		return nil, err
	}
	return messages, nil
}

func visibleRefAuthorMemberIDs(refs []VisibleMessageRef) []string {
	memberIDs := make([]string, 0, len(refs))
	for _, ref := range refs {
		memberIDs = append(memberIDs, ref.Message.AuthorMemberID)
	}
	return memberIDs
}
