package v1

import (
	"context"
	stderrors "errors"
	"fmt"
	"log/slog"
	"time"

	"github.com/pkg/errors"
	"google.golang.org/protobuf/types/known/timestamppb"

	v1pb "github.com/usememos/memos/proto/gen/api/v1"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/store"
)

var (
	errMemoCreatorNotFound     = stderrors.New("memo creator not found")
	errReactionCreatorNotFound = stderrors.New("reaction creator not found")
)

type memoConversionOptions struct {
	displayWithUpdateTime bool
}

func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) {
	creatorMap, err := s.listUsersByID(ctx, []int32{memo.CreatorID})
	if err != nil {
		return nil, errors.Wrap(err, "failed to list memo creators")
	}
	return s.convertMemoFromStoreWithCreators(ctx, memo, reactions, attachments, relations, creatorMap)
}

func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User) (*v1pb.Memo, error) {
	instanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get instance memo related setting")
	}
	return s.convertMemoFromStoreWithCreatorsAndOptions(
		ctx,
		memo,
		reactions,
		attachments,
		relations,
		creatorMap,
		memoConversionOptions{displayWithUpdateTime: instanceMemoRelatedSetting.DisplayWithUpdateTime},
	)
}

func (s *APIV1Service) convertMemoFromStoreWithCreatorsAndOptions(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation, creatorMap map[int32]*store.User, options memoConversionOptions) (*v1pb.Memo, error) {
	displayTs := memo.CreatedTs
	if options.displayWithUpdateTime {
		displayTs = memo.UpdatedTs
	}

	name := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
	creator := creatorMap[memo.CreatorID]
	if creator == nil {
		return nil, errMemoCreatorNotFound
	}
	memoMessage := &v1pb.Memo{
		Name:        name,
		State:       convertStateFromStore(memo.RowStatus),
		Creator:     BuildUserName(creator.Username),
		CreateTime:  timestamppb.New(time.Unix(memo.CreatedTs, 0)),
		UpdateTime:  timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
		DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
		Content:     memo.Content,
		Visibility:  convertVisibilityFromStore(memo.Visibility),
		Pinned:      memo.Pinned,
	}
	if memo.Payload != nil {
		memoMessage.Tags = memo.Payload.Tags
		memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
		memoMessage.Location = convertLocationFromStore(memo.Payload.Location)
	}

	if memo.ParentUID != nil {
		parentName := fmt.Sprintf("%s%s", MemoNamePrefix, *memo.ParentUID)
		memoMessage.Parent = &parentName
	}

	reactionMessages, err := s.convertReactionsFromStoreWithCreators(ctx, reactions, creatorMap)
	if err != nil {
		return nil, errors.Wrap(err, "failed to convert reactions")
	}
	memoMessage.Reactions = reactionMessages

	if relations != nil {
		memoMessage.Relations = relations
	} else {
		memoMessage.Relations = []*v1pb.MemoRelation{}
	}

	memoMessage.Attachments = []*v1pb.Attachment{}
	for _, attachment := range attachments {
		attachmentResponse := convertAttachmentFromStore(attachment)
		memoMessage.Attachments = append(memoMessage.Attachments, attachmentResponse)
	}

	snippet, err := s.getMemoContentSnippet(memo.Content)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get memo content snippet")
	}
	memoMessage.Snippet = snippet

	return memoMessage, nil
}

func (s *APIV1Service) listUsersByIDWithExisting(ctx context.Context, userIDs []int32, existing map[int32]*store.User) (map[int32]*store.User, error) {
	usersByID := make(map[int32]*store.User, len(existing)+len(userIDs))
	for userID, user := range existing {
		if user != nil {
			usersByID[userID] = user
		}
	}

	missingUserIDs := make([]int32, 0, len(userIDs))
	seenMissingUserIDs := make(map[int32]struct{}, len(userIDs))
	for _, userID := range userIDs {
		if _, ok := usersByID[userID]; ok {
			continue
		}
		if _, ok := seenMissingUserIDs[userID]; ok {
			continue
		}
		seenMissingUserIDs[userID] = struct{}{}
		missingUserIDs = append(missingUserIDs, userID)
	}

	if len(missingUserIDs) == 0 {
		return usersByID, nil
	}

	missingUsersByID, err := s.listUsersByID(ctx, missingUserIDs)
	if err != nil {
		return nil, err
	}
	for userID, user := range missingUsersByID {
		if user != nil {
			usersByID[userID] = user
		}
	}
	return usersByID, nil
}

func (s *APIV1Service) convertReactionsFromStoreWithCreators(ctx context.Context, reactions []*store.Reaction, creatorMap map[int32]*store.User) ([]*v1pb.Reaction, error) {
	if len(reactions) == 0 {
		return []*v1pb.Reaction{}, nil
	}

	creatorIDs := make([]int32, 0, len(reactions))
	for _, reaction := range reactions {
		creatorIDs = append(creatorIDs, reaction.CreatorID)
	}
	creatorsByID, err := s.listUsersByIDWithExisting(ctx, creatorIDs, creatorMap)
	if err != nil {
		return nil, err
	}

	reactionMessages := make([]*v1pb.Reaction, 0, len(reactions))
	for _, reaction := range reactions {
		reactionMessage, err := convertReactionFromStoreWithCreators(reaction, creatorsByID)
		if err != nil {
			if stderrors.Is(err, errReactionCreatorNotFound) {
				slog.Warn("Skipping reaction with missing creator",
					slog.Int64("reaction_id", int64(reaction.ID)),
					slog.Int64("creator_id", int64(reaction.CreatorID)),
					slog.String("content_id", reaction.ContentID),
				)
				continue
			}
			return nil, err
		}
		reactionMessages = append(reactionMessages, reactionMessage)
	}
	return reactionMessages, nil
}

func convertReactionFromStoreWithCreators(reaction *store.Reaction, creatorsByID map[int32]*store.User) (*v1pb.Reaction, error) {
	creator := creatorsByID[reaction.CreatorID]
	if creator == nil {
		return nil, errReactionCreatorNotFound
	}

	reactionUID := fmt.Sprintf("%d", reaction.ID)
	return &v1pb.Reaction{
		Name:         fmt.Sprintf("%s/%s%s", reaction.ContentID, ReactionNamePrefix, reactionUID),
		Creator:      BuildUserName(creator.Username),
		ContentId:    reaction.ContentID,
		ReactionType: reaction.ReactionType,
		CreateTime:   timestamppb.New(time.Unix(reaction.CreatedTs, 0)),
	}, nil
}

// batchConvertMemoRelations batch-loads relations for a list of memos and returns
// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.
func (s *APIV1Service) batchConvertMemoRelations(ctx context.Context, memos []*store.Memo, includeSnippets bool) (map[int32][]*v1pb.MemoRelation, error) {
	if len(memos) == 0 {
		return map[int32][]*v1pb.MemoRelation{}, nil
	}

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get user")
	}
	var memoFilter string
	if currentUser == nil {
		memoFilter = `visibility == "PUBLIC"`
	} else {
		memoFilter = fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID)
	}

	memoIDs := make([]int32, len(memos))
	memoIDSet := make(map[int32]bool, len(memos))
	for i, m := range memos {
		memoIDs[i] = m.ID
		memoIDSet[m.ID] = true
	}

	outgoingRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
		SourceMemoIDList: memoIDs,
		MemoFilter:       &memoFilter,
	})
	if err != nil {
		return nil, errors.Wrap(err, "failed to batch list outgoing memo relations")
	}
	incomingRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
		RelatedMemoIDList: memoIDs,
		MemoFilter:        &memoFilter,
	})
	if err != nil {
		return nil, errors.Wrap(err, "failed to batch list incoming memo relations")
	}
	allRelations := mergeMemoRelations(outgoingRelations, incomingRelations)

	// Collect all memo IDs referenced in relations that we need to resolve.
	neededIDs := make(map[int32]bool)
	for _, r := range allRelations {
		neededIDs[r.MemoID] = true
		neededIDs[r.RelatedMemoID] = true
	}

	// Build ID→UID map from the memos we already have.
	memoIDToUID := make(map[int32]string, len(memos))
	memoIDToSnippet := make(map[int32]string, len(memos))
	for _, m := range memos {
		memoIDToUID[m.ID] = m.UID
		if includeSnippets {
			snippet, err := s.getMemoContentSnippet(m.Content)
			if err != nil {
				return nil, errors.Wrap(err, "failed to get memo content snippet")
			}
			memoIDToSnippet[m.ID] = snippet
		}
		delete(neededIDs, m.ID)
	}

	// Batch fetch any additional memos referenced by relations that we don't already have.
	if len(neededIDs) > 0 {
		extraIDs := make([]int32, 0, len(neededIDs))
		for id := range neededIDs {
			extraIDs = append(extraIDs, id)
		}
		extraFind := &store.FindMemo{IDList: extraIDs, ExcludeContent: !includeSnippets}
		extraMemos, err := s.Store.ListMemos(ctx, extraFind)
		if err != nil {
			return nil, errors.Wrap(err, "failed to batch fetch related memos")
		}
		for _, m := range extraMemos {
			memoIDToUID[m.ID] = m.UID
			if includeSnippets {
				snippet, err := s.getMemoContentSnippet(m.Content)
				if err != nil {
					return nil, errors.Wrap(err, "failed to get related memo content snippet")
				}
				memoIDToSnippet[m.ID] = snippet
			}
		}
	}

	// Build the result map: memo ID → its relations (both directions).
	result := make(map[int32][]*v1pb.MemoRelation, len(memos))
	for _, r := range allRelations {
		memoUID, ok1 := memoIDToUID[r.MemoID]
		relatedUID, ok2 := memoIDToUID[r.RelatedMemoID]
		if !ok1 || !ok2 {
			continue
		}

		relation := &v1pb.MemoRelation{
			Memo: &v1pb.MemoRelation_Memo{
				Name:    fmt.Sprintf("%s%s", MemoNamePrefix, memoUID),
				Snippet: memoIDToSnippet[r.MemoID],
			},
			RelatedMemo: &v1pb.MemoRelation_Memo{
				Name:    fmt.Sprintf("%s%s", MemoNamePrefix, relatedUID),
				Snippet: memoIDToSnippet[r.RelatedMemoID],
			},
			Type: convertMemoRelationTypeFromStore(r.Type),
		}

		// Add to the memo that owns this relation (both directions).
		if memoIDSet[r.MemoID] {
			result[r.MemoID] = append(result[r.MemoID], relation)
		}
		if memoIDSet[r.RelatedMemoID] {
			result[r.RelatedMemoID] = append(result[r.RelatedMemoID], relation)
		}
	}

	return result, nil
}

// loadMemoRelations loads relations for a single memo and converts them to API format.
func (s *APIV1Service) loadMemoRelations(ctx context.Context, memo *store.Memo) ([]*v1pb.MemoRelation, error) {
	relationMap, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo}, true)
	if err != nil {
		return nil, err
	}
	return relationMap[memo.ID], nil
}

func mergeMemoRelations(groups ...[]*store.MemoRelation) []*store.MemoRelation {
	seen := make(map[string]struct{})
	merged := make([]*store.MemoRelation, 0)
	for _, relations := range groups {
		for _, relation := range relations {
			key := fmt.Sprintf("%d:%d:%s", relation.MemoID, relation.RelatedMemoID, relation.Type)
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			merged = append(merged, relation)
		}
	}
	return merged
}

func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.Memo_Property {
	if property == nil {
		return nil
	}
	return &v1pb.Memo_Property{
		HasLink:            property.HasLink,
		HasTaskList:        property.HasTaskList,
		HasCode:            property.HasCode,
		HasIncompleteTasks: property.HasIncompleteTasks,
		Title:              property.Title,
	}
}

func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location {
	if location == nil {
		return nil
	}
	return &v1pb.Location{
		Placeholder: location.Placeholder,
		Latitude:    location.Latitude,
		Longitude:   location.Longitude,
	}
}

func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location {
	if location == nil {
		return nil
	}
	return &storepb.MemoPayload_Location{
		Placeholder: location.Placeholder,
		Latitude:    location.Latitude,
		Longitude:   location.Longitude,
	}
}

func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {
	switch visibility {
	case store.Private:
		return v1pb.Visibility_PRIVATE
	case store.Protected:
		return v1pb.Visibility_PROTECTED
	case store.Public:
		return v1pb.Visibility_PUBLIC
	default:
		return v1pb.Visibility_VISIBILITY_UNSPECIFIED
	}
}

func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {
	switch visibility {
	case v1pb.Visibility_PROTECTED:
		return store.Protected
	case v1pb.Visibility_PUBLIC:
		return store.Public
	default:
		return store.Private
	}
}
