package v1

import (
	"context"
	"fmt"
	"time"

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

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

func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUserStatsRequest) (*v1pb.ListAllUserStatsResponse, error) {
	workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get workspace memo related setting")
	}

	normalStatus := store.Normal
	memoFind := &store.FindMemo{
		// Exclude comments by default.
		ExcludeComments: true,
		ExcludeContent:  true,
		RowStatus:       &normalStatus,
	}

	currentUser, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	if currentUser == nil {
		memoFind.VisibilityList = []store.Visibility{store.Public}
	} else {
		if memoFind.CreatorID == nil {
			internalFilter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID)
			if memoFind.Filter != nil {
				filter := fmt.Sprintf("(%s) && (%s)", *memoFind.Filter, internalFilter)
				memoFind.Filter = &filter
			} else {
				memoFind.Filter = &internalFilter
			}
		} else if *memoFind.CreatorID != currentUser.ID {
			memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
		}
	}
	memos, err := s.Store.ListMemos(ctx, memoFind)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
	}
	userStatsMap := map[string]*v1pb.UserStats{}
	for _, memo := range memos {
		creator := fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID)
		if _, ok := userStatsMap[creator]; !ok {
			userStatsMap[creator] = &v1pb.UserStats{
				Name:                  creator,
				MemoDisplayTimestamps: []*timestamppb.Timestamp{},
				MemoTypeStats:         &v1pb.UserStats_MemoTypeStats{},
				TagCount:              map[string]int32{},
			}
		}
		displayTs := memo.CreatedTs
		if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
			displayTs = memo.UpdatedTs
		}
		userStats := userStatsMap[creator]
		userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
		// Handle duplicated tags.
		for _, tag := range memo.Payload.Tags {
			userStats.TagCount[tag]++
		}
		if memo.Pinned {
			userStats.PinnedMemos = append(userStats.PinnedMemos, fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID))
		}
		if memo.Payload.Property.GetHasLink() {
			userStats.MemoTypeStats.LinkCount++
		}
		if memo.Payload.Property.GetHasCode() {
			userStats.MemoTypeStats.CodeCount++
		}
		if memo.Payload.Property.GetHasTaskList() {
			userStats.MemoTypeStats.TodoCount++
		}
		if memo.Payload.Property.GetHasIncompleteTasks() {
			userStats.MemoTypeStats.UndoCount++
		}
		userStats.TotalMemoCount++
	}
	userStatsList := []*v1pb.UserStats{}
	for _, userStats := range userStatsMap {
		userStatsList = append(userStatsList, userStats)
	}
	return &v1pb.ListAllUserStatsResponse{
		UserStats: userStatsList,
	}, nil
}

func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) {
	userID, err := ExtractUserIDFromName(request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}

	normalStatus := store.Normal
	memoFind := &store.FindMemo{
		// Exclude comments by default.
		ExcludeComments: true,
		ExcludeContent:  true,
		CreatorID:       &userID,
		RowStatus:       &normalStatus,
	}

	currentUser, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	visibilities := []store.Visibility{store.Public}
	if currentUser != nil {
		visibilities = append(visibilities, store.Protected)
		if currentUser.ID == user.ID {
			visibilities = append(visibilities, store.Private)
		}
	}
	memoFind.VisibilityList = visibilities
	memos, err := s.Store.ListMemos(ctx, memoFind)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
	}

	workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get workspace memo related setting")
	}
	userStats := &v1pb.UserStats{
		Name:                  fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
		MemoDisplayTimestamps: []*timestamppb.Timestamp{},
		MemoTypeStats:         &v1pb.UserStats_MemoTypeStats{},
		TagCount:              map[string]int32{},
		TotalMemoCount:        int32(len(memos)),
	}
	for _, memo := range memos {
		displayTs := memo.CreatedTs
		if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
			displayTs = memo.UpdatedTs
		}
		userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
		// Handle duplicated tags.
		for _, tag := range memo.Payload.Tags {
			userStats.TagCount[tag]++
		}
		if memo.Pinned {
			userStats.PinnedMemos = append(userStats.PinnedMemos, fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID))
		}
		if memo.Payload.Property.GetHasLink() {
			userStats.MemoTypeStats.LinkCount++
		}
		if memo.Payload.Property.GetHasCode() {
			userStats.MemoTypeStats.CodeCount++
		}
		if memo.Payload.Property.GetHasTaskList() {
			userStats.MemoTypeStats.TodoCount++
		}
		if memo.Payload.Property.GetHasIncompleteTasks() {
			userStats.MemoTypeStats.UndoCount++
		}
	}
	return userStats, nil
}
